From fbc932688b0b095bb451065b0b5fa31408cd095c Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Wed, 7 Oct 2026 18:37:18 +0200 Subject: [PATCH] feat: leave the Laravel queue out of auto until a connection is set for async actions --- CHANGELOG.md | 2 +- config/hooks.php | 9 ++++++--- .../Providers/AsyncServiceProvider.php | 7 +++++-- .../Feature/Hook/Async/AsyncServiceProviderTest.php | 12 +++++++++--- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19e2a55d..45522809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/Pollora/framework/compare/v13.35.1...develop) ### Added -- Asynchronous actions in the framework (pollora/hook 1.4): `Action::add(...)->async()` can queue handlers as Laravel jobs. The `queue` driver dispatches a `RunAsyncAction` job on `hooks.async.queue.connection` (the default connection when null) and on the action's `onQueue()` or `hooks.async.queue.queue`. `auto`, the default, now tries the Laravel queue first, then Action Scheduler, then WP-Cron; a `sync` or `null` connection is always skipped. The job is tried once: retries are new jobs queued after the backoff, and the last failure lands in the failed jobs table. It also clears WordPress's in-memory cache before each handler, since a worker lives across jobs +- Asynchronous actions in the framework (pollora/hook 1.4): `Action::add(...)->async()` can queue handlers as Laravel jobs. The `queue` driver dispatches a `RunAsyncAction` job on `hooks.async.queue.connection` (the default connection when null) and on the action's `onQueue()` or `hooks.async.queue.queue`. `auto`, the default, tries the Laravel queue first once `HOOKS_ASYNC_CONNECTION` names the connection a worker runs (without a worker, jobs would never run, so the queue is opt-in), then Action Scheduler, then WP-Cron; a `sync` or `null` connection is always skipped. `via('queue')` and `HOOKS_ASYNC_DRIVER=queue` use the default connection. The job is tried once: retries are new jobs queued after the backoff, and the last failure lands in the failed jobs table. It also clears WordPress's in-memory cache before each handler, since a worker lives across jobs - `config/hooks.php`, published with `php artisan vendor:publish --tag=pollora-hooks`: default driver (`HOOKS_ASYNC_DRIVER`, `sync` in a developer's `.env` runs every handler at once), attempts, backoff, `as_user`, queue connection and name - `#[Async]`, next to `#[Action]`, makes a method asynchronous with the options of `->async()` as named parameters: `delay`, `via`, `onQueue`, `unique` (`true` or the lock duration), `tries`, `backoff`, `asUser`, `capture` and `when` (public methods of the class), `keepMissing`, `except`. On a class it applies to every `#[Action]` method, and a method's own `#[Async]` replaces it. A declaration that cannot be honoured (a hook in `except` the method does not declare, a missing or non-public `capture`/`when` method, invalid attempts, backoff or lock) is logged and the action runs synchronously; `#[Async]` on a `#[Filter]` or without `#[Action]` is logged - Async handlers in the framework: debug mode follows `app.debug`, incidents go to the Laravel log, closures are signed with the application key, Eloquent models travel by class and key and are reloaded at execution, and parameters typed with a service are resolved from the container diff --git a/config/hooks.php b/config/hooks.php index 0ebc8b3f..8b485337 100644 --- a/config/hooks.php +++ b/config/hooks.php @@ -12,8 +12,11 @@ | Action::add(...)->async() and #[Async] queue a handler instead of running | it inside the request that fires the hook. | - | default: auto picks the Laravel queue when its connection is not "sync", - | then Action Scheduler when a plugin bundling it is active, then WP-Cron. + | default: auto picks the Laravel queue once HOOKS_ASYNC_CONNECTION names + | the connection a queue worker runs (never a "sync" or "null" one), then + | Action Scheduler when a plugin bundling it is active, then WP-Cron. + | Without a worker, queued jobs would never run, so the queue is opt-in; + | via('queue') or HOOKS_ASYNC_DRIVER=queue uses the default connection. | Set HOOKS_ASYNC_DRIVER=sync in a developer's .env to run every handler | at once, without touching the code. | @@ -33,7 +36,7 @@ 'as_user' => false, 'queue' => [ - // null: the default queue connection + // The connection a worker runs. null: the default connection, and auto leaves the queue out 'connection' => env('HOOKS_ASYNC_CONNECTION'), // Overridden per action by onQueue() or #[Async(onQueue: ...)] 'queue' => env('HOOKS_ASYNC_QUEUE', 'default'), diff --git a/src/Hook/Infrastructure/Providers/AsyncServiceProvider.php b/src/Hook/Infrastructure/Providers/AsyncServiceProvider.php index 2d03fcb5..c8b9f7b9 100644 --- a/src/Hook/Infrastructure/Providers/AsyncServiceProvider.php +++ b/src/Hook/Infrastructure/Providers/AsyncServiceProvider.php @@ -18,7 +18,7 @@ * Connects the asynchronous actions of pollora/hook to the application. * * - config/hooks.php (publishable with the "pollora-hooks" tag): default driver, attempts, backoff, as_user, queue - * - the "queue" driver, first of the drivers "auto" tries + * - the "queue" driver, first of the drivers "auto" tries once hooks.async.queue.connection is set * - debug mode from app.debug, incidents to the Laravel log * - closures signed with the application key * - Eloquent models carried by reference @@ -45,7 +45,10 @@ private function configureAsync(Repository $config): void $container = $this->app; Async::extend('queue', static fn (): AsyncDriver => new QueueDriver($container->make(Dispatcher::class), $config)); - Async::setAutoDrivers(['queue', 'action-scheduler', 'wp-cron']); + // The Laravel queue needs a worker: auto only picks it once a connection is set for async actions + Async::setAutoDrivers($config->get('hooks.async.queue.connection') !== null + ? ['queue', 'action-scheduler', 'wp-cron'] + : ['action-scheduler', 'wp-cron']); $default = $config->get('hooks.async.default', 'auto'); Async::setDefaultDriver(is_string($default) && $default !== '' && $default !== 'auto' ? $default : null); diff --git a/tests/Feature/Hook/Async/AsyncServiceProviderTest.php b/tests/Feature/Hook/Async/AsyncServiceProviderTest.php index 5dae74fd..f6aa8012 100644 --- a/tests/Feature/Hook/Async/AsyncServiceProviderTest.php +++ b/tests/Feature/Hook/Async/AsyncServiceProviderTest.php @@ -47,14 +47,20 @@ function bootAsyncProvider(array $config = []): void ->and(array_values(ServiceProvider::pathsToPublish(AsyncServiceProvider::class, 'pollora-hooks')))->toBe([config_path('hooks.php')]); }); -it('makes the Laravel queue the first driver auto tries, skipped on a sync connection', function (): void { - bootAsyncProvider(); +it('leaves the Laravel queue out of auto until a connection is set for async actions', function (): void { + bootAsyncProvider(['queue.default' => 'database']); expect(Async::defaultDriver())->toBe('wp-cron'); +}); - config(['queue.default' => 'database']); +it('makes the Laravel queue the first driver auto tries once a connection is set, skipped when it is sync', function (): void { + bootAsyncProvider(['hooks.async.queue.connection' => 'database']); expect(Async::defaultDriver())->toBe('queue'); + + config(['hooks.async.queue.connection' => 'sync']); + + expect(Async::defaultDriver())->toBe('wp-cron'); }); it('sets the default driver from the configuration', function (): void {