DocHooks - Sugar syntax for hooking system in WordPress

Learn how to use the WordPress hooking system in an object-oriented approach with simplicity comparable to the one in a procedural using DocHooks.

tl;drGitHub

Hooks are a fundamental aspect of the WordPress ecosystem, providing a powerful and flexible way to modify and customize functionality. Their usage is simple and well documented, but today I'll show you something more. I'll try to make them look better.


What the hooks really are?

Hooks act as connection points within the WordPress codebase, allowing to run custom code at specific execution points (actions) or modify the data during the execution time (filters). This is called hooking. They can be understood as the following requests:

  • Action - Run my function when when template_redirect action is run.
  • Filter - Modify the results from the_content filter using my function.

How they can be used?

Hooks can be used in various ways depending on personal preferences or app structure.

Procedural

Consists of creating a callback which should be run when specific action or filter is run and assigning it (hooking) to the hook using add_action or add_filter function.

function addLinks() {
  echo '<ul>...</ul>';
}

add_action('get_sidebar', 'addLinks');

Sometimes it might be useful to use an anonymous function here, but there won't be a possibility to unhook the callback using remove_action or remove_filter which might be crucial when creating plugins widely used by others.

add_action('get_sidebar', function() {
  echo '<ul>...</ul>';
});

OOP

I need to create a public callback and hooking it in the class constructor, but once the class methods are not available in the global scope, using just a callback name for hooking doesn't work. I have to point out the object from which a specific function should be used and it can be done using array structure [$this, 'addLinks'].

namespace FM\Core;

class Widgets
{
    public function __construct()
    {
        add_action('get_sidebar', [$this, 'addLinks']);
    }

    public function addLinks(): void
    {
        echo '<ul>...</ul>';
    }
}

