A client came in with a plugin that was a genuine nightmare. It ran a critical part of their inventory system, but every time the team added a feature, something else broke. The code was a tangle of classes calling each other directly, with add_action and add_filter calls buried deep in constructors and methods. Total spaghetti. They were losing money, and hair, fast.
My first move was to isolate the worst offender, a class called Product_Sync. I figured I would refactor it, clean it up, and get a quick win. That did not work. It turned out Product_Sync was calling methods directly in Order_Manager, the User_Profile class, and two other custom classes. Touch one and the rest fell over. That is where the WordPress mediator pattern stops being an academic idea and becomes genuinely useful.
Understanding the WordPress mediator pattern
Think about it like an airport. You don’t want every pilot talking directly to every other pilot to figure out who lands when. It would be chaos. Instead, every plane talks to the control tower. The tower is the single source of truth, the mediator that coordinates everything. No plane talks to another plane directly.
WordPress already gave you a control tower: the Plugin API. The whole system of hooks and filters is one big built-in mediator. When you cram add_action() calls inside a random class method, you are letting a 747 talk straight to a Cessna and ignoring the system built to keep order. The move is to work with the mediator instead of fighting it. I first saw this explained well on carlalexander.ca: treat the Plugin API as the coordinator it already is.
So how do you actually do it?
You stop putting hooks inside your logic classes. Instead, you add a dedicated class, a subscriber or a manager, whose only job is to talk to the control tower. Your core logic classes stay clean and have no idea that WordPress hooks exist. They just do their work.
<?php
/**
* This class ONLY talks to WordPress hooks. Nothing else.
* This is our control tower operator.
*/
class Hook_Subscriber {
protected $inventory_manager;
public function __construct( Inventory_Manager $inventory_manager ) {
$this->inventory_manager = $inventory_manager;
}
public function register_hooks() {
add_action( 'woocommerce_order_status_completed', array( $this->inventory_manager, 'reduce_stock' ), 10, 1 );
}
}
/**
* This class ONLY handles inventory logic.
* It has no idea what an "action" or "filter" is.
*/
class Inventory_Manager {
public function reduce_stock( $order_id ) {
// ... all your clean, testable logic lives here.
// Get the order, get the products, reduce stock counts.
// No WordPress coupling. Pure logic.
}
}
// Then, in your main plugin file, you wire them up:
$inventory_logic = new Inventory_Manager();
$hook_subscriber = new Hook_Subscriber( $inventory_logic );
$hook_subscriber->register_hooks();
Why does this matter?
Done this way, you get a clean separation of concerns. Your Inventory_Manager is fully testable on its own, with no need to boot a whole WordPress environment. The Hook_Subscriber is deliberately dumb; it just registers events. Your core business logic is now decoupled from the WordPress API. The payoff shows up later, when a client asks for a new feature and you are not spending a week untangling spaghetti before you can even start.
This stuff gets complicated fast. If you are tired of debugging someone else’s mess and just want your site to work, get in touch with my team. We have probably seen your problem before.