DocHooks: Sugar Syntax Hooks 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.
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 are the WordPress hooks?
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 to use WordPress hooks?
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>';
});
Object Oriented
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 with WordPress hooks?
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.
- Once the codebase grows, navigating between callback and hooks in the file becomes frustrating.
- 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?
What are 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 isprivate
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());
}
}
Important concerns when using DocHooks
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.
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!
Looking for a developer who
truly cares about your business?
My team and I provide expert consultations, top-notch coding, and comprehensive audits to elevate your success.
Feedback
How satisfied you are after reading this article?