Table of Contents
Almost every WordPress developer has written this code at some point. Most WordPress developers learn classes and hooks around the same time. So the pattern feels obvious:
“I’ll put my hooks in the constructor. When the class loads, everything is wired up.”
It looks clean. It works. And for small plugins, it might even survive for a while.
But sooner or later, this pattern starts fighting you.
Constructors Are About Birth, Not Behaviour
Let’s step away from WordPress for a moment and think in plain, everyday terms.
Imagine a constructor as the moment a car rolls off the factory line.
At that exact moment, the car’s job is simple and very specific. It should be fully assembled, with all parts in place and nothing loose. It should be in a valid state, meaning the engine is running, the brakes are responsive, and the steering is aligned. And it should be ready to drive whenever a driver turns the key.
What it should not do is far more critical.
The car should not start driving on its own. It should not merge into traffic. It should not react to other vehicles by honking or changing lanes.
Those actions belong to the driver and the road, not the factory.
When you add hooks inside a constructor, you are asking the car to leave the factory and immediately start participating in traffic. The object is no longer merely being created; it is already reacting to the external environment. This makes behavior harder to predict, harder to test, and tightly coupled to its environment.
A constructor’s responsibility is to create and validate, nothing more. Interaction should happen after the object exists, just like driving happens after the car is built and handed over to the driver.
They take an object that is merely being created and immediately push it into the application’s flow. Creation and participation get mixed.
In object-oriented design, this separation matters deeply.
A constructor answers one question only:
“Is this object ready to exist?”
Hooks answer a completely different question:
“How does this object interact with the system?”
When those two questions are answered in the same place, you lose control over your application’s behaviour.
Let me show you why, using a real example.
The Tempting Pattern (That Bites Later)
You’ve probably written something like this:
class OrderHandler {
public function __construct() {
add_action(
'woocommerce_order_status_completed',
[ $this, 'process_completed_order' ]
);
}
public function process_completed_order( $order_id ) {
// Handle order completion logic
}
}
Usage is simple:
new OrderHandler();
One line. Done. Hooks registered.
So what’s the problem?
1. The Moment You Try to Unit Test
Now imagine you want to unit test process_completed_order().
You write a simple test:
$handler = new OrderHandler();
$handler->process_completed_order( 123 );
Boom.
The constructor fires.add_action() Runs.
WordPress must be loaded.
Your “unit test” now depends on WordPress core, global functions, and plugin bootstrap logic. What should have been a fast, isolated test turns into an integration test.
This is where many developers quietly give up on testing or start mocking global WordPress functions, which is fragile and painful.
The core issue is simple:
Creating the object already changed global state.
A constructor should never do that.
2. Side Effects You Didn’t Ask For
Let’s say you reuse this class in a WP-CLI command, migration script, or background job.
You just want the logic.
But the moment you instantiate the class, hooks are registered. Actions may fire. Filters may run. Behavior leaks into places you didn’t expect.
Even worse, if someone accidentally instantiates the class twice:
new OrderHandler();
new OrderHandler();
The same hook is registered twice.
Now your logic runs twice. No errors. Just incorrect behavior.
These are the hardest bugs to debug because nothing looks obviously wrong.
3. The Constructor Is Doing Too Much
At this point, the constructor is responsible for:
- Creating the object
- Integrating it with WordPress
- Changing application behavior
That violates the Single Responsibility Principle.
When developers can’t safely instantiate a class without triggering side effects, they stop trusting the code. That’s when a codebase becomes fragile.
The Better Pattern: Explicit Hook Registration
The fix is not complicated. It just requires discipline.
Separate object creation from hook registration.
Step 1: Keep the Constructor Clean
class OrderHandler {
private OrderService $service;
public function __construct( OrderService $service ) {
$this->service = $service;
}
public function process_completed_order( $order_id ) {
$this->service->handle_completion( $order_id );
}
}
Now the constructor only prepares the object. No side effects.
Step 2: Register Hooks Explicitly
class OrderHandler {
private OrderService $service;
public function __construct( OrderService $service ) {
$this->service = $service;
}
public function register() {
add_action(
'woocommerce_order_status_completed',
[ $this, 'process_completed_order' ]
);
}
public function process_completed_order( $order_id ) {
$this->service->handle_completion( $order_id );
}
}
Usage becomes intentional:
$order_handler = new OrderHandler( new OrderService() );
$order_handler->register();
Now you control when the class connects to WordPress.
What You Gain Immediately?
- You can instantiate the class safely in tests
- No hooks are registered accidentally
- Side effects are explicit, not hidden
This single change dramatically improves maintainability.
Let’s take another example of Hooks System in WordPress
On larger projects, even calling add_action() inside your classes can feel noisy.
A cleaner approach is to let classes declare their hooks and have a central system register them.
Step 1: Declare Hooks
interface Hookable {
public function get_actions(): array;
public function get_filters(): array;
}
class OrderHandler implements Hookable {
public function get_actions(): array {
return [
[ 'woocommerce_order_status_completed', 'process_completed_order' ],
];
}
public function get_filters(): array {
return [];
}
public function process_completed_order( $order_id ) {
// Handle order completion logic
}
}
Step 2: Centralize Registration
class HooksManager {
public function register( Hookable $hookable ) {
foreach ( $hookable->get_actions() as $action ) {
add_action( $action[0], [ $hookable, $action[1] ] );
}
foreach ( $hookable->get_filters() as $filter ) {
add_filter( $filter[0], [ $hookable, $filter[1] ] );
}
}
}
Usage:
$hooks = new HooksManager();
$hooks->register( new OrderHandler() );
Now hook wiring is centralized, predictable, and easy to audit.
A constructor should answer one question only:
“Is this object ready to be used?”
Not:
“What global behavior should change because I exist?”
By separating object creation from hook registration, you write WordPress code that scales, tests cleanly, and behaves predictably.
That’s not over-engineering. That’s professionalism.

Leave a Reply to Ronak VanpariyaCancel reply