From 690d135b49d0e68f93a1278eb2d2dd30a4ad13ab Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 18:47:08 +0200 Subject: [PATCH 01/20] feat: introduces a run method --- CHANGELOG.md | 6 ++ shepherd.php | 2 +- src/Regulator.php | 147 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e090d6..4c8fab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. This project adhere to the [Semantic Versioning](http://semver.org/) standard. +## [0.1.0] 2025-12-16 + +* Feature - Introduces a method `run` to the Regulator class which enables running a set of tasks synchronously. + +[0.0.9]: https://github.com/stellarwp/shepherd/releases/tag/0.0.9 + ## [0.0.9] 2025-11-17 * Tweak - Add a filter `shepherd_{prefix}_dispatch_handler` to allow for custom dispatch handlers. diff --git a/shepherd.php b/shepherd.php index 80eab0d..fb6a22b 100644 --- a/shepherd.php +++ b/shepherd.php @@ -9,7 +9,7 @@ * @wordpress-plugin * Plugin Name: Shepherd * Description: A library for offloading tasks to background processes. - * Version: 0.0.9 + * Version: 0.1.0 * Author: StellarWP * Author URI: https://stellarwp.com * License: GPL-2.0-or-later diff --git a/src/Regulator.php b/src/Regulator.php index a55ba61..0c79b09 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -24,6 +24,7 @@ use StellarWP\Shepherd\Exceptions\ShepherdTaskFailWithoutRetryException; use StellarWP\Shepherd\Traits\Loggable; use StellarWP\Shepherd\Tasks\Herding; +use ActionScheduler_QueueRunner; /** * Shepherd's regulator. @@ -325,6 +326,152 @@ protected function dispatch_callback( Task $task, int $delay ): void { } } + /** + * Run a set of tasks. + * + * @since 0.1.0 + * + * @param Task[] $tasks The tasks to run. + * @param array $callables The callables to run. + * 'before' => function ( Task $task ): void {}, + * 'after' => function ( Task $task ): void {}, + * 'on_error' => function ( ?Task $task, Exception $e ): void {}, + * 'always' => function ( Task $task ): void {}, + * + * @return void + */ + public function run( array $tasks, array $callables = [] ): void { + $prefix = Config::get_hook_prefix(); + + if ( ! did_action( "shepherd_{$prefix}_tables_registered" ) ) { + foreach ( $tasks as $task ) { + $task->process(); + + /** + * Fires an action when a task is run synchronously. + * + * @since 0.1.0 + * + * @param Task $task The task that was dispatched synchronously. + */ + do_action( "shepherd_{$prefix}_task_run_sync", $task ); + } + + return; + } + + if ( did_action( 'action_scheduler_init' ) || doing_action( 'action_scheduler_init' ) ) { + $this->run_callback( $tasks, $callables ); + return; + } + + add_action( + 'action_scheduler_init', + function () use ( $tasks, $callables ): void { + $this->run_callback( $tasks, $callables ); + }, + 10 + ); + } + + /** + * Runs a set of tasks. + * + * @since 0.1.0 + * + * @param Task[] $tasks The tasks to run. + * @param array $callables The callables to run. + * 'before' => function ( Task $task ): void {}, + * 'after' => function ( Task $task ): void {}, + * 'on_error' => function ( ?Task $task, Exception $e ): void {}, + * 'always' => function ( Task $task ): void {}, + * + * @return void + */ + private function run_callback( array $tasks, array $callables = [] ): void { + $scheduled_task_ids = array_map( fn( Task $task ) => $task->get_id(), $this->scheduled_tasks ); + + $callables = wp_parse_args( + $callables, + [ + 'before' => function ( Task $task ): void {}, + 'after' => function ( Task $task ): void {}, + 'on_error' => function ( Task $task, Exception $e ): void {}, + 'always' => function ( Task $task ): void {}, + ] + ); + + $context = defined( 'WP_CLI' ) && WP_CLI ? ' CLI' : ''; + $context = ! $context && defined( 'REST_REQUEST' ) && REST_REQUEST ? ' REST' : $context; + $prefix = Config::get_hook_prefix(); + + try { + $runner = ActionScheduler_QueueRunner::instance(); + + foreach ( $tasks as $task ) { + if ( ! in_array( $task->get_id(), $scheduled_task_ids, true ) ) { + $this->dispatch_callback( $task, 0 ); + } + + if ( is_callable( $callables['before'] ) ) { + $callables['before']( $task ); + } + + /** + * Fires when a task is about to be run. + * + * @since 0.1.0 + * + * @param Task $task The task that is about to be run. + */ + do_action( "shepherd_{$prefix}_task_before_run", $task ); + + $runner->process_action( $task->get_action_id(), "Shepherd{$context}" ); + + if ( is_callable( $callables['after'] ) ) { + $callables['after']( $task ); + } + + /** + * Fires when a task is finished running. + * + * @since 0.1.0 + * + * @param Task $task The task that is finished running. + */ + do_action( "shepherd_{$prefix}_task_after_run", $task ); + } + } catch ( Exception $e ) { + // The process_task method already catches and handles all the exceptions before throwing them again. + if ( is_callable( $callables['on_error'] ) ) { + $callables['on_error']( isset( $task ) ? $task : null, $e ); + } + + /** + * Fires when a set of tasks fails to be run. + * + * @since 0.1.0 + * + * @param ?Task $task The task that failed to run. + * @param Exception $e The exception that was thrown. + */ + do_action( "shepherd_{$prefix}_tasks_run_failed", isset( $task ) ? $task : null, $e ); + } + + if ( is_callable( $callables['always'] ) ) { + $callables['always']( $tasks ); + } + + /** + * Fires when a set of tasks is finished running. + * + * @since 0.1.0 + * + * @param Task[] $tasks The tasks that were run. + */ + do_action( "shepherd_{$prefix}_tasks_finished", $tasks ); + } + /** * Gets the last scheduled task ID. * From 8665d98046e41b51643fd77b7a7a4e1df3faae32 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 19:14:08 +0200 Subject: [PATCH 02/20] docs: update documentation to reflect the run method --- CLAUDE.md | 12 +++ docs/advanced-usage.md | 163 ++++++++++++++++++++++++++++++++++++++++ docs/api-reference.md | 42 +++++++++++ docs/getting-started.md | 3 + 4 files changed, 220 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 813de50..9fcdacf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,6 +7,7 @@ Shepherd is a lightweight background processing library for WordPress built on t ## Key Features - **Background Task Processing**: Offload time-consuming operations to background processes +- **Synchronous Task Execution**: Run tasks immediately with lifecycle callbacks via `run()` method (since 0.1.0) - **Automatic Retries**: Configurable retry mechanism with exponential backoff - **Task Debouncing**: Prevents tasks from running too frequently with customizable delays - **Unique Task Enforcement**: Prevents duplicate tasks from being scheduled @@ -105,6 +106,17 @@ shepherd()->dispatch(new My_Task($arg1, $arg2)); // Dispatch with delay (in seconds) shepherd()->dispatch(new My_Task($arg1, $arg2), 300); // 5 minutes +// Run tasks synchronously with lifecycle callbacks (since 0.1.0) +shepherd()->run( + [ new My_Task($arg1, $arg2), new Another_Task() ], + [ + 'before' => function( $task ) { /* called before each task */ }, + 'after' => function( $task ) { /* called after each task */ }, + 'on_error' => function( $task, $e ) { /* called on failure */ }, + 'always' => function( $tasks ) { /* called after all tasks */ }, + ] +); + // Retrieve task logs use StellarWP\Shepherd\Contracts\Logger; use StellarWP\Shepherd\Provider; diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index a960e01..1f68bc1 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -371,6 +371,169 @@ The task tables include indexes on: - **Indexed Queries**: All cleanup queries use indexed columns for optimal performance - **Minimal Overhead**: Action deletion hooks add minimal overhead to Action Scheduler operations +## Synchronous Task Execution (Since 0.1.0) + +The `run()` method allows you to execute tasks synchronously with full control over the execution lifecycle. This is useful for CLI commands, REST API endpoints, or any scenario where you need tasks to execute immediately. + +### Basic Usage + +```php +use function StellarWP\Shepherd\shepherd; + +// Run a single task immediately +shepherd()->run( [ new My_Task() ] ); + +// Run multiple tasks in sequence +$tasks = [ + new Process_Image_Task( $image_id ), + new Generate_Thumbnail_Task( $image_id ), + new Update_Metadata_Task( $image_id ), +]; + +shepherd()->run( $tasks ); +``` + +### Lifecycle Callbacks + +The `run()` method accepts an optional array of callbacks for fine-grained control: + +```php +shepherd()->run( $tasks, [ + // Called before each task runs + 'before' => function( Task $task ): void { + error_log( 'Starting task: ' . get_class( $task ) ); + }, + + // Called after each task completes successfully + 'after' => function( Task $task ): void { + error_log( 'Completed task: ' . get_class( $task ) ); + }, + + // Called if any task throws an exception + 'on_error' => function( ?Task $task, Exception $e ): void { + error_log( 'Task failed: ' . $e->getMessage() ); + // Optionally notify administrators or log to external service + }, + + // Called after all tasks complete (even if some failed) + 'always' => function( array $tasks ): void { + error_log( 'Finished processing ' . count( $tasks ) . ' tasks' ); + }, +] ); +``` + +### CLI Command Example + +```php +use WP_CLI; +use function StellarWP\Shepherd\shepherd; + +WP_CLI::add_command( 'myapp process-images', function( $args, $assoc_args ) { + $image_ids = get_unprocessed_image_ids(); + $tasks = array_map( + fn( $id ) => new Process_Image_Task( $id ), + $image_ids + ); + + $processed = 0; + $failed = 0; + + shepherd()->run( $tasks, [ + 'before' => function( Task $task ) { + WP_CLI::log( 'Processing image...' ); + }, + 'after' => function( Task $task ) use ( &$processed ) { + $processed++; + WP_CLI::success( 'Image processed!' ); + }, + 'on_error' => function( ?Task $task, Exception $e ) use ( &$failed ) { + $failed++; + WP_CLI::warning( 'Failed: ' . $e->getMessage() ); + }, + 'always' => function( array $tasks ) use ( &$processed, &$failed ) { + WP_CLI::line( "Processed: {$processed}, Failed: {$failed}" ); + }, + ] ); +} ); +``` + +### REST API Example + +```php +use function StellarWP\Shepherd\shepherd; + +register_rest_route( 'myapp/v1', '/process', [ + 'methods' => 'POST', + 'callback' => function( WP_REST_Request $request ) { + $items = $request->get_param( 'items' ); + $tasks = array_map( + fn( $item ) => new Process_Item_Task( $item ), + $items + ); + + $results = [ + 'processed' => [], + 'failed' => [], + ]; + + shepherd()->run( $tasks, [ + 'after' => function( Task $task ) use ( &$results ) { + $results['processed'][] = $task->get_args()[0]; + }, + 'on_error' => function( ?Task $task, Exception $e ) use ( &$results ) { + $results['failed'][] = [ + 'item' => $task ? $task->get_args()[0] : null, + 'error' => $e->getMessage(), + ]; + }, + ] ); + + return new WP_REST_Response( $results, 200 ); + }, + 'permission_callback' => fn() => current_user_can( 'manage_options' ), +] ); +``` + +### Behavior Notes + +- **Already scheduled tasks**: If a task was previously dispatched via `dispatch()`, `run()` will execute it without re-dispatching +- **Fallback mode**: When Shepherd's database tables are not registered, tasks execute immediately via `process()` without Action Scheduler +- **Error handling**: When a task fails, the `on_error` callback is invoked and the remaining tasks in the batch are skipped +- **Context detection**: Shepherd automatically detects CLI and REST contexts for proper logging + +### WordPress Hooks + +Monitor synchronous task execution using WordPress actions: + +```php +$prefix = Config::get_hook_prefix(); + +// Fired before each task runs +add_action( "shepherd_{$prefix}_task_before_run", function( Task $task ) { + // Prepare for task execution +}, 10, 1 ); + +// Fired after each task completes +add_action( "shepherd_{$prefix}_task_after_run", function( Task $task ) { + // Post-task cleanup or notifications +}, 10, 1 ); + +// Fired when any task fails +add_action( "shepherd_{$prefix}_tasks_run_failed", function( ?Task $task, Exception $e ) { + // Handle batch failure +}, 10, 2 ); + +// Fired after all tasks complete +add_action( "shepherd_{$prefix}_tasks_finished", function( array $tasks ) { + // Batch completion handling +}, 10, 1 ); + +// Fired when tables aren't registered (fallback mode) +add_action( "shepherd_{$prefix}_task_run_sync", function( Task $task ) { + // Track fallback executions +}, 10, 1 ); +``` + ## Custom Dispatch Handlers **Since 0.0.9**, you can completely override Shepherd's default dispatch behavior by providing a custom handler via a filter. This is useful for advanced scenarios where you need full control over how tasks are dispatched. diff --git a/docs/api-reference.md b/docs/api-reference.md index 333b8bf..e85488c 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -37,6 +37,33 @@ Schedules a task for execution. - `shepherd_{prefix}_task_scheduling_failed` - `shepherd_{prefix}_task_already_exists` +##### `run( array $tasks, array $callables = [] ): void` + +Runs a set of tasks synchronously with optional lifecycle callbacks. + +- **Parameters:** + - `$tasks` - Array of Task instances to run + - `$callables` - Optional array of lifecycle callbacks: + - `'before'` - `function( Task $task ): void` - Called before each task runs + - `'after'` - `function( Task $task ): void` - Called after each task completes + - `'on_error'` - `function( ?Task $task, Exception $e ): void` - Called if a task fails + - `'always'` - `function( array $tasks ): void` - Called after all tasks complete (even on error) +- **Since:** 0.1.0 +- **Behavior:** + - When tables are registered: Dispatches tasks if not already scheduled, then processes them immediately using Action Scheduler's queue runner + - When tables are NOT registered: Processes tasks immediately in a synchronous manner (fallback) + - Tasks already scheduled (via `dispatch()`) will be executed without re-dispatching +- **Actions Fired:** + - `shepherd_{prefix}_task_run_sync` - When tables are not registered and task runs synchronously + - `shepherd_{prefix}_task_before_run` - Before each task is processed + - `shepherd_{prefix}_task_after_run` - After each task completes successfully + - `shepherd_{prefix}_tasks_run_failed` - When a task fails during the run + - `shepherd_{prefix}_tasks_finished` - After all tasks have been processed +- **Use Cases:** + - CLI commands that need immediate task execution + - REST API endpoints that need synchronous task processing + - Testing scenarios requiring controlled task execution + ##### `get_last_scheduled_task_id(): ?int` Returns the ID of the most recently scheduled task. @@ -526,6 +553,21 @@ Table name: `shepherd_{prefix}_task_logs` - `shepherd_{prefix}_http_request_processed` - Fired after successful HTTP request - Parameters: `$task` (HTTP_Request instance), `$response` (wp_remote_request response array) +- `shepherd_{prefix}_task_run_sync` - Fired when a task is run synchronously via `run()` when tables are not registered (since 0.1.0) + - Parameters: `$task` (Task instance) + +- `shepherd_{prefix}_task_before_run` - Fired before a task is processed via `run()` (since 0.1.0) + - Parameters: `$task` (Task instance) + +- `shepherd_{prefix}_task_after_run` - Fired after a task completes via `run()` (since 0.1.0) + - Parameters: `$task` (Task instance) + +- `shepherd_{prefix}_tasks_run_failed` - Fired when a task fails during `run()` (since 0.1.0) + - Parameters: `$task` (Task instance or null), `$exception` (Exception) + +- `shepherd_{prefix}_tasks_finished` - Fired after all tasks have been processed via `run()` (since 0.1.0) + - Parameters: `$tasks` (array of Task instances) + ### Filters - `shepherd_{prefix}_should_log` - Filter to control whether logging should occur (since 0.0.5) diff --git a/docs/getting-started.md b/docs/getting-started.md index 4ca065b..4612347 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -134,6 +134,9 @@ shepherd()->dispatch( $my_task ); // Or dispatch with a delay (in seconds) shepherd()->dispatch( $my_task, 5 * MINUTE_IN_SECONDS ); // Execute after 5 minutes + +// Run tasks immediately (synchronous execution - since 0.1.0) +shepherd()->run( [ $my_task ] ); ``` ### What Happens Next? From 6e84b2e065def65548ff7077a44285037c9d19d3 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 19:16:08 +0200 Subject: [PATCH 03/20] tests: add test coverage --- tests/integration/Regulator_Test.php | 134 ++++++++++++++++++++++ tests/wpunit/Regulator_Test.php | 161 +++++++++++++++++++++++++++ 2 files changed, 295 insertions(+) diff --git a/tests/integration/Regulator_Test.php b/tests/integration/Regulator_Test.php index 5688506..750251b 100644 --- a/tests/integration/Regulator_Test.php +++ b/tests/integration/Regulator_Test.php @@ -252,4 +252,138 @@ public function it_should_retry_task_and_succeed(): void { $this->assertMatchesLogSnapshot( $logs ); } + + /** + * @test + */ + public function it_should_run_tasks_and_log_lifecycle(): void { + $shepherd = shepherd(); + $this->assertNull( $shepherd->get_last_scheduled_task_id() ); + + $task1 = new Do_Prefixed_Action_Task( 'run_log_1' ); + $task2 = new Do_Prefixed_Action_Task( 'run_log_2' ); + + $this->assertSame( 0, did_action( $task1->get_task_name() ) ); + $this->assertSame( 0, did_action( $task2->get_task_name() ) ); + + $shepherd->run( [ $task1, $task2 ] ); + + $this->assertSame( 1, did_action( $task1->get_task_name() ) ); + $this->assertSame( 1, did_action( $task2->get_task_name() ) ); + + // Verify logs for task1 + $logs1 = $this->get_logger()->retrieve_logs( $task1->get_id() ); + $this->assertCount( 3, $logs1 ); + $this->assertSame( 'created', $logs1[0]->get_type() ); + $this->assertSame( 'started', $logs1[1]->get_type() ); + $this->assertSame( 'finished', $logs1[2]->get_type() ); + + // Verify logs for task2 + $logs2 = $this->get_logger()->retrieve_logs( $task2->get_id() ); + $this->assertCount( 3, $logs2 ); + $this->assertSame( 'created', $logs2[0]->get_type() ); + $this->assertSame( 'started', $logs2[1]->get_type() ); + $this->assertSame( 'finished', $logs2[2]->get_type() ); + } + + /** + * @test + */ + public function it_should_run_tasks_and_call_on_error_when_task_fails(): void { + $shepherd = shepherd(); + + $task1 = new Do_Prefixed_Action_Task( 'run_error_1' ); + $task2 = new Always_Fail_Task( 'run_error_2' ); + $task3 = new Do_Prefixed_Action_Task( 'run_error_3' ); + + $error_task = null; + $error_exception = null; + $always_called = false; + $always_tasks = null; + + $prefix = tests_shepherd_get_hook_prefix(); + $run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" ); + + $shepherd->run( [ $task1, $task2, $task3 ], [ + 'on_error' => function( $task, $e ) use ( &$error_task, &$error_exception ) { + $error_task = $task; + $error_exception = $e; + }, + 'always' => function( $tasks ) use ( &$always_called, &$always_tasks ) { + $always_called = true; + $always_tasks = $tasks; + }, + ] ); + + // First task should have run + $this->assertSame( 1, did_action( $task1->get_task_name() ) ); + + // Error callback should have been called + $this->assertNotNull( $error_task ); + $this->assertNotNull( $error_exception ); + $this->assertInstanceOf( Exception::class, $error_exception ); + + // Always callback should have been called even on error + $this->assertTrue( $always_called, 'always callable should be called even when tasks fail' ); + + // tasks_run_failed action should have fired + $this->assertSame( $run_failed_count + 1, did_action( "shepherd_{$prefix}_tasks_run_failed" ) ); + } + + /** + * @test + */ + public function it_should_run_task_that_was_previously_dispatched(): void { + $shepherd = shepherd(); + + $task = new Do_Prefixed_Action_Task( 'run_dispatched' ); + + // First dispatch the task normally + $shepherd->dispatch( $task ); + $task_id = $shepherd->get_last_scheduled_task_id(); + + $this->assertSame( 0, did_action( $task->get_task_name() ) ); + + // Now run it - should use the already dispatched task + $shepherd->run( [ $task ] ); + + $this->assertSame( 1, did_action( $task->get_task_name() ) ); + + // Verify the logs show the full lifecycle + $logs = $this->get_logger()->retrieve_logs( $task_id ); + $this->assertCount( 3, $logs ); + $this->assertSame( 'created', $logs[0]->get_type() ); + $this->assertSame( 'started', $logs[1]->get_type() ); + $this->assertSame( 'finished', $logs[2]->get_type() ); + } + + /** + * @test + */ + public function it_should_run_single_task_successfully(): void { + $shepherd = shepherd(); + + $task = new Do_Action_Task(); + + $before_called = false; + $after_called = false; + $always_called = false; + + $shepherd->run( [ $task ], [ + 'before' => function() use ( &$before_called ) { + $before_called = true; + }, + 'after' => function() use ( &$after_called ) { + $after_called = true; + }, + 'always' => function() use ( &$always_called ) { + $always_called = true; + }, + ] ); + + $this->assertSame( 1, did_action( $task->get_task_name() ) ); + $this->assertTrue( $before_called, 'before callable should have been called' ); + $this->assertTrue( $after_called, 'after callable should have been called' ); + $this->assertTrue( $always_called, 'always callable should have been called' ); + } } diff --git a/tests/wpunit/Regulator_Test.php b/tests/wpunit/Regulator_Test.php index 634752d..d8fa11a 100644 --- a/tests/wpunit/Regulator_Test.php +++ b/tests/wpunit/Regulator_Test.php @@ -327,4 +327,165 @@ public function it_should_do_default_when_returning_non_callable(): void { $this->assertNotNull( $regulator->get_last_scheduled_task_id(), 'Task should be scheduled when custom handler is not callable' ); } + + /** + * @test + */ + public function it_should_run_tasks_synchronously_when_tables_not_registered(): void { + $prefix = Config::get_hook_prefix(); + + $this->set_fn_return( 'did_action', function( $action ) use ( $prefix ) { + if ( $action === "shepherd_{$prefix}_tables_registered" ) { + return 0; + } + + return did_action( $action ); + }, true ); + + $regulator = Config::get_container()->get( Regulator::class ); + + $task1 = new Do_Prefixed_Action_Task( 'run_task_1' ); + $task2 = new Do_Prefixed_Action_Task( 'run_task_2' ); + + $this->assertSame( 0, did_action( $task1->get_task_name() ) ); + $this->assertSame( 0, did_action( $task2->get_task_name() ) ); + + $sync_run_count = 0; + add_action( "shepherd_{$prefix}_task_run_sync", function() use ( &$sync_run_count ) { + $sync_run_count++; + } ); + + $regulator->run( [ $task1, $task2 ] ); + + $this->assertSame( 1, did_action( $task1->get_task_name() ), 'First task should have run' ); + $this->assertSame( 1, did_action( $task2->get_task_name() ), 'Second task should have run' ); + $this->assertSame( 2, $sync_run_count, 'sync action should have fired twice' ); + } + + /** + * @test + */ + public function it_should_run_tasks_and_dispatch_when_tables_registered(): void { + $prefix = Config::get_hook_prefix(); + $regulator = Config::get_container()->get( Regulator::class ); + + $task1 = new Do_Prefixed_Action_Task( 'run_dispatch_1' ); + $task2 = new Do_Prefixed_Action_Task( 'run_dispatch_2' ); + + $this->assertSame( 0, did_action( $task1->get_task_name() ) ); + $this->assertSame( 0, did_action( $task2->get_task_name() ) ); + + $before_run_count = 0; + $after_run_count = 0; + $tasks_finished_count = 0; + + add_action( "shepherd_{$prefix}_task_before_run", function() use ( &$before_run_count ) { + $before_run_count++; + } ); + + add_action( "shepherd_{$prefix}_task_after_run", function() use ( &$after_run_count ) { + $after_run_count++; + } ); + + add_action( "shepherd_{$prefix}_tasks_finished", function() use ( &$tasks_finished_count ) { + $tasks_finished_count++; + } ); + + $regulator->run( [ $task1, $task2 ] ); + + $this->assertSame( 1, did_action( $task1->get_task_name() ), 'First task should have run' ); + $this->assertSame( 1, did_action( $task2->get_task_name() ), 'Second task should have run' ); + $this->assertSame( 2, $before_run_count, 'before_run action should have fired twice' ); + $this->assertSame( 2, $after_run_count, 'after_run action should have fired twice' ); + $this->assertSame( 1, $tasks_finished_count, 'tasks_finished action should have fired once' ); + } + + /** + * @test + */ + public function it_should_execute_before_callable_for_each_task(): void { + $regulator = Config::get_container()->get( Regulator::class ); + + $task1 = new Do_Prefixed_Action_Task( 'before_callable_1' ); + $task2 = new Do_Prefixed_Action_Task( 'before_callable_2' ); + + $before_tasks = []; + + $regulator->run( [ $task1, $task2 ], [ + 'before' => function( $task ) use ( &$before_tasks ) { + $before_tasks[] = $task; + }, + ] ); + + $this->assertCount( 2, $before_tasks ); + $this->assertSame( $task1->get_args_hash(), $before_tasks[0]->get_args_hash() ); + $this->assertSame( $task2->get_args_hash(), $before_tasks[1]->get_args_hash() ); + } + + /** + * @test + */ + public function it_should_execute_after_callable_for_each_task(): void { + $regulator = Config::get_container()->get( Regulator::class ); + + $task1 = new Do_Prefixed_Action_Task( 'after_callable_1' ); + $task2 = new Do_Prefixed_Action_Task( 'after_callable_2' ); + + $after_tasks = []; + + $regulator->run( [ $task1, $task2 ], [ + 'after' => function( $task ) use ( &$after_tasks ) { + $after_tasks[] = $task; + }, + ] ); + + $this->assertCount( 2, $after_tasks ); + $this->assertSame( $task1->get_args_hash(), $after_tasks[0]->get_args_hash() ); + $this->assertSame( $task2->get_args_hash(), $after_tasks[1]->get_args_hash() ); + } + + /** + * @test + */ + public function it_should_execute_always_callable_after_all_tasks(): void { + $regulator = Config::get_container()->get( Regulator::class ); + + $task1 = new Do_Prefixed_Action_Task( 'always_callable_1' ); + $task2 = new Do_Prefixed_Action_Task( 'always_callable_2' ); + + $always_executed = false; + $always_tasks = null; + + $regulator->run( [ $task1, $task2 ], [ + 'always' => function( $tasks ) use ( &$always_executed, &$always_tasks ) { + $always_executed = true; + $always_tasks = $tasks; + }, + ] ); + + $this->assertTrue( $always_executed, 'always callable should have been executed' ); + $this->assertCount( 2, $always_tasks ); + } + + /** + * @test + */ + public function it_should_not_dispatch_already_scheduled_tasks_when_running(): void { + $prefix = Config::get_hook_prefix(); + $regulator = Config::get_container()->get( Regulator::class ); + + $task = new Do_Prefixed_Action_Task( 'already_scheduled_run' ); + + // First dispatch the task + $regulator->dispatch( $task ); + $first_task_id = $regulator->get_last_scheduled_task_id(); + + $created_count = did_action( "shepherd_{$prefix}_task_created" ); + + // Now run it - should not dispatch again since it's already scheduled + $regulator->run( [ $task ] ); + + // Check that task_created was NOT fired again + $this->assertSame( $created_count, did_action( "shepherd_{$prefix}_task_created" ), 'Task should not be dispatched again if already scheduled' ); + } } From a08f3833c36cb5effa34c3e31ef7380115b90d21 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Dec 2025 17:16:52 +0000 Subject: [PATCH 04/20] chore: autopublish 2025-12-16T17:16:52Z --- src/Regulator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Regulator.php b/src/Regulator.php index 0c79b09..da2ed22 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -444,7 +444,7 @@ private function run_callback( array $tasks, array $callables = [] ): void { } catch ( Exception $e ) { // The process_task method already catches and handles all the exceptions before throwing them again. if ( is_callable( $callables['on_error'] ) ) { - $callables['on_error']( isset( $task ) ? $task : null, $e ); + $callables['on_error']( $task ?? null, $e ); } /** @@ -455,7 +455,7 @@ private function run_callback( array $tasks, array $callables = [] ): void { * @param ?Task $task The task that failed to run. * @param Exception $e The exception that was thrown. */ - do_action( "shepherd_{$prefix}_tasks_run_failed", isset( $task ) ? $task : null, $e ); + do_action( "shepherd_{$prefix}_tasks_run_failed", $task ?? null, $e ); } if ( is_callable( $callables['always'] ) ) { From 5fbee64759e4f46b6a24a3170f6e8a76907b6ec6 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 19:24:50 +0200 Subject: [PATCH 05/20] feat: clean up memory every 10 tasks --- src/Regulator.php | 49 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Regulator.php b/src/Regulator.php index da2ed22..fe31b3d 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -25,6 +25,7 @@ use StellarWP\Shepherd\Traits\Loggable; use StellarWP\Shepherd\Tasks\Herding; use ActionScheduler_QueueRunner; +use WP_Object_Cache; /** * Shepherd's regulator. @@ -408,7 +409,7 @@ private function run_callback( array $tasks, array $callables = [] ): void { try { $runner = ActionScheduler_QueueRunner::instance(); - foreach ( $tasks as $task ) { + foreach ( array_values( $tasks ) as $offset => $task ) { if ( ! in_array( $task->get_id(), $scheduled_task_ids, true ) ) { $this->dispatch_callback( $task, 0 ); } @@ -440,6 +441,11 @@ private function run_callback( array $tasks, array $callables = [] ): void { * @param Task $task The task that is finished running. */ do_action( "shepherd_{$prefix}_task_after_run", $task ); + + // Free memory every 10 tasks to avoid memory issues. + if ( 0 === $offset % 10 ) { + $this->free_memory(); + } } } catch ( Exception $e ) { // The process_task method already catches and handles all the exceptions before throwing them again. @@ -697,4 +703,45 @@ public function schedule_cleanup_task(): void { */ do_action( 'shepherd_' . Config::get_hook_prefix() . '_cleanup_task_scheduled' ); } + + /** + * Reduce memory footprint by clearing the database query and object caches. + * + * @since 0.1.0 + * + * @return void + */ + private function free_memory(): void { + /** + * Globals. + * + * @var $wpdb \wpdb + * @var $wp_object_cache \WP_Object_Cache + */ + global $wpdb, $wp_object_cache; + + $wpdb->queries = array(); + + if ( ! $wp_object_cache instanceof WP_Object_Cache ) { + return; + } + + // Not all drop-ins support these props, however, there may be existing installations that rely on these being cleared. + if ( property_exists( $wp_object_cache, 'group_ops' ) ) { + $wp_object_cache->group_ops = array(); + } + if ( property_exists( $wp_object_cache, 'stats' ) ) { + $wp_object_cache->stats = array(); + } + if ( property_exists( $wp_object_cache, 'memcache_debug' ) ) { + $wp_object_cache->memcache_debug = array(); + } + if ( property_exists( $wp_object_cache, 'cache' ) ) { + $wp_object_cache->cache = array(); + } + + if ( is_callable( array( $wp_object_cache, '__remoteset' ) ) ) { + call_user_func( array( $wp_object_cache, '__remoteset' ) ); // important! + } + } } From b243f11c15c3b4eaab845c19b07673f616b5697d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Dec 2025 17:25:18 +0000 Subject: [PATCH 06/20] chore: autopublish 2025-12-16T17:25:18Z --- src/Regulator.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Regulator.php b/src/Regulator.php index fe31b3d..148887a 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -720,7 +720,7 @@ private function free_memory(): void { */ global $wpdb, $wp_object_cache; - $wpdb->queries = array(); + $wpdb->queries = []; if ( ! $wp_object_cache instanceof WP_Object_Cache ) { return; @@ -728,20 +728,20 @@ private function free_memory(): void { // Not all drop-ins support these props, however, there may be existing installations that rely on these being cleared. if ( property_exists( $wp_object_cache, 'group_ops' ) ) { - $wp_object_cache->group_ops = array(); + $wp_object_cache->group_ops = []; } if ( property_exists( $wp_object_cache, 'stats' ) ) { - $wp_object_cache->stats = array(); + $wp_object_cache->stats = []; } if ( property_exists( $wp_object_cache, 'memcache_debug' ) ) { - $wp_object_cache->memcache_debug = array(); + $wp_object_cache->memcache_debug = []; } if ( property_exists( $wp_object_cache, 'cache' ) ) { - $wp_object_cache->cache = array(); + $wp_object_cache->cache = []; } - if ( is_callable( array( $wp_object_cache, '__remoteset' ) ) ) { - call_user_func( array( $wp_object_cache, '__remoteset' ) ); // important! + if ( is_callable( [ $wp_object_cache, '__remoteset' ] ) ) { + call_user_func( [ $wp_object_cache, '__remoteset' ] ); // important! } } } From 5cfb45f47b72b03ac35e1273fb7f68a28b03750e Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 19:27:49 +0200 Subject: [PATCH 07/20] tweak: provide control of number of tasks before clean up --- src/Regulator.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Regulator.php b/src/Regulator.php index 148887a..9389da4 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -409,6 +409,15 @@ private function run_callback( array $tasks, array $callables = [] ): void { try { $runner = ActionScheduler_QueueRunner::instance(); + /** + * Filters the number of tasks to clean up after. + * + * @since 0.1.0 + * + * @param int $clean_up_memory_every The number of tasks to clean up the memory after. + */ + $clean_up_memory_every = apply_filters( "shepherd_{$prefix}_clean_up_memory_every", 10 ); + foreach ( array_values( $tasks ) as $offset => $task ) { if ( ! in_array( $task->get_id(), $scheduled_task_ids, true ) ) { $this->dispatch_callback( $task, 0 ); @@ -442,8 +451,7 @@ private function run_callback( array $tasks, array $callables = [] ): void { */ do_action( "shepherd_{$prefix}_task_after_run", $task ); - // Free memory every 10 tasks to avoid memory issues. - if ( 0 === $offset % 10 ) { + if ( 0 === $offset % $clean_up_memory_every ) { $this->free_memory(); } } From a52af145b6e32d7f2cd77b34b6b24da12b69c141 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 19:36:03 +0200 Subject: [PATCH 08/20] chrore: fix phpcs issues --- src/Regulator.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Regulator.php b/src/Regulator.php index 9389da4..eef75d6 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -332,12 +332,8 @@ protected function dispatch_callback( Task $task, int $delay ): void { * * @since 0.1.0 * - * @param Task[] $tasks The tasks to run. + * @param Task[] $tasks The tasks to run. * @param array $callables The callables to run. - * 'before' => function ( Task $task ): void {}, - * 'after' => function ( Task $task ): void {}, - * 'on_error' => function ( ?Task $task, Exception $e ): void {}, - * 'always' => function ( Task $task ): void {}, * * @return void */ @@ -380,18 +376,15 @@ function () use ( $tasks, $callables ): void { * * @since 0.1.0 * - * @param Task[] $tasks The tasks to run. + * @param Task[] $tasks The tasks to run. * @param array $callables The callables to run. - * 'before' => function ( Task $task ): void {}, - * 'after' => function ( Task $task ): void {}, - * 'on_error' => function ( ?Task $task, Exception $e ): void {}, - * 'always' => function ( Task $task ): void {}, * * @return void */ private function run_callback( array $tasks, array $callables = [] ): void { $scheduled_task_ids = array_map( fn( Task $task ) => $task->get_id(), $this->scheduled_tasks ); + /** @var array{before: void function ( Task $task ), after: void function ( Task $task ), on_error: void function ( ?Task $task, Exception $e ), always: void function ( Task $task )} $callables */ $callables = wp_parse_args( $callables, [ From 51aa3c014ba552ba89237b3cb4fa9f1a024792be Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 19:39:37 +0200 Subject: [PATCH 09/20] chore: fix static analysis --- src/Regulator.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Regulator.php b/src/Regulator.php index eef75d6..a834435 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -384,14 +384,14 @@ function () use ( $tasks, $callables ): void { private function run_callback( array $tasks, array $callables = [] ): void { $scheduled_task_ids = array_map( fn( Task $task ) => $task->get_id(), $this->scheduled_tasks ); - /** @var array{before: void function ( Task $task ), after: void function ( Task $task ), on_error: void function ( ?Task $task, Exception $e ), always: void function ( Task $task )} $callables */ + /** @var array{before: callable( Task $task ): void, after: callable( Task $task ): void, on_error: callable( ?Task $task, Exception $e ): void, always: callable( list $tasks ): void} $callables */ $callables = wp_parse_args( $callables, [ - 'before' => function ( Task $task ): void {}, - 'after' => function ( Task $task ): void {}, - 'on_error' => function ( Task $task, Exception $e ): void {}, - 'always' => function ( Task $task ): void {}, + 'before' => static function ( Task $task ): void {}, + 'after' => static function ( Task $task ): void {}, + 'on_error' => static function ( Task $task, Exception $e ): void {}, + 'always' => static function ( array $tasks ): void {}, ] ); @@ -716,8 +716,8 @@ private function free_memory(): void { /** * Globals. * - * @var $wpdb \wpdb - * @var $wp_object_cache \WP_Object_Cache + * @var \wpdb $wpdb + * @var WP_Object_Cache $wp_object_cache */ global $wpdb, $wp_object_cache; From a962da6ad432dfd7b7d25252c24f578934a1de5b Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 19:40:53 +0200 Subject: [PATCH 10/20] chore: update AS workflow --- .github/workflows/update_as.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/update_as.yml b/.github/workflows/update_as.yml index 9e460af..5459a03 100644 --- a/.github/workflows/update_as.yml +++ b/.github/workflows/update_as.yml @@ -64,8 +64,7 @@ jobs: run: | ${SLIC_BIN} up wordpress ${SLIC_BIN} wp core version - ${SLIC_BIN} wp core update --force --version=6.7 - ${SLIC_BIN} wp core version + ${SLIC_BIN} wp core update --force ${SLIC_BIN} use shepherd - name: Install WooCommerce From 8067f44c8383b057aaf3f52143196430330e3397 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 19:43:40 +0200 Subject: [PATCH 11/20] chore: update as workflow once again --- .github/workflows/update_as.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update_as.yml b/.github/workflows/update_as.yml index 5459a03..1b9a325 100644 --- a/.github/workflows/update_as.yml +++ b/.github/workflows/update_as.yml @@ -64,7 +64,8 @@ jobs: run: | ${SLIC_BIN} up wordpress ${SLIC_BIN} wp core version - ${SLIC_BIN} wp core update --force + ${SLIC_BIN} wp core update --force --version=6.9 + ${SLIC_BIN} wp core version ${SLIC_BIN} use shepherd - name: Install WooCommerce From cc6d0f394f0121c61147907b12915c1cc11c7215 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 19:55:55 +0200 Subject: [PATCH 12/20] tweak: remove on_error method --- CLAUDE.md | 1 - docs/advanced-usage.md | 17 ------ docs/api-reference.md | 1 - src/Regulator.php | 86 +++++++++++----------------- tests/integration/Regulator_Test.php | 44 -------------- 5 files changed, 33 insertions(+), 116 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9fcdacf..913ab9b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -112,7 +112,6 @@ shepherd()->run( [ 'before' => function( $task ) { /* called before each task */ }, 'after' => function( $task ) { /* called after each task */ }, - 'on_error' => function( $task, $e ) { /* called on failure */ }, 'always' => function( $tasks ) { /* called after all tasks */ }, ] ); diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 1f68bc1..2813df3 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -409,12 +409,6 @@ shepherd()->run( $tasks, [ error_log( 'Completed task: ' . get_class( $task ) ); }, - // Called if any task throws an exception - 'on_error' => function( ?Task $task, Exception $e ): void { - error_log( 'Task failed: ' . $e->getMessage() ); - // Optionally notify administrators or log to external service - }, - // Called after all tasks complete (even if some failed) 'always' => function( array $tasks ): void { error_log( 'Finished processing ' . count( $tasks ) . ' tasks' ); @@ -446,10 +440,6 @@ WP_CLI::add_command( 'myapp process-images', function( $args, $assoc_args ) { $processed++; WP_CLI::success( 'Image processed!' ); }, - 'on_error' => function( ?Task $task, Exception $e ) use ( &$failed ) { - $failed++; - WP_CLI::warning( 'Failed: ' . $e->getMessage() ); - }, 'always' => function( array $tasks ) use ( &$processed, &$failed ) { WP_CLI::line( "Processed: {$processed}, Failed: {$failed}" ); }, @@ -480,12 +470,6 @@ register_rest_route( 'myapp/v1', '/process', [ 'after' => function( Task $task ) use ( &$results ) { $results['processed'][] = $task->get_args()[0]; }, - 'on_error' => function( ?Task $task, Exception $e ) use ( &$results ) { - $results['failed'][] = [ - 'item' => $task ? $task->get_args()[0] : null, - 'error' => $e->getMessage(), - ]; - }, ] ); return new WP_REST_Response( $results, 200 ); @@ -498,7 +482,6 @@ register_rest_route( 'myapp/v1', '/process', [ - **Already scheduled tasks**: If a task was previously dispatched via `dispatch()`, `run()` will execute it without re-dispatching - **Fallback mode**: When Shepherd's database tables are not registered, tasks execute immediately via `process()` without Action Scheduler -- **Error handling**: When a task fails, the `on_error` callback is invoked and the remaining tasks in the batch are skipped - **Context detection**: Shepherd automatically detects CLI and REST contexts for proper logging ### WordPress Hooks diff --git a/docs/api-reference.md b/docs/api-reference.md index e85488c..22f8b71 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -46,7 +46,6 @@ Runs a set of tasks synchronously with optional lifecycle callbacks. - `$callables` - Optional array of lifecycle callbacks: - `'before'` - `function( Task $task ): void` - Called before each task runs - `'after'` - `function( Task $task ): void` - Called after each task completes - - `'on_error'` - `function( ?Task $task, Exception $e ): void` - Called if a task fails - `'always'` - `function( array $tasks ): void` - Called after all tasks complete (even on error) - **Since:** 0.1.0 - **Behavior:** diff --git a/src/Regulator.php b/src/Regulator.php index a834435..c7c6f35 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -382,15 +382,12 @@ function () use ( $tasks, $callables ): void { * @return void */ private function run_callback( array $tasks, array $callables = [] ): void { - $scheduled_task_ids = array_map( fn( Task $task ) => $task->get_id(), $this->scheduled_tasks ); - - /** @var array{before: callable( Task $task ): void, after: callable( Task $task ): void, on_error: callable( ?Task $task, Exception $e ): void, always: callable( list $tasks ): void} $callables */ + /** @var array{before: callable( Task $task ): void, after: callable( Task $task ): void, always: callable( list $tasks ): void} $callables */ $callables = wp_parse_args( $callables, [ 'before' => static function ( Task $task ): void {}, 'after' => static function ( Task $task ): void {}, - 'on_error' => static function ( Task $task, Exception $e ): void {}, 'always' => static function ( array $tasks ): void {}, ] ); @@ -399,70 +396,53 @@ private function run_callback( array $tasks, array $callables = [] ): void { $context = ! $context && defined( 'REST_REQUEST' ) && REST_REQUEST ? ' REST' : $context; $prefix = Config::get_hook_prefix(); - try { - $runner = ActionScheduler_QueueRunner::instance(); + $runner = ActionScheduler_QueueRunner::instance(); + + /** + * Filters the number of tasks to clean up after. + * + * @since 0.1.0 + * + * @param int $clean_up_memory_every The number of tasks to clean up the memory after. + */ + $clean_up_memory_every = apply_filters( "shepherd_{$prefix}_clean_up_memory_every", 10 ); + + foreach ( array_values( $tasks ) as $offset => $task ) { + if ( ! in_array( $task->get_id(), $this->scheduled_tasks, true ) ) { + $this->dispatch_callback( $task, 0 ); + } + + if ( is_callable( $callables['before'] ) ) { + $callables['before']( $task ); + } /** - * Filters the number of tasks to clean up after. + * Fires when a task is about to be run. * * @since 0.1.0 * - * @param int $clean_up_memory_every The number of tasks to clean up the memory after. + * @param Task $task The task that is about to be run. */ - $clean_up_memory_every = apply_filters( "shepherd_{$prefix}_clean_up_memory_every", 10 ); - - foreach ( array_values( $tasks ) as $offset => $task ) { - if ( ! in_array( $task->get_id(), $scheduled_task_ids, true ) ) { - $this->dispatch_callback( $task, 0 ); - } - - if ( is_callable( $callables['before'] ) ) { - $callables['before']( $task ); - } - - /** - * Fires when a task is about to be run. - * - * @since 0.1.0 - * - * @param Task $task The task that is about to be run. - */ - do_action( "shepherd_{$prefix}_task_before_run", $task ); - - $runner->process_action( $task->get_action_id(), "Shepherd{$context}" ); + do_action( "shepherd_{$prefix}_task_before_run", $task ); - if ( is_callable( $callables['after'] ) ) { - $callables['after']( $task ); - } - - /** - * Fires when a task is finished running. - * - * @since 0.1.0 - * - * @param Task $task The task that is finished running. - */ - do_action( "shepherd_{$prefix}_task_after_run", $task ); + $runner->process_action( $task->get_action_id(), "Shepherd{$context}" ); - if ( 0 === $offset % $clean_up_memory_every ) { - $this->free_memory(); - } - } - } catch ( Exception $e ) { - // The process_task method already catches and handles all the exceptions before throwing them again. - if ( is_callable( $callables['on_error'] ) ) { - $callables['on_error']( $task ?? null, $e ); + if ( is_callable( $callables['after'] ) ) { + $callables['after']( $task ); } /** - * Fires when a set of tasks fails to be run. + * Fires when a task is finished running. * * @since 0.1.0 * - * @param ?Task $task The task that failed to run. - * @param Exception $e The exception that was thrown. + * @param Task $task The task that is finished running. */ - do_action( "shepherd_{$prefix}_tasks_run_failed", $task ?? null, $e ); + do_action( "shepherd_{$prefix}_task_after_run", $task ); + + if ( 0 === $offset % $clean_up_memory_every ) { + $this->free_memory(); + } } if ( is_callable( $callables['always'] ) ) { diff --git a/tests/integration/Regulator_Test.php b/tests/integration/Regulator_Test.php index 750251b..6c8baf8 100644 --- a/tests/integration/Regulator_Test.php +++ b/tests/integration/Regulator_Test.php @@ -286,50 +286,6 @@ public function it_should_run_tasks_and_log_lifecycle(): void { $this->assertSame( 'finished', $logs2[2]->get_type() ); } - /** - * @test - */ - public function it_should_run_tasks_and_call_on_error_when_task_fails(): void { - $shepherd = shepherd(); - - $task1 = new Do_Prefixed_Action_Task( 'run_error_1' ); - $task2 = new Always_Fail_Task( 'run_error_2' ); - $task3 = new Do_Prefixed_Action_Task( 'run_error_3' ); - - $error_task = null; - $error_exception = null; - $always_called = false; - $always_tasks = null; - - $prefix = tests_shepherd_get_hook_prefix(); - $run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" ); - - $shepherd->run( [ $task1, $task2, $task3 ], [ - 'on_error' => function( $task, $e ) use ( &$error_task, &$error_exception ) { - $error_task = $task; - $error_exception = $e; - }, - 'always' => function( $tasks ) use ( &$always_called, &$always_tasks ) { - $always_called = true; - $always_tasks = $tasks; - }, - ] ); - - // First task should have run - $this->assertSame( 1, did_action( $task1->get_task_name() ) ); - - // Error callback should have been called - $this->assertNotNull( $error_task ); - $this->assertNotNull( $error_exception ); - $this->assertInstanceOf( Exception::class, $error_exception ); - - // Always callback should have been called even on error - $this->assertTrue( $always_called, 'always callable should be called even when tasks fail' ); - - // tasks_run_failed action should have fired - $this->assertSame( $run_failed_count + 1, did_action( "shepherd_{$prefix}_tasks_run_failed" ) ); - } - /** * @test */ From 48965e6b880207fb3d70757fd58a62476ec33066 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Dec 2025 17:58:23 +0000 Subject: [PATCH 13/20] chore: autopublish 2025-12-16T17:58:23Z --- src/Regulator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Regulator.php b/src/Regulator.php index c7c6f35..b370786 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -386,9 +386,9 @@ private function run_callback( array $tasks, array $callables = [] ): void { $callables = wp_parse_args( $callables, [ - 'before' => static function ( Task $task ): void {}, - 'after' => static function ( Task $task ): void {}, - 'always' => static function ( array $tasks ): void {}, + 'before' => static function ( Task $task ): void {}, + 'after' => static function ( Task $task ): void {}, + 'always' => static function ( array $tasks ): void {}, ] ); From 4401ca255db50dbec2f9219c01c06716071d7ca2 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Tue, 16 Dec 2025 20:31:21 +0200 Subject: [PATCH 14/20] chore: trigger php tests --- tests/integration/Regulator_Test.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/integration/Regulator_Test.php b/tests/integration/Regulator_Test.php index 6c8baf8..9580084 100644 --- a/tests/integration/Regulator_Test.php +++ b/tests/integration/Regulator_Test.php @@ -271,14 +271,12 @@ public function it_should_run_tasks_and_log_lifecycle(): void { $this->assertSame( 1, did_action( $task1->get_task_name() ) ); $this->assertSame( 1, did_action( $task2->get_task_name() ) ); - // Verify logs for task1 $logs1 = $this->get_logger()->retrieve_logs( $task1->get_id() ); $this->assertCount( 3, $logs1 ); $this->assertSame( 'created', $logs1[0]->get_type() ); $this->assertSame( 'started', $logs1[1]->get_type() ); $this->assertSame( 'finished', $logs1[2]->get_type() ); - // Verify logs for task2 $logs2 = $this->get_logger()->retrieve_logs( $task2->get_id() ); $this->assertCount( 3, $logs2 ); $this->assertSame( 'created', $logs2[0]->get_type() ); @@ -294,18 +292,15 @@ public function it_should_run_task_that_was_previously_dispatched(): void { $task = new Do_Prefixed_Action_Task( 'run_dispatched' ); - // First dispatch the task normally $shepherd->dispatch( $task ); $task_id = $shepherd->get_last_scheduled_task_id(); $this->assertSame( 0, did_action( $task->get_task_name() ) ); - // Now run it - should use the already dispatched task $shepherd->run( [ $task ] ); $this->assertSame( 1, did_action( $task->get_task_name() ) ); - // Verify the logs show the full lifecycle $logs = $this->get_logger()->retrieve_logs( $task_id ); $this->assertCount( 3, $logs ); $this->assertSame( 'created', $logs[0]->get_type() ); From 902459172fa9381b74bed178066c79c5232e0701 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 17 Dec 2025 16:44:10 +0200 Subject: [PATCH 15/20] tweak: amend CR comments --- .github/workflows/update_as.yml | 2 +- src/Regulator.php | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/update_as.yml b/.github/workflows/update_as.yml index 1b9a325..bd1dc7b 100644 --- a/.github/workflows/update_as.yml +++ b/.github/workflows/update_as.yml @@ -64,7 +64,7 @@ jobs: run: | ${SLIC_BIN} up wordpress ${SLIC_BIN} wp core version - ${SLIC_BIN} wp core update --force --version=6.9 + ${SLIC_BIN} wp core update --force --version=latest ${SLIC_BIN} wp core version ${SLIC_BIN} use shepherd diff --git a/src/Regulator.php b/src/Regulator.php index b370786..8dbba1b 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -335,6 +335,12 @@ protected function dispatch_callback( Task $task, int $delay ): void { * @param Task[] $tasks The tasks to run. * @param array $callables The callables to run. * + * @phpstan-param array{ + * before: callable( Task $task ): void, + * after: callable( Task $task ): void, + * always: callable( list $tasks ): void, + * } + * * @return void */ public function run( array $tasks, array $callables = [] ): void { @@ -379,10 +385,15 @@ function () use ( $tasks, $callables ): void { * @param Task[] $tasks The tasks to run. * @param array $callables The callables to run. * + * @phpstan-param array{ + * before: callable( Task $task ): void, + * after: callable( Task $task ): void, + * always: callable( list $tasks ): void, + * } + * * @return void */ private function run_callback( array $tasks, array $callables = [] ): void { - /** @var array{before: callable( Task $task ): void, after: callable( Task $task ): void, always: callable( list $tasks ): void} $callables */ $callables = wp_parse_args( $callables, [ @@ -404,8 +415,10 @@ private function run_callback( array $tasks, array $callables = [] ): void { * @since 0.1.0 * * @param int $clean_up_memory_every The number of tasks to clean up the memory after. + * + * @return int The number of tasks to clean up the memory after. */ - $clean_up_memory_every = apply_filters( "shepherd_{$prefix}_clean_up_memory_every", 10 ); + $clean_up_memory_every = (int) apply_filters( "shepherd_{$prefix}_clean_up_memory_every", 10 ); foreach ( array_values( $tasks ) as $offset => $task ) { if ( ! in_array( $task->get_id(), $this->scheduled_tasks, true ) ) { @@ -440,7 +453,7 @@ private function run_callback( array $tasks, array $callables = [] ): void { */ do_action( "shepherd_{$prefix}_task_after_run", $task ); - if ( 0 === $offset % $clean_up_memory_every ) { + if ( 0 === ( $offset + 1 ) % $clean_up_memory_every ) { $this->free_memory(); } } From 033cc8bd17bdfdf1fb560f7b021bf77b04de218b Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 17 Dec 2025 16:58:32 +0200 Subject: [PATCH 16/20] tweak: fix static analysis --- src/Regulator.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Regulator.php b/src/Regulator.php index 8dbba1b..bf103f9 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -335,11 +335,11 @@ protected function dispatch_callback( Task $task, int $delay ): void { * @param Task[] $tasks The tasks to run. * @param array $callables The callables to run. * - * @phpstan-param array{ + * @phpstan-param array{} | array{ * before: callable( Task $task ): void, * after: callable( Task $task ): void, * always: callable( list $tasks ): void, - * } + * } $callables * * @return void */ @@ -385,11 +385,11 @@ function () use ( $tasks, $callables ): void { * @param Task[] $tasks The tasks to run. * @param array $callables The callables to run. * - * @phpstan-param array{ + * @phpstan-param array{} | array{ * before: callable( Task $task ): void, * after: callable( Task $task ): void, * always: callable( list $tasks ): void, - * } + * } $callables * * @return void */ From c2adecb1e63759b1dba15a1aa465b62c01858014 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 17 Dec 2025 17:03:47 +0200 Subject: [PATCH 17/20] tweak: ensure possible exceptions inside callables are caught --- src/Regulator.php | 113 ++++++++++++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 45 deletions(-) diff --git a/src/Regulator.php b/src/Regulator.php index bf103f9..1bf6f11 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -409,67 +409,90 @@ private function run_callback( array $tasks, array $callables = [] ): void { $runner = ActionScheduler_QueueRunner::instance(); - /** - * Filters the number of tasks to clean up after. - * - * @since 0.1.0 - * - * @param int $clean_up_memory_every The number of tasks to clean up the memory after. - * - * @return int The number of tasks to clean up the memory after. - */ - $clean_up_memory_every = (int) apply_filters( "shepherd_{$prefix}_clean_up_memory_every", 10 ); - - foreach ( array_values( $tasks ) as $offset => $task ) { - if ( ! in_array( $task->get_id(), $this->scheduled_tasks, true ) ) { - $this->dispatch_callback( $task, 0 ); - } - - if ( is_callable( $callables['before'] ) ) { - $callables['before']( $task ); - } + try { /** - * Fires when a task is about to be run. + * Filters the number of tasks to clean up after. * * @since 0.1.0 * - * @param Task $task The task that is about to be run. + * @param int $clean_up_memory_every The number of tasks to clean up the memory after. + * + * @return int The number of tasks to clean up the memory after. */ - do_action( "shepherd_{$prefix}_task_before_run", $task ); + $clean_up_memory_every = (int) apply_filters( "shepherd_{$prefix}_clean_up_memory_every", 10 ); - $runner->process_action( $task->get_action_id(), "Shepherd{$context}" ); + foreach ( array_values( $tasks ) as $offset => $task ) { + if ( ! in_array( $task->get_id(), $this->scheduled_tasks, true ) ) { + $this->dispatch_callback( $task, 0 ); + } + + if ( is_callable( $callables['before'] ) ) { + $callables['before']( $task ); + } - if ( is_callable( $callables['after'] ) ) { - $callables['after']( $task ); + /** + * Fires when a task is about to be run. + * + * @since 0.1.0 + * + * @param Task $task The task that is about to be run. + */ + do_action( "shepherd_{$prefix}_task_before_run", $task ); + + $runner->process_action( $task->get_action_id(), "Shepherd{$context}" ); + + if ( is_callable( $callables['after'] ) ) { + $callables['after']( $task ); + } + + /** + * Fires when a task is finished running. + * + * @since 0.1.0 + * + * @param Task $task The task that is finished running. + */ + do_action( "shepherd_{$prefix}_task_after_run", $task ); + + if ( 0 === ( $offset + 1 ) % $clean_up_memory_every ) { + $this->free_memory(); + } + } + + if ( is_callable( $callables['always'] ) ) { + $callables['always']( $tasks ); } /** - * Fires when a task is finished running. + * Fires when a set of tasks is finished running. * * @since 0.1.0 * - * @param Task $task The task that is finished running. + * @param Task[] $tasks The tasks that were run. */ - do_action( "shepherd_{$prefix}_task_after_run", $task ); - - if ( 0 === ( $offset + 1 ) % $clean_up_memory_every ) { - $this->free_memory(); - } - } - - if ( is_callable( $callables['always'] ) ) { - $callables['always']( $tasks ); + do_action( "shepherd_{$prefix}_tasks_finished", $tasks ); + } catch ( Exception $e ) { + /** + * Fires when a set of tasks fails to be run. + * + * @since 0.1.0 + * + * @param Task[] $tasks The tasks that failed to be run. + * @param Exception $e The exception that was thrown. + */ + do_action( "shepherd_{$prefix}_tasks_run_failed", $tasks, $e ); + } catch ( Throwable $e ) { + /** + * Fires when a set of tasks fails to be run. + * + * @since 0.1.0 + * + * @param Task[] $tasks The tasks that failed to be run. + * @param Throwable $e The exception that was thrown. + */ + do_action( "shepherd_{$prefix}_tasks_run_failed", $tasks, $e ); } - - /** - * Fires when a set of tasks is finished running. - * - * @since 0.1.0 - * - * @param Task[] $tasks The tasks that were run. - */ - do_action( "shepherd_{$prefix}_tasks_finished", $tasks ); } /** From 0262946fc54d4f815742146b030d92feba29c787 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 17 Dec 2025 17:12:35 +0200 Subject: [PATCH 18/20] chore: add test coverage and update docs --- CHANGELOG.md | 2 +- CLAUDE.md | 6 +- docs/advanced-usage.md | 12 +-- docs/api-reference.md | 4 +- tests/integration/Regulator_Test.php | 120 +++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c8fab5..bbfc576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. This project adhere to the [Semantic Versioning](http://semver.org/) standard. -## [0.1.0] 2025-12-16 +## [0.1.0] 2025-12-17 * Feature - Introduces a method `run` to the Regulator class which enables running a set of tasks synchronously. diff --git a/CLAUDE.md b/CLAUDE.md index 913ab9b..7e57d33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,9 +110,9 @@ shepherd()->dispatch(new My_Task($arg1, $arg2), 300); // 5 minutes shepherd()->run( [ new My_Task($arg1, $arg2), new Another_Task() ], [ - 'before' => function( $task ) { /* called before each task */ }, - 'after' => function( $task ) { /* called after each task */ }, - 'always' => function( $tasks ) { /* called after all tasks */ }, + 'before' => function( $task ) { /* called before each task */ }, + 'after' => function( $task ) { /* called after each task */ }, + 'always' => function( $tasks ) { /* called after all tasks complete successfully */ }, ] ); diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 2813df3..a849fab 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -409,7 +409,7 @@ shepherd()->run( $tasks, [ error_log( 'Completed task: ' . get_class( $task ) ); }, - // Called after all tasks complete (even if some failed) + // Called after all tasks complete (even on error) 'always' => function( array $tasks ): void { error_log( 'Finished processing ' . count( $tasks ) . ' tasks' ); }, @@ -483,6 +483,7 @@ register_rest_route( 'myapp/v1', '/process', [ - **Already scheduled tasks**: If a task was previously dispatched via `dispatch()`, `run()` will execute it without re-dispatching - **Fallback mode**: When Shepherd's database tables are not registered, tasks execute immediately via `process()` without Action Scheduler - **Context detection**: Shepherd automatically detects CLI and REST contexts for proper logging +- **Exception handling**: Exceptions or Throwables thrown inside callables (`before`, `after`, `always`) are caught and trigger the `tasks_run_failed` action ### WordPress Hooks @@ -501,12 +502,13 @@ add_action( "shepherd_{$prefix}_task_after_run", function( Task $task ) { // Post-task cleanup or notifications }, 10, 1 ); -// Fired when any task fails -add_action( "shepherd_{$prefix}_tasks_run_failed", function( ?Task $task, Exception $e ) { - // Handle batch failure +// Fired when any task or callable fails (catches Exception and Throwable) +add_action( "shepherd_{$prefix}_tasks_run_failed", function( array $tasks, Throwable $e ) { + // Handle batch failure - receives all tasks and the exception/error + error_log( 'Tasks failed: ' . $e->getMessage() ); }, 10, 2 ); -// Fired after all tasks complete +// Fired after all tasks complete successfully add_action( "shepherd_{$prefix}_tasks_finished", function( array $tasks ) { // Batch completion handling }, 10, 1 ); diff --git a/docs/api-reference.md b/docs/api-reference.md index 22f8b71..f35ac94 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -561,8 +561,8 @@ Table name: `shepherd_{prefix}_task_logs` - `shepherd_{prefix}_task_after_run` - Fired after a task completes via `run()` (since 0.1.0) - Parameters: `$task` (Task instance) -- `shepherd_{prefix}_tasks_run_failed` - Fired when a task fails during `run()` (since 0.1.0) - - Parameters: `$task` (Task instance or null), `$exception` (Exception) +- `shepherd_{prefix}_tasks_run_failed` - Fired when a task or callable fails during `run()` (since 0.1.0) + - Parameters: `$tasks` (array of Task instances), `$exception` (Exception or Throwable) - `shepherd_{prefix}_tasks_finished` - Fired after all tasks have been processed via `run()` (since 0.1.0) - Parameters: `$tasks` (array of Task instances) diff --git a/tests/integration/Regulator_Test.php b/tests/integration/Regulator_Test.php index 9580084..5744e56 100644 --- a/tests/integration/Regulator_Test.php +++ b/tests/integration/Regulator_Test.php @@ -337,4 +337,124 @@ public function it_should_run_single_task_successfully(): void { $this->assertTrue( $after_called, 'after callable should have been called' ); $this->assertTrue( $always_called, 'always callable should have been called' ); } + + /** + * @test + */ + public function it_should_catch_exception_thrown_in_before_callable(): void { + $shepherd = shepherd(); + $prefix = tests_shepherd_get_hook_prefix(); + + $task = new Do_Action_Task(); + + $run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" ); + $captured_tasks = null; + $captured_exception = null; + + add_action( "shepherd_{$prefix}_tasks_run_failed", function( $tasks, $e ) use ( &$captured_tasks, &$captured_exception ) { + $captured_tasks = $tasks; + $captured_exception = $e; + }, 10, 2 ); + + $shepherd->run( [ $task ], [ + 'before' => function() { + throw new Exception( 'Before callable failed' ); + }, + ] ); + + $this->assertSame( $run_failed_count + 1, did_action( "shepherd_{$prefix}_tasks_run_failed" ), 'tasks_run_failed action should have fired' ); + $this->assertIsArray( $captured_tasks ); + $this->assertCount( 1, $captured_tasks ); + $this->assertInstanceOf( Exception::class, $captured_exception ); + $this->assertSame( 'Before callable failed', $captured_exception->getMessage() ); + } + + /** + * @test + */ + public function it_should_catch_exception_thrown_in_after_callable(): void { + $shepherd = shepherd(); + $prefix = tests_shepherd_get_hook_prefix(); + + $task = new Do_Action_Task(); + + $run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" ); + $captured_exception = null; + + add_action( "shepherd_{$prefix}_tasks_run_failed", function( $tasks, $e ) use ( &$captured_exception ) { + $captured_exception = $e; + }, 10, 2 ); + + $shepherd->run( [ $task ], [ + 'after' => function() { + throw new Exception( 'After callable failed' ); + }, + ] ); + + // Task should have run before the after callable threw + $this->assertSame( 1, did_action( $task->get_task_name() ), 'Task should have run before after callable failed' ); + $this->assertSame( $run_failed_count + 1, did_action( "shepherd_{$prefix}_tasks_run_failed" ), 'tasks_run_failed action should have fired' ); + $this->assertInstanceOf( Exception::class, $captured_exception ); + $this->assertSame( 'After callable failed', $captured_exception->getMessage() ); + } + + /** + * @test + */ + public function it_should_catch_exception_thrown_in_always_callable(): void { + $shepherd = shepherd(); + $prefix = tests_shepherd_get_hook_prefix(); + + $task = new Do_Action_Task(); + + $run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" ); + $tasks_finished_count = did_action( "shepherd_{$prefix}_tasks_finished" ); + $captured_exception = null; + + add_action( "shepherd_{$prefix}_tasks_run_failed", function( $tasks, $e ) use ( &$captured_exception ) { + $captured_exception = $e; + }, 10, 2 ); + + $shepherd->run( [ $task ], [ + 'always' => function() { + throw new Exception( 'Always callable failed' ); + }, + ] ); + + // Task should have run successfully + $this->assertSame( 1, did_action( $task->get_task_name() ), 'Task should have run' ); + // tasks_finished should NOT have fired because always callable threw before it + $this->assertSame( $tasks_finished_count, did_action( "shepherd_{$prefix}_tasks_finished" ), 'tasks_finished should not have fired' ); + // tasks_run_failed should have fired + $this->assertSame( $run_failed_count + 1, did_action( "shepherd_{$prefix}_tasks_run_failed" ), 'tasks_run_failed action should have fired' ); + $this->assertInstanceOf( Exception::class, $captured_exception ); + $this->assertSame( 'Always callable failed', $captured_exception->getMessage() ); + } + + /** + * @test + */ + public function it_should_catch_throwable_in_callable(): void { + $shepherd = shepherd(); + $prefix = tests_shepherd_get_hook_prefix(); + + $task = new Do_Action_Task(); + + $run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" ); + $captured_throwable = null; + + add_action( "shepherd_{$prefix}_tasks_run_failed", function( $tasks, $e ) use ( &$captured_throwable ) { + $captured_throwable = $e; + }, 10, 2 ); + + $shepherd->run( [ $task ], [ + 'before' => function() { + throw new \Error( 'Type error in callable' ); + }, + ] ); + + $this->assertSame( $run_failed_count + 1, did_action( "shepherd_{$prefix}_tasks_run_failed" ), 'tasks_run_failed action should have fired for Throwable' ); + $this->assertInstanceOf( \Throwable::class, $captured_throwable ); + $this->assertSame( 'Type error in callable', $captured_throwable->getMessage() ); + } } From 32437746a27a0f7f1f843a8eb637e22e2e7b0ae1 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 17 Dec 2025 17:15:46 +0200 Subject: [PATCH 19/20] tests: fix broken test --- tests/integration/Regulator_Test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/Regulator_Test.php b/tests/integration/Regulator_Test.php index 5744e56..6ed77ad 100644 --- a/tests/integration/Regulator_Test.php +++ b/tests/integration/Regulator_Test.php @@ -348,11 +348,11 @@ public function it_should_catch_exception_thrown_in_before_callable(): void { $task = new Do_Action_Task(); $run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" ); - $captured_tasks = null; + $captured_tasks = []; $captured_exception = null; add_action( "shepherd_{$prefix}_tasks_run_failed", function( $tasks, $e ) use ( &$captured_tasks, &$captured_exception ) { - $captured_tasks = $tasks; + $captured_tasks[] = $tasks; $captured_exception = $e; }, 10, 2 ); From bad6d9bbddadd76bc62fe567b5c4a62625448588 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 17 Dec 2025 17:33:40 +0200 Subject: [PATCH 20/20] tweak: remove exception and catch only throwables --- src/Regulator.php | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/src/Regulator.php b/src/Regulator.php index 1bf6f11..ddda1aa 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -16,7 +16,6 @@ use StellarWP\Shepherd\Contracts\Task; use StellarWP\Shepherd\Tables\Tasks as Tasks_Table; use RuntimeException; -use Exception; use Throwable; use StellarWP\DB\DB; use StellarWP\Shepherd\Exceptions\ShepherdTaskException; @@ -168,7 +167,7 @@ public function dispatch( Task $task, int $delay = 0 ): self { if ( null !== $handler && is_callable( $handler ) ) { try { $handler( $task, $delay ); - } catch ( Exception $e ) { + } catch ( Throwable $e ) { /** * Documented in the dispatch_callback method. */ @@ -472,16 +471,6 @@ private function run_callback( array $tasks, array $callables = [] ): void { * @param Task[] $tasks The tasks that were run. */ do_action( "shepherd_{$prefix}_tasks_finished", $tasks ); - } catch ( Exception $e ) { - /** - * Fires when a set of tasks fails to be run. - * - * @since 0.1.0 - * - * @param Task[] $tasks The tasks that failed to be run. - * @param Exception $e The exception that was thrown. - */ - do_action( "shepherd_{$prefix}_tasks_run_failed", $tasks, $e ); } catch ( Throwable $e ) { /** * Fires when a set of tasks fails to be run. @@ -537,7 +526,6 @@ public function bust_runtime_cached_tasks(): void { * @throws RuntimeException If no action ID is found, no Shepherd task is found with the action ID, or the task arguments hash does not match the expected hash. * @throws ShepherdTaskException If the task fails to be processed. * @throws ShepherdTaskFailWithoutRetryException If the task fails to be processed without retry. - * @throws Exception If the task fails to be processed. * @throws Throwable If the task fails to be processed. */ public function process_task( string $args_hash ): void { @@ -613,23 +601,6 @@ public function process_task( string $args_hash ): void { $this->log_failed( $task->get_id(), array_merge( $log_data, [ 'exception' => $e->getMessage() ] ) ); - throw $e; - } catch ( Exception $e ) { - /** - * Fires when a task fails to be processed. - * - * @since 0.0.1 - * - * @param Task $task The task that failed to be processed. - * @param Exception $e The exception that was thrown. - */ - do_action( 'shepherd_' . Config::get_hook_prefix() . '_task_failed', $task, $e ); - - if ( $this->should_retry( $task ) ) { - throw new ShepherdTaskException( esc_html_x( 'The task failed, but will be retried.', 'This error is thrown when a task fails to be processed, but will be retried.', 'stellarwp-shepherd' ) ); - } - - $this->log_failed( $task->get_id(), array_merge( $log_data, [ 'exception' => $e->getMessage() ] ) ); throw $e; } catch ( Throwable $e ) { /**