Tutorials UPDATED: 13 July 2023

Part 7 – WordPress and Object Oriented Programming: A WordPress Example – Implementation: Managing WordPress Hooks

Tassos Antoniou

6 min read

Up to this point, interacting with the Plugin API meant calling add_action() and add_filters() in the constructor of each class.

So far that approach was good enough, as it kept things simple and allowed us to focus on learning more about object-oriented programming with WordPress. However, it’s not ideal.

If an object registers all of its hooks when it’s created, things like unit testing become tricky.

NOTE: Unit tests should test each “unit” in isolation. Even if you’re not writing unit tests at the moment, writing testable code will save you a lot of time refactoring later, if you ever decide to write tests.

The Hooks Manager

Let’s take this one step further and introduce a new class to manage our hooks, we’ll call it Hooks_Manager. This class is going to be responsible for the registration of all of our hooks. So, we’ll create a new class with a register() method.

class Hooks_Manager {

    /**
     * Register the hooks of the given object.
     *
     * @param object $object
     */
    public function register( $object ) {
        // Register the hooks the specified object needs
    }

}

We need an interface for each class that needs to register hooks to implement.

interface Hooks {

    /**
     * Return the actions to register.
     *
     * @return array
     */
    public function get_actions();

}

You can think of an interface as a contract, where a class that implements that interface is “contractually bound” to implement all methods defined in that interface.

For example, a Login_Error class that hooks into the login_head action, must implement the get_actions() method of our Hooks interface.

class Login_Error implements Hooks {

    public function get_actions() {
        return array(
            'login_head' => array( 'add_errors', 10, 1 ),
        );
    }

}

The register() method of Hooks_Manager accepts an object, calls its get_actions() method and registers all of its actions.

public function register( $object ) {
    $actions = $object->get_actions();

    foreach ( $actions as $action_name => $action_details ) {
        $method        = $action_details[0];
        $priority      = $action_details[1];
        $accepted_args = $action_details[2];

        add_action(
            $action_name,
            array( $object, $method ),
            $priority,
            $accepted_args
        );
    }
}

Let’s add a get_filters() method to our interface, so we can register both actions and filters.

interface Hooks {

    /**
     * Return the actions to register.
     *
     * @return array
     */
    public function get_actions();

    /**
     * Return the filters to register.
     *
     * @return array
     */
    public function get_filters();

}

Back to our Login_Error class, we need to implement this new get_filters() method.

class Login_Error implements Hooks {

    public function get_actions() {
        return array(
            'login_head' => array( 'add_errors', 10, 1 ),
        );
    }
    public function get_filters() {
        return array(
            'authenticate'     => array( 'track_credentials', 10, 3 ),
            'shake_error_code' => array( 'add_error_code', 10, 1 ),
            'login_errors'     => array( 'format_error_message', 10, 1 ),
        );
    }

}

We’ll rename the register() method of our Hooks_Manager to register_actions(). We’ll also add a register_filters() method. These two methods will be responsible for registering actions and filters respectively.

class Hooks_Manager {
    
    /**
     * Register the actions of the given object.
     *
     * @param object $object
     */
    private function register_actions( $object ) {
        $actions = $object->get_actions();

        foreach ( $actions as $action_name => $action_details ) {
            $method        = $action_details[0];
            $priority      = $action_details[1];
            $accepted_args = $action_details[2];

            add_action(
                $action_name,
                array( $object, $method ),
                $priority,
                $accepted_args
            );
        }
    }
    
    /**
     * Register the filters of the given object.
     *
     * @param object $object
     */
    private function register_filters( $object ) {
        $filters = $object->get_filters();

        foreach ( $filters as $filter_name => $filter_details ) {
            $method        = $filter_details[0];
            $priority      = $filter_details[1];
            $accepted_args = $filter_details[2];

            add_filter(
                $filter_name,
                array( $object, $method ),
                $priority,
                $accepted_args
            );
        }
    }
    
}

