Rethinking do_action(): Events as objects, hooks as class names

firasd1 pts0 comments

Rethinking do_action(): Events as objects, hooks as class names – WordPress Developer Blog

Get WordPress

WordPress.org

WordPress Developer Blog

Rethinking do_action(): Events as objects, hooks as class names

For a recent project, I explored the concept of building an event dispatcher for WordPress following the PSR-14 standard. You may be asking why I would even attempt that when WordPress already has its own system that dispatches events, called actions.

It was part fun exploration, part for my own edification. And I learned a lot along the way.

If you build on top of WordPress, you’ve used do_action(). You might have called it a hundred times. Maybe even thousands. And you’ve probably written custom hooks in your own plugins and themes so other code can hang behavior off yours. It’s one of the oldest extensibility tricks in WordPress, and it still works as beautifully as it did when it was introduced in WordPress 1.2.0.

But somewhere along the way, we all quietly accepted a few rough edges as just "how hooks work."

A tiny change to how you call do_action() — passing a plain object instead of a fistful of arguments — smooths every one of them out. It needs no custom library. No framework. No new dependency. Just a better default for a function you already use.

As I explored my own event dispatcher, it turned out that nearly all the value lived in one small habit. Let me walk through what I mean.

Table of Contents

The hooks to tolerate

Suppose you’re building a plugin that registers new members. Let’s look at examples of a couple of custom hooks that might appear in that plugin.

First, you might that announce that a registration happened:

do_action( 'myplugin_member_registered', $userId, $plan );

And another plugin might listen:

add_action( 'myplugin_member_registered', static function ( $userId, $plan ) {<br>// ...<br>}, 10, 2 );

Second, your plugin needs an answer back, not just a notice: should this member get a welcome email? That’s a decision, so you apply filters instead:

$sendWelcomeEmail = apply_filters( 'myplugin_send_welcome_mail', true, $userId, $plan );

if ( $sendWelcomeEmail ) {<br>// ...queue the welcome email.

And another plugin listens, changing the value based on the arguments provided:

add_filter( 'myplugin_send_welcome_email', static function ( $send, $userId, $plan ) {<br>if ( 'free' === $plan ) {<br>$send = false;

return $send;<br>}, 10, 3 );

Both of these techniques are fine, and they’re on par with how WordPress itself fires its own hooks. They’re also carrying the same small annoyances that’s easy to stop noticing:

The arguments are positional: Was the order $userId, $plan or $plan, $userId? How many were there? You have to go read the call to be sure, and remember to pass 10, 2 (or 10, 3 for the filter) so that every argument gets passed along.

The name lives in the global scope: myplugin_member_registered and myplugin_send_welcome_mail each share one flat namespace with every other plugin on the site. You prefix them and hope.

Nothing is typed: $plan could be anything, such as a string, an ID, an object. Most code editors can’t help you, because as far as they know, it’s just a variable named $plan.

The filter adds a fourth papercut all its own: you have to remember to return the value. If you forget return $send;, it breaks any filter along the chain, and it’s hard to trace the problem back to the filter you added.

None of these are hook problems. They’re payload problems, and they show up whether the hook is an action or a filter. A hook can carry a single object just as easily as it can carry multiple loose arguments. And an object fixes all of it at once.

The one-line idea

Instead of passing loose arguments, pass one object that describes what happened. And instead of inventing a string name, use the object’s own class name as the hook:

do_action( $event::class, $event );

That’s the whole idea. The payload of the hook is now a single, typed object. The name is the class of that object, so it’s namespaced by construction. No other plugin’s FullyQualified\ClassName is going to collide with yours without a fatal error.

Notice this is still do_action(), not apply_filters(). That’s not an oversight. Part of the merit of this technique is that a mutable property on the event can stand in for whatever a filter’s return value would otherwise carry, minus the return-discipline problem the filter version has. We’ll watch that play out with the welcome-email decision in a moment.

Naming the hook: the class, or a custom string

There are two good ways to name the hook, and the choice comes down to a single question: do you want a name that’s guaranteed never to change?

Every hook in WordPress is global. Anyone can call add_action() on any name. So this isn’t about who’s allowed to listen; it’s about whether the name stays put when you refactor.

Use the class name when you’re fine with the hook name tracking the class:

do_action( MemberRegistered::class, $event...

wordpress class name do_action plan hook

Related Articles