Technote

Development workflow Advanced

Series Using hooks and filters properly Part 7 of 8

Adding your own hooks: extension points instead of edits

Code with no extension points gets forked or edited in place. Where you put do_action and apply_filters — and how many — decides how long that code survives.

So far this has been about consuming extension points other people left. Once you are the one writing the code, the question flips: where do you leave what, so that others can get their way without editing your files?

Code with no extension points has only two endings. It gets forked, or it gets edited in place with updates abandoned. Both mean your code is no longer maintained on that site.

Filters for values, actions for moments

The rule is the one from part one. Values you produce that somebody might reasonably want different go out through apply_filters(); moments in your work where somebody might reasonably want to join in are announced with do_action().

// A value — keep a default, and validate what comes back
$fields = apply_filters( 'wper_report_fields', $defaults, $report_id );

if ( ! is_array( $fields ) ) {
	$fields = $defaults;   // never trust the returned value blindly
}

// A moment — announce a place to join in
do_action( 'wper_report_before_render', $report_id, $fields );

Validating the return matters. If a callback attached to your filter forgets its return — the mistake from part one — your code is what falls over. Not failing fatally because of somebody else’s slip is your responsibility, not theirs.

Pass context, or the filter cannot decide anything

The arguments after the first are not decoration. They are the grounds on which the receiving code decides, and without enough of them a filter is close to useless.

The diagnostic plugin in this repository is the example. It exposes its scoring categories through a filter, and passes the context of that one diagnostic run alongside them. Without that context the receiver can only see the current screen settings, so opening an old report rescores it under a different configuration and past scores change quietly. One context argument is what makes the rule “an old report is scored with the configuration it ran under” possible at all.

The presence of extension points decides how long the code survives

More hooks is not the answer

A published hook becomes a promise you have to keep. Change its name, its arguments or when it fires and the code depending on it breaks. Placement matters far more than count.

One good test: put a hook exactly where somebody would otherwise have to edit your file. In practice that is usually three places — where a value destined for the screen is finalised, where a list or table is assembled, and immediately before you send something outwards.

Do not, by contrast, expose intermediate states of your implementation. Publishing those costs you the freedom to change how it works. If a name genuinely has to change, keep the old one alive for a while through apply_filters_deprecated().

The free plugins we build on these principles are readable in full from our free tools, and further writing on extension-point design sits in the development workflow archive.

Next part

The final part is diagnosis: how to look at which callbacks are attached to a hook and at what priority, and the order in which to chase a cause.

More on this topic

All technotes

Development workflow Advanced

The design QA checklist to run before release

Most design problems found after release are catchable before it, in order. Here is the pass, in three stages: rules, states and real devices.

Designers 6 min read

Development workflow Advanced

Never deploy without a way back

A rollback is not one button. It is two tracks — code and database — with an order to reverse them in, written down before you deploy.

Designers 9 min read

₩270,000 · Join the program