I inherited a WooCommerce project that was a total mess: years of different developers, overlapping features, and a dozen custom plugins piled on top of each other. The client’s complaint was simple. “Sometimes, when we update a product, the cache doesn’t clear. Other times, it clears twice.” The culprit was a chaotic web of add_action and add_filter calls scattered across class constructors all over the codebase. To work out what any single class was responsible for, you had to read the whole file. There was no clear contract, and that is where PHP interfaces come in.
Putting your hooks in a class constructor seems logical at first. The object gets created, the hooks get added, done. But it makes for tightly coupled code that is a pain to test and hard to follow at a glance. My first thought was to refactor the hooks into a dedicated init() method on each class. It did look a bit cleaner, but it did not solve the real problem. I was just moving the mess around, and the code still was not predictable.
Using PHP interfaces to register hooks
An interface is just a contract. It holds no logic of its own. It is a set of rules that says, “any class that implements me must have these specific public methods.” That forces a predictable structure onto your code. Instead of guessing where hooks get registered, you know exactly where to look, and that makes a real difference.
So we define a simple contract for any class that needs to register WordPress actions. The interface requires a single method: get_actions().
interface ActionHookSubscriberInterface {
/**
* Returns an array of actions that the object needs to be subscribed to.
*
* @return array
*/
public static function get_actions();
}
Now any class in the plugin can “sign” this contract. It agrees to provide a static method named get_actions that returns an array of its hooks. From there we can build a simple manager to handle registration for us. I first saw this general approach laid out over at carlalexander.ca, and it holds up well.
class HookManager {
public function register( $object ) {
if ( $object instanceof ActionHookSubscriberInterface ) {
foreach ( $object::get_actions() as $hook => $method ) {
add_action( $hook, [ $object, $method ] );
}
}
}
}
// And here's a class that uses it:
class My_Custom_Class implements ActionHookSubscriberInterface {
public static function get_actions() {
return [
'save_post' => 'clear_post_cache',
];
}
public function clear_post_cache( $post_id ) {
// ... do the actual work here
}
}
// Now, registration is clean.
$manager = new HookManager();
$my_class = new My_Custom_Class();
$manager->register( $my_class );
So what’s the real payoff?
The payoff is decoupling and predictability. The HookManager does not need to know anything about My_Custom_Class beyond the fact that it fulfills the ActionHookSubscriberInterface contract. That is all. Your classes no longer register themselves; they just declare the hooks they need.
- It documents itself. When you see
implements ActionHookSubscriberInterface, you immediately know the class registers action hooks. - It is easy to test. You can test your class methods without ever touching the WordPress event system.
- It is easier to maintain. When a new developer joins, they do not have to hunt for hooks. They just look for the contract.
This stuff gets complicated fast. If you are tired of debugging someone else’s mess and just want your site to work, drop my team a line. We have probably seen it before.
Keep your hooks out of constructors and think in terms of contracts instead. It takes a little more setup up front, but it saves you a lot of pain down the road.