For as long as I’ve been a WordPress developer, I’ve had a lot of people ask “where should I add my WordPress hooks?” Yes, this is a sign I need more friends who are not WordPress developers, but that’s not the point of this article.
Of course, there is no right answer. While ultimately, the answer depends on a variety of things, I want to share a few different patterns and discuss pros and cons of each. That said, I think that these discussions miss an important point: The WordPress Plugins API — aka hooks — is an API that we need to be careful of coupling our code to strongly to.
Writing code that is decoupled from the APIs it interacts with is an important part of writing good code. At the same time, the WordPress Plugins API, or hooks, is so essential to how WordPress works, decoupling from the plugins API is not always practical. But it is still an important goal. It might not always be possible, but you will thank yourself later when you need to write tests or reuse the logic of a hook’s callback.
So, let’s look at a few places to add a hook and the pros and cons of those locations. We will keep our focus on how strongly or weakly coupled this code is, and why that may or may not be a problem.
In the Constructor
When most of us learn how to use OOP PHP in a WordPress context, we generally learn to put hooks in the class constructor. Class constructors are magic methods that run when the class is instantiated. This pattern leads to the hooks being added magically.
Here is a fairly typical implementation of this pattern:
<?php class Term_Control_1 { /** * Create object */ public function __construct(){ add_action( 'save_post', [ $this, 'save_post' ], 10 ); } /** * When post is updated, check if has category 7 or not and add or remove custom field * * @param $id */ public function save_post( $id ){ remove_action( 'save_post', [ $this, 'save_post' ] ); if( ! empty( $_POST[ 'post_category' ] ) && in_array( 7, $_POST[ 'post_category' ] ) ){ update_post_meta( $id, '_fancy_cats', 1 ); }else{ delete_post_meta( $id, '_fancy_cats', 1 ); } } } new Term_Control_1();
In this code, we declare a class that will cause a callback function to run on the save_post hook. At save_post, we check if a specific category is being set on the post and update a custom field based on that. To make sure this code runs, we instantiate the class at the bottom of the file.
This totally works, but it is very strongly coupled to both the POST super global — and therefore an HTTP request — and the plugins API.
What if we want to run this code somewhere else, using an arbitrary post ID? To do that, we’d have to create another instance of the class, which is doable. We’d also need to change the POST superglobal, which is possible but messy.
How would we unit test this? We would have to mock the incoming HTTP request and the plugins API. This is doable — check out wp_mock — but I’d like to test everything in isolation.
This approach works but has issues. Also, we can’t remove the hook, which could be a problem. Also, this class it instantiated on every request, which is wasteful. A minor point when we look at one file, but if this pattern is repeated, that can add up.
Adding the Hook with a Function
Using a function to handle hooks is necessary when not using classes, but is also great for an object-oriented design approach. The callback function, a function is used just to handle translating from one specific context to the proper dependency the class needs.
This allows the class to act as a system for updating post meta based on categories, in any context and the function to be the connector to the plugins API.
This class doesn’t have any interaction with the Plugins API or the current HTTP request. That is not its concern. This class’ concern is the logic of the system, not to interact with other APIs. This is a practical example of the benefits of following the principle of separation of concerns.
This class isn’t concerned with how it gets its inputs, its only concerned with what it does with its inputs. We still need a connection to those inputs — POST data and the Plugins API. So, we will need to have a function for that:
<?php add_action( 'save_post', 'my_update_post', 10, 2 ); function my_update_post( $id, $post ){ if( ! empty( $_POST[ 'post_category' ] ) ){ $categories = $_POST[ 'post_category' ]; $control = new Term_Control_2( $post, $categories ); $control->update_categories(); } }
This function is just concerned with connecting this class to the outside world. As a nice bonus, removing this hook is now very easy. More importantly, we can reuse the class on any post, with any array of categories. We could extend this class to change the field, or category, or even use them add or remove field methods separately from the internal logic.
Now we can test the logic of the class, as well as the hook and callback function separately.
Using Static Methods
You could have a class with static methods that handle the callbacks for your hooks. This strategy has a lot of the same benefits that we’ve covered before but also gives you the organizational benefits of using classes to structure your code.
This example moves from a function to a class with static methods to do basically the same thing:
<?php add_action( 'save_post', [ 'Term_Control_Hooks', 'save_post' ], 10, 2 ); class Term_Control_Hooks { public static function save_post( $id, $post ){ if( ! empty( $_POST[ 'post_category' ] ) ){ $categories = $_POST[ 'post_category' ]; $control = new Term_Control_2( $post, $categories ); $control->update_categories(); } } }
Right now, this isn’t necessarily better. It’s probably a few nanoseconds slower and doesn’t have real benefits. But with a slight change, it can become something more useful.
Let’s change the action we use to “admin_int” and use that to call an “add_hooks” method of this class:
<?php add_action( 'admin_init', [ 'Term_Control_Hooks', 'admin_hooks' ] ); class Term_Control_Hooks { public static function admin_hooks(){ add_action( 'save_post', [ __CLASS__, 'save_post' ], 10, 2 ); } public static function save_post( $id, $post ){ if( ! empty( $_POST[ 'post_category' ] ) ){ $categories = $_POST[ 'post_category' ]; $control = new Term_Control_2( $post, $categories ); $control->update_categories(); } } }
In this example, it does the same thing, but if we have other parts of the system that we need to run on other admin hooks, we can do that without any other major changes.
<?php add_action( 'admin_init', [ 'Term_Control_Hooks', 'admin_hooks' ] ); class Term_Control_Hooks { public static function admin_hooks(){ add_action( 'save_post', [ __CLASS__, 'save_post' ], 10, 2 ); add_action( 'admin_enqueue_scripts', [ __CLASS__ , 'load_scripts' ] ); } public static function save_post( $id, $post ){ if( ! empty( $_POST[ 'post_category' ] ) ){ $categories = $_POST[ 'post_category' ]; $control = new Term_Control_2( $post, $categories ); $control->update_categories(); } } public static function load_scripts(){ } }
Now changes to our class to add new hooks just need a new method and an add_action() or add_fitler() call. I like this as it will keep this class as a layer for connecting to the plugins API while keeping the logic outside of this connection class.
Hooks in the Class
I do often put hooks in the same class as their logic. It’s still strongly coupled, but sometimes this is worth it in terms of keeping related code together. Sometimes the advantages of readability are more important than other concerns. But, I still don’t like using the constructor, as that gives the constructor the second concern in addition to setting up the state of the object.
Here is an example where the class for appending a call to action message to a post using “the_content” filter. This class has separate methods for adding and removing the hook, as well as the actual callback:
<?php class Add_CTA_To_Content { /** * Call To Action to add to post * * @var string */ protected $call_to_action; /** * Add_CTA_To_Content constructor. * * @param string $call_to_action Call To Action to add to post */ public function __construct( $call_to_action ){ $this->call_to_action = $call_to_action; } /** * Add the filter */ public function add_hook(){ add_filter( 'the_content', [ $this, 'filter_content' ] ); } /** * Remove the filter */ public function remove_hook(){ remove_filter( 'the_content', [ $this, 'filter_content' ] ); } /** * Append CTA to post content * * @param string $content * * @return string */ public function filter_content( $content ){ return $content . $this->call_to_action; } }
Each method in this class has one concern. In that sense, we are following the single responsibility principle, according to the definition that every function or class should have one job. But, by the same definition, the class is not, because this class has two jobs: interacting with the Plugins API and adding to a post’s content.
I do like this design. IT keeps the whole system in one single unit. The system can be reused in a different context. We don’t have to use the _content filter with it. Take a look at this example:
<?php add_action( 'save_post', 'save_cta', 10, 2 ); function save_cta( $id, $post ){ remove_action( 'save_post', 'save_cta' ); $appender = new Add_CTA_To_Content( get_option( 'saved_cta' ) ); $post->post_content = $appender->filter_content( $post->post_content ); wp_update_post( $post ); } Raw
We are now using the same code to apply the same change when saving the post. This example shows how this class could survive being disconnected from the plugins API entirely.
According to Tom McFarlin, thinking of the single responsibility principle as “A class, and each of its methods should have one job.” Instead, he says we should think about it by asking the question ““If this class was to change, what would need to be changed?” if that question has more than one answer than the single responsibility principle has been violated.
Asking this question of this class only gives us one answer.These principles of software design are not about punching arbitrary rules. They are about making code that is easy to work with and easy to refactor. Following them makes for more stable systems that don’t require major factors to make one change
Where Isn’t the Point
There are a lot more places you could add your hooks. For example, your project may require a more robust hook manager system. But I’ll stop here because the point of this article was to get you thinking about the consequences of your design decisions when it comes to the Plugins API.
Thinking about your decisions in terms of flexibility, testability and refactoring lead to better code. It’s a practical way to think about the single responsibility and the separation of concerns principles in practical terms.
Like most questions. I can’t give you one answer if you ask me “where should I put my hooks?” All the different ways work. So for each of these methods, consider why you would choose that method and what effects it will have good and bad.
No Comments