Run addLinks callback from an $this object when the get_sidebar action is run.` 


What are the problems?

You may ask a goog question: "Oh gosh, what problems it might have? It's simple."

In general, handling hooks this way doesn't bring problems which should stop you from using them, but in some aspects of my workflow, they can be iritating.

  1. Once the codebase grows, navigating between callback and hooks in the file becomes frustrating.
  2. Once the hooks are added in the constructor, they are added every time I create a new instance of the object, even when I don't need this. 

Those issues aren't something regular, but what If I could reduce them? What if I could keep hooks and callbacks together as in a procedural way and don't perform unnecessary operations with a small effort?


Let's meet DocHooks!

Let's meet something called DocHooks which are a custom way of handling hooks in classes. They allow hooking callbacks to actions or filters using the method's doc block annotations like @action or @filter with the following syntax.

They are just like syntax sugar for the hooking system in WordPress. There's nothing fancy under the hood what gives the same possibilities and results as standard ways, but additionally, it allows to combine the simplicity of a procedural approach with the advantages of object-oriented. So little and yet so much! That's something I love a lot 🔥

class Widgets
{
    /**
     * @action get_sidebar
     */
    public function addLinks(): void
    {
        echo '<ul>...</ul>';
    }
}

How to implement it?

At first, I need to create a handler that will extract the hook's configuration from the class doc blocks using ReflectionObject which is a native PHP utility useful for reporting information about objects.

I create a new analyzer and pass an object to verify as a constructor parameter (15). Iterate through all the methods available there using getMethods function (17), verify doc blocks for annotations (18) using getDocComment function and regexp (5) and build a collection of data that should be used for hooking (20-26).

namespace FM\Core;

class Hooks
{
    private const PATTERN = '#\* @(?P<type>filter|action|shortcode)\s+(?P<name>[a-z0-9\-\.\/_]+)(\s+(?P<priority>\d+))?#';

    private static function extract(object $object): array
    {
        $handlers = [];

        if (empty($object)) {
            return $handlers;
        }

        $reflector = new \ReflectionObject($object);

        foreach ($reflector->getMethods() as $method) {
            if (preg_match_all(self::PATTERN, $method->getDocComment(), $matches, PREG_SET_ORDER)) {
                foreach ($matches as $match) {
                    $handlers[] = [
                        'hook' => sprintf('add_%s', $match['type']),
                        'name' => $match['name'],
                        'callback' => [$object, $method->getName()],
                        'priority' => $match['priority'] ?? 10,
                        'args' => $method->getNumberOfParameters(),
                    ];
                }
            }
        }

        return $handlers;
    }
}

The extract function will return an array consisting of data that can be used for calling native add_action or add_filter WordPress methods which looks like here:

Array
(
    [0] => Array
        (
            [hook] => add_action
            [name] => get_sidebar
            [callback] => Array
                (
                    [0] => FM\Core\Widgets Object()
                    [1] => addLinks
                )
            [priority] => 10
            [args] => 0
        )

)

Now, I need to create a function that will analyze the objects that I pass there for initializing hooks available inside.

It won't be used often, because it's useful mostly just to initialize module facades so I decide to use the public static init method that accepts an object to verification as a parameter, extract hooks, fires them and return the same instance for further usage.

namespace FM\Core;

class Hooks
{
    private const PATTERN = '#\* @(?P<type>filter|action|shortcode)\s+(?P<name>[a-z0-9\-\.\/_]+)(\s+(?P<priority>\d+))?#';

    public static function init(object $object): object
    {
        foreach (self::extract($object) as $config) {
            call_user_func($config['hook'], $config['name'], $config['callback'], $config['priority'], $config['args']);
        }

        return $object;
    }

    private static function extract(object $object): array
    {
        $handlers = [];

        if (empty($object)) {
            return $handlers;
        }

        $reflector = new \ReflectionObject($object);

        foreach ($reflector->getMethods() as $method) {
            if (preg_match_all(self::PATTERN, $method->getDocComment(), $matches, PREG_SET_ORDER)) {
                foreach ($matches as $match) {
                    $handlers[] = [
                        'hook' => sprintf('add_%s', $match['type']),
                        'name' => $match['name'],
                        'callback' => [$object, $method->getName()],
                        'priority' => $match['priority'] ?? 10,
                        'args' => $method->getNumberOfParameters(),
                    ];
                }
            }
        }

        return $handlers;
    }
}

TIP: You can see another great example of encapsulation here. The extract method is private because the client should not have information about internal details. The only thing that matters is that module will initialize hooks as agreed (init).

Once the handler is created, I need to use it somehow. It won't work automatically, because I need to pass an object to the verification every time I want to add a hooks. So I create something like a module initializer in my application facade which uses init function created in the previous step.

public static function init(object $module): object
{
    return \FM\Core\Hooks::init($module);
}

I could just use \FM\Core\Hooks::init but \FM\App looks better IMO. So now all the module facades must be created this way.

namespace FM\Core;

use FM\Core\Widgets;

class Core
{
    public function __construct()
    {
        \FM\App::init(new Widgets());
    }
}

Any concerns?

There are a few things that can be treated as problematic by some of you:

  • opcache.save_comments config must be enabled, otherwise, all doc comments will be discarded from the opcode cache which results in no hooks to add. So for example, you should be careful when using this while building a plugin for widely use, because some environments (like in Kinsta) might have this disabled. The change is simple and not really problematic, but you shopuld know about this.
  • The object must be wrapped in the \FM\App::init function. I'm not sure if it's really a problem because it solves other problems with small effort. For me not, but I leave it to your judgment.

Summary

Is it a must-have in WordPress? Not really. Is it must have for me? Totally! 😎

It is a sugar syntax which doesn't have an impact on the data flow, the performance, or the way how the application is executed. It is only a faster/more readable way of writing the code which is replaced then with "clean" WordPress methods, so not using it won't harm your code. But if the code readability is important to you, or if it just looks better for you, I think that it is totally worth trying.

I started using it many years ago and I've never stopped. I must be careful now because I liked it so much that the default way of handling hooks looks ugly to me now 😅 But I'm probably talking about tastes here. Let me know what you think about this!


Surprise

As a reward for staying till the end, I leave you a VS Code snippet for quickly adding filters and actions in WordPress. Enjoy it! 🎉

"action": {
    "prefix": "action",
    "body": [
        "/**",
        " * @action ${1:template_redirect}",
        " */",
        "public function ${2:action}(): void",
        "{",
        "\t${3}",
        "}"
    ]
},
"filter": {
    "prefix": "filter",
    "body": [
        "/**",
        " * @filter ${1:the_title}",
        " */",
        "public function ${2:filter}(${4:string} \\$${3:title}): ${4:string}",
        "{",
        "\treturn \\$${3:title};",
        "}"
    ]
},

Feedback

How satisfied you are after reading this article?