I’m going to argue that
add_action is actually a more elaborate version of
goto hidden in plain sight, an anti-pattern, that it encourages unstructured programming, and obscures dependencies between code by providing the illusion of structured code, and encouraging spaghetti architecture.
The Dangers of Goto
Firstly what is a
goto (GOTO, GO TO or other case combinations, depending on the programming language) is a statement found in many computer programming languages. It performs a one-way transfer of control to another line of code; in contrast a function call normally returns control. The jumped-to locations are usually identified using labels, though some languages use line numbers. At the machine code level, a goto is a form of branch or jump statement. Many languages support the goto statement, and many do not; see language support for discussion.
goto statement itself was added in PHP 5.3, and has been present in programming for decades. It’s famous for being a notorious omen of bad code. It’s caused problems everywhere from scaling code, to preventing auto-optimising compilers from doing their job.
Computer science has proven that
goto like statements are unnecessary to construct a program via the structured program theorem, and giants of computer science and programming have poured scorn on the
goto statement since its inception. Few aspects of coding have become so ingrained as to warrant an XKCD comic warning of their use.
The primary danger faced by users of
goto is that it completely bypasses any structure or order in your program by jumping into a completely different context without warning.
do_action(), you’re doing the same thing. Code execution heads to a collection of arbitrary functions that have registered themselves as interested. These callable items may be anywhere in the code, they may call more actions and trigger more events, and they may loop back on each other. Some may not exist in the normal sense as anonymous functions with no means of manipulating them via normal methods.
Coupled with the lack of an abort/termination procedure, and loose controls, this makes the hook system dangerously opaque, an unstructured wild goose chase of unknowns.
A Note on add_filter
Filtering values is not the same as actioning an event. When passing a value into a filter, your expectation is that the filter modifies the value. Actions however have business logic. They don’t just modify values, they create things and do work, and one should never do work in a filter.
Consider this code for a moment:
View the code on Gist.
How would you remove that action? The object it’s bound to lives inside the filter system with no other references, so you cannot reconstruct the value passed into
add_action. To do so you would need to delve deep into the internal implementation of the filter system.
The ‘all’ hook
Every action/hook fired also fires the ‘all’ action. You can use this to intercept all actions and filters, but in doing so you pay a hefty performance cost.
jQuery( document ).click() style event bindings is frowned upon.
It encourages a sprawling disorganised collection of procedural code, with no incentive to pull common functionality together into common files. Why might this be a problem? Lets take a look at a WordPress callgraph:
Click on the image for the full graph, be warned though, the image is 7k x 9k and 3MB after being ran through photoshop optimisation ( previously 7.5MB PNG-24 ). Here we see a spahghetti bowl architecture. The bold line running from the start to the end is the critical path PHP takes through the program from start to end.
We can see here that WordPress core has a choke point at
apply_filters consuming as much as 21% of total page load time.
Here is an example of a Drupal callgraph:
Far more orderly and structured. What about the Drupal equivalent of hooks and filters? Hooks in Drupal take the form of a function named
modulename_hookname. This enforces structure and modularisation. It’s easy to check if a module makes use of a hook, how many pieces of code have a hook, and where to expect a modules entry point for an event will be situated. It could be better but it has advantages. You can read more on Drupal hooks and events here. Note that I am not advocating adoption of the Drupal system.
Another example of a better callbacks and event hooks/actions would be the DOM. While it isn’t a perfect system, one can handle and abort the propagation of an event. Code is bound to an actual location on the DOM rather than arbitrary identifiers, with well defined behavior concerning the flow of an event as it bubbles up the DOM.
Callgraphs can differ depending on the environment they’re ran in, but you can generate your own callgraphs using xhprof.
Loops & Misdirection
Consider this code:
View the code on Gist.
Here we can see an API that provides an action indicating it has done its job. We then add an action to do operations when the action is fired, causing an infinite loop.
While this is a simple case that’s easier to debug, once actions call functions that fire actions, the layering can obscure the program flow. This non-linear flow is confusing and hard to visualise.
Functions may directly interact through a changing tree of callbacks and actions, despite being in unrelated areas of WordPress with no documentation or indication they’re related. This is one of the reasons why disabling all plugins and reverting to the default theme is such an effective debugging tool in WordPress, and that isn’t a good thing.
do_action & Global State
Consider this code providing useful action hooks:
View the code on Gist.
You have written tests to ensure that this function does what it needs to do. When you run your test suite the test passes, however, when you run the test in isolation it fails. What could the issue be?
Consider the reverse situation. The functions performs flawlessly in isolation, but as part of a greater whole, fails miserably. What then?
The problem here is that the functionality of the code has undeclared dependencies in the global state. Things are being set up or initialised during these hooks that are needed for the function to work, but anybody looking at the function would be unaware. It may also be that the expected environment is changed during the hook and the assumptions held no longer apply.
What should happen is that the code should require all of its dependencies up front, such as posts, APIs, and other objects, allowing proper isolation and self documentation. These dependencies can then be intercepted, wrapped, overridden, or manipulated as is necessary.
For example, here is a plugin that separates out its data into a model, that’s then passed through a filter. It can be replaced with a new object implementing the same interface, or a wrapper object that intercepts values as methods are called. This way hooks and filters are unnecessary, yet structure remains intact in a linear, easier to debug way.
WordPress has a strong incentive to keep backwards compatibility, and this means keeping deprecated functions and being careful with the API. Part of this API is hooks and filters, so they’re never going away entirely, but that isn’t the issue. The issue is that hooks being used as a paradigm for software development are hiding and obscuring the lack of clean high level architecture in WordPress by fooling people into thinking it is a high level architecture.
There are a multitude of basic fundamental design patterns that could be applied to WordPress core with trivial effort and immense long term gains. For example, if global variables were stored in a container object instead of the global namespace, or if files were internally arranged appropriately into modules. These could be done without changing hooks or the function APIs.
New people exposed to this paradigm get caught up in the process, and people believe it is the best way to do things. It may well be that the core developers have no idea that this is the case.
In conclusion, I would recommend the following:
- WordPress style actions and hooks are massively misused, and dangerous.
- Actions and hooks obscure program flow making debugging and design hard to manage
- Choke points are introduced, making architectural decisions difficult, and introducing unnecessary complexity
- WP Core code complexity can be simplified in a backwards compatible way by instead relying on good OO design, modularisation, and use of good design patterns, such as wrappers, encapsulation, and other means.
- JS code should inherit the well studied and applied lessons of the JS frameworks, rather than reinvent a failed paradigm in a new language
There is no magic fix going forward, but actions and hooks are neither healthy or necessary.