feat: document the nested-add_action-at-lower-priority hook bug (real incident, 3 plugins affected)
This commit is contained in:
@@ -142,6 +142,42 @@ time it's `require`'d a second time (as a stray plugin include), ABSPATH
|
||||
twice from two different code paths." Don't assume that guard is doing
|
||||
more than it actually does.
|
||||
|
||||
## Never `add_action('some_hook', ..., $lower_priority)` from inside a callback already running on `some_hook`
|
||||
|
||||
A callback added to a LOWER (earlier) priority than the one currently
|
||||
executing, from inside another callback on the SAME hook, will silently
|
||||
never run in the current pass. `WP_Hook::apply_filters()` snapshots the
|
||||
sorted priority keys once at the start of each `do_action()`/
|
||||
`apply_filters()` call; adding a new priority mid-iteration only affects
|
||||
a *future* call to that hook, and most hooks (`plugins_loaded`, `init`,
|
||||
etc.) only fire once per request. There is no error, no warning — the
|
||||
nested callback's own registration line executes fine, it's the callback
|
||||
*inside* it that never runs.
|
||||
|
||||
**Confirmed real incident, copied into 3 separate plugins before being
|
||||
caught**: every iWP-branded plugin's bootstrap instantiated its shared
|
||||
license/update-checker class like this:
|
||||
```php
|
||||
private function __construct() {
|
||||
add_action('plugins_loaded', ['IWP_Cache', 'instance']); // default priority 10
|
||||
}
|
||||
// ...inside instance()'s constructor:
|
||||
add_action('plugins_loaded', function () {
|
||||
new IWP_Updater([...]);
|
||||
}, 5); // priority 5 -- LOWER than the 10 already executing -- never runs
|
||||
```
|
||||
Verified directly on a live site (`wp eval 'global $wp_filter; var_dump(isset($wp_filter["pre_set_site_transient_update_plugins"]));'` returned `false`) that the updater's own filter registration — and therefore all license validation and update checking — was silently dead on every plugin using this pattern, since the plugin was first built.
|
||||
|
||||
**The fix**: don't nest a lower-priority `add_action` inside a callback
|
||||
already running on that hook at all. If the code you're deferring doesn't
|
||||
actually need to wait for anything else on that same hook (check: does
|
||||
its own constructor register hooks on *other*, later-firing actions? If
|
||||
so, timing within the current hook doesn't matter) — just call it
|
||||
directly, immediately, inline. Only use a nested `add_action` on the
|
||||
*same* hook if the target priority is equal-or-later than the one
|
||||
currently executing (still fragile — prefer restructuring to avoid the
|
||||
nesting entirely).
|
||||
|
||||
## Don't build what WordPress core already gives you
|
||||
|
||||
Before writing custom code for: cron scheduling (`wp_schedule_event`),
|
||||
|
||||
Reference in New Issue
Block a user