Now we can add a register() method again, which is simply going to call both register_actions() and register_filters().

class Hooks_Manager {

    /**
     * Register an object.
     *
     * @param object $object
     */
    public function register( $object ) {
        $this->register_actions( $object );
        $this->register_filters( $object );
    }
    
    // ...

What if a class doesn’t need to register both actions and filters? The Hooks interface contains two methods: get_actions() and get_filters(). All classes that implement that interface will be forced to implement both methods.

class Cookie_Login implements Hooks {

    public function get_actions() {
        return array(
            'auth_cookie_bad_username' => array( 'handle_bad_username', 10, 1 ),
            'auth_cookie_bad_hash'     => array( 'handle_bad_hash', 10, 1 ),
            'auth_cookie_valid'        => array( 'handle_valid', 10, 2 ),
        );
    }

    public function get_filters() {
        return array();
    }

}

For example, the Cookie_Login class has to register only actions, but it’s now forced to implement the get_filters() method just to return an empty array.

The Interface Segregation Principle (ISP), the “I” in S.O.L.I.D, states:

“No client should be forced to depend on methods it does not use.”

Meaning that what we’re doing now is exactly what we shouldn’t be doing.

Interface Segregation

We can fix this by splitting our interface into smaller, more specific ones so our classes will only have to know about the methods that are of interest to them.

interface Actions {


    /**
     * Return the actions to register.
     *
     * @return array
     */
    public function get_actions();

}
interface Filters {


    /**
     * Return the filters to register.
     *
     * @return array
     */
    public function get_filters();

}

We don’t need both get_actions() and get_filters() anymore, we can implement only the Actions interface and get rid of get_filters()

class Cookie_Login implements Actions {

    public function get_actions() {
        return array(
            'auth_cookie_bad_username' => array( 'handle_bad_username', 10, 1 ),
            'auth_cookie_bad_hash'     => array( 'handle_bad_hash', 10, 1 ),
            'auth_cookie_valid'        => array( 'handle_valid', 10, 2 ),
        );
    }

}

On the other hand, Login_Error, which needs actions and filters, just has to implement both interfaces. Classes may implement more than one interface by separating them with a comma.

class Login_Error implements Actions, Filters {

    public function get_actions() {
        return array(
            'login_head' => array( 'add_errors', 10, 1 ),
        );
    }

    public function get_filters() {
        return array(
            'authenticate'     => array( 'track_credentials', 10, 3 ),
            'shake_error_code' => array( 'add_error_code', 10, 1 ),
            'login_errors'     => array( 'format_error_message', 10, 1 ),
        );
    }

}

Now that we’ve segregated our interface, we just have to update the register() method of Hooks_Manager to reflect our changes.

class Hooks_Manager {

    /**
     * Register an object.
     *
     * @param object $object
     */
    public function register( $object ) {
        if ( $object instanceof Actions ) {
            $this->register_actions( $object );
        }
        
        if ( $object instanceof Filters ) {
            $this->register_filters( $object );
        }
    }
    
    // ...

That way, we conditionally call only register_actions(), only register_filters(), or both, based on the interface(s) the specified object implements.

Try our Award-Winning WordPress Hosting today!

To actually use the hooks manager:

$hooks_manager = new Hooks_Manager();
$hooks_manager->register( $login_error );
$hooks_manager->register( $cookie_login );

That’s it! We can now use that object to manage hooks across the entire codebase.

Conclusion

Of course, there are several ways to manage your hooks in an object-oriented way, we just showed you one of them. You should experiment and find one that fits your needs.

Stay with us for the last part of our Objected Oriented Programming Series, where we’ll see how to handle options in an object-oriented way. We will talk about encapsulation, abstraction and how to decouple your classes to create a flexible plugin that’s easy to extend!

See Also

Start Your 14 Day Free Trial

Try our award winning WordPress Hosting!

OUR READERS ALSO VIEWED:

See how Pressidium can help you scale
your business with ease.