diff --git a/CHANGELOG.md b/CHANGELOG.md index ca1707d..75c26e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ All notable changes to `pollora/hook` are documented in this file. -## [Unreleased] +## [1.5.0] - 2026-10-09 + +### Added + +- `AbstractHook::all()` lists every registration made through a hook instance, keyed by hook name, for debugging tools that mark Pollora's callbacks +- The `pollora/async/dispatched` action announces each queued handler with its payload and delay ## [1.4.1] - 2026-10-07 diff --git a/README.md b/README.md index 31ec9b1..c569c03 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ Choose the default with the `POLLORA_ASYNC_DRIVER` constant in `wp-config.php`, **Closures.** With `laravel/serializable-closure` installed, a closure can be queued. It is serialized and signed with a key derived from the WordPress salts, and the signature is checked before anything is unserialized. Declare it `static` and let it use IDs rather than objects. -**Failures.** At execution, the original site and locale are restored, and a handler that fires its own hook does not queue itself again. A handler that throws is retried while it has attempts left; the last failure is reported and announced through the `pollora/async/failed` action. When an action cannot be queued, `WP_DEBUG` throws; otherwise the incident goes to the PHP error log (or `Async::reportUsing()`) and the handler runs in place, so the work always happens. +**Failures.** At execution, the original site and locale are restored, and a handler that fires its own hook does not queue itself again. A handler that throws is retried while it has attempts left; the last failure is reported and announced through the `pollora/async/failed` action. Each handler queued is announced through `pollora/async/dispatched` (the payload and the delay), for debugging tools. When an action cannot be queued, `WP_DEBUG` throws; otherwise the incident goes to the PHP error log (or `Async::reportUsing()`) and the handler runs in place, so the work always happens. **Testing.** `Async::fake()` records queued handlers instead of queuing them: diff --git a/src/Async/AsyncDispatcher.php b/src/Async/AsyncDispatcher.php index df3be89..c50d9c5 100644 --- a/src/Async/AsyncDispatcher.php +++ b/src/Async/AsyncDispatcher.php @@ -72,6 +72,11 @@ public function dispatch(QueuedHandler $handler, array $arguments): void ); $driver->dispatch($payload, $handler->options->delayInSeconds()); + + // Debugging tools list what a request queued; nothing else needs it + if (function_exists('do_action')) { + do_action('pollora/async/dispatched', $payload, $handler->options->delayInSeconds()); + } } catch (\Throwable $throwable) { if ($uniqueKey !== null) { UniqueLock::release($uniqueKey); diff --git a/src/Domain/Service/AbstractHook.php b/src/Domain/Service/AbstractHook.php index abd21e7..00fd3f1 100644 --- a/src/Domain/Service/AbstractHook.php +++ b/src/Domain/Service/AbstractHook.php @@ -213,6 +213,20 @@ public function callbacks(string $hook): ?array return $this->hooks[$hook]; } + /** + * Every registration made through this instance, by hook name. + * + * WordPress's own `$wp_filter` holds the same callbacks, but no longer says + * who registered them: a profiler reads this to mark the ones Pollora put + * there, and the class and method they resolved to. + * + * @return array> + */ + public function all(): array + { + return $this->hooks; + } + /** * Resolve the callback for the hook. * diff --git a/tests/Unit/Async/FrameworkHooksTest.php b/tests/Unit/Async/FrameworkHooksTest.php index a9143e6..78573cf 100644 --- a/tests/Unit/Async/FrameworkHooksTest.php +++ b/tests/Unit/Async/FrameworkHooksTest.php @@ -192,3 +192,26 @@ function failedAnnouncementsCount(): int expect(RecordingHandler::$calls)->toBe([]); }); }); + +describe('Announcing a dispatch', function (): void { + it('announces each queued handler with its payload and delay', function (): void { + $this->action->add('save_post', [RecordingHandler::class, 'handle'])->async()->delay(30); + + wp_stub_fire('save_post', 7); + + $announced = array_values(array_filter($GLOBALS['wp_actions_done'], fn (array $call): bool => $call['hook'] === 'pollora/async/dispatched')); + + expect($announced)->toHaveCount(1) + ->and($announced[0]['args'][0])->toBeInstanceOf(AsyncPayload::class) + ->and($announced[0]['args'][0]->hook)->toBe('save_post') + ->and($announced[0]['args'][1])->toBe(30); + }); + + it('announces nothing when the condition keeps the handler from being queued', function (): void { + $this->action->add('save_post', [RecordingHandler::class, 'handle'])->async()->when(fn (): bool => false); + + wp_stub_fire('save_post', 7); + + expect(array_filter($GLOBALS['wp_actions_done'], fn (array $call): bool => $call['hook'] === 'pollora/async/dispatched'))->toBe([]); + }); +}); diff --git a/tests/Unit/Domain/Service/AbstractHookTest.php b/tests/Unit/Domain/Service/AbstractHookTest.php index e8b2cb9..00fc70e 100644 --- a/tests/Unit/Domain/Service/AbstractHookTest.php +++ b/tests/Unit/Domain/Service/AbstractHookTest.php @@ -255,3 +255,28 @@ protected function removeHookEvent(string $hook, callable|string|array $callback expect($this->hook->callbacks('save_post'))->toBeNull(); }); }); + +describe('Listing every registration', function (): void { + it('lists the registrations of every hook, keyed by hook name', function (): void { + $this->hook->add('init', 'boot_something', 5); + $this->hook->add(['wp_head', 'wp_footer'], 'print_something'); + + $all = $this->hook->all(); + + expect(array_keys($all))->toBe(['init', 'wp_head', 'wp_footer']) + ->and($all['init'][0]['callback'])->toBe('boot_something') + ->and($all['init'][0]['priority'])->toBe(5) + ->and($all['wp_footer'][0]['hook'])->toBe('wp_footer'); + }); + + it('lists nothing before any registration', function (): void { + expect($this->hook->all())->toBe([]); + }); + + it('forgets a registration once it is removed', function (): void { + $this->hook->add('init', 'boot_something'); + $this->hook->remove('init', 'boot_something'); + + expect($this->hook->all()['init'] ?? [])->toBe([]); + }); +});