Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions config/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
|
Expand All @@ -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'),
Expand Down
7 changes: 5 additions & 2 deletions src/Hook/Infrastructure/Providers/AsyncServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
12 changes: 9 additions & 3 deletions tests/Feature/Hook/Async/AsyncServiceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading