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.
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.