diff --git a/CHANGELOG.md b/CHANGELOG.md index c5131da..2e090d6 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.0.9] 2025-11-17 + +* Tweak - Add a filter `shepherd_{prefix}_dispatch_handler` to allow for custom dispatch handlers. + +[0.0.9]: https://github.com/stellarwp/shepherd/releases/tag/0.0.9 + ## [0.0.8] 2025-10-02 * Feature - Update the stellarwp/schema library to v3. diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 0918f86..a960e01 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -138,14 +138,14 @@ add_filter( "shepherd_{$prefix}_should_dispatch_sync_on_tables_unavailable", fun // Default behavior (since 0.0.8): // - Returns true when delay is 0 (immediate execution) // - Returns false when delay > 0 (skip execution) - + // Override examples: // Always process synchronously regardless of delay return true; - + // Never process synchronously // return false; - + // Custom logic based on task type // return $task instanceof Critical_Task; }, 10, 2 ); @@ -371,6 +371,31 @@ 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 +## 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. + +### Basic Usage + +```php +$prefix = Config::get_hook_prefix(); + +add_filter( "shepherd_{$prefix}_dispatch_handler", function( $handler, $task, $delay ) { + // Return a callable that will handle the dispatch + return function( $task, $delay ) { + // Your custom dispatch logic + my_custom_task_queue()->add( $task, $delay ); + }; +}, 10, 3 ); +``` + +### Important Notes + +- **Return null to use default handler**: If you return `null` or a non-callable value, Shepherd will use its default dispatch logic +- **Handler signature**: Your custom handler must accept two parameters: `$task` (Task instance) and `$delay` (integer) +- **Complete override**: When you provide a custom handler, Shepherd's default dispatch logic (including Action Scheduler integration) is completely bypassed +- **Responsibility**: Your custom handler is responsible for all aspects of task execution, including scheduling, retries, and logging + ## Advanced Integration ### WordPress Hooks diff --git a/shepherd.php b/shepherd.php index 66c9470..80eab0d 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.8 + * Version: 0.0.9 * Author: StellarWP * Author URI: https://stellarwp.com * License: GPL-2.0-or-later diff --git a/src/Regulator.php b/src/Regulator.php index 2a57673..a55ba61 100644 --- a/src/Regulator.php +++ b/src/Regulator.php @@ -143,6 +143,7 @@ public function untrack_action(): void { * @since 0.0.7 Updated to check if the Shepherd tables have been registered already. * @since 0.0.7 Updated to use the `action_scheduler_init` hook instead of the `init` hook to check if Action Scheduler is initialized. * @since 0.0.8 Updated to use the delay to determine if the task should be dispatched synchronously. + * @since 0.0.9 Added a filter `shepherd_{prefix}_dispatch_handler` to allow for custom dispatch handlers. * * @param Task $task The task to dispatch. * @param int $delay The delay in seconds before the task is processed. @@ -152,6 +153,29 @@ public function untrack_action(): void { public function dispatch( Task $task, int $delay = 0 ): self { $prefix = Config::get_hook_prefix(); + /** + * Filters the dispatch handler. + * + * @since TBD + * + * @param callable|null $handler The dispatch handler. + * @param Task $task The task to dispatch. + * @param int $delay The delay in seconds before the task is processed. + */ + $handler = apply_filters( "shepherd_{$prefix}_dispatch_handler", null, $task, $delay ); + if ( null !== $handler && is_callable( $handler ) ) { + try { + $handler( $task, $delay ); + } catch ( Exception $e ) { + /** + * Documented in the dispatch_callback method. + */ + do_action( 'shepherd_' . Config::get_hook_prefix() . '_task_scheduling_failed', $task, new RuntimeException( $e->getMessage(), $e->getCode(), $e ) ); + } + + return $this; + } + if ( ! did_action( "shepherd_{$prefix}_tables_registered" ) ) { /** * Filters whether to dispatch a task synchronously. diff --git a/tests/wpunit/Regulator_Test.php b/tests/wpunit/Regulator_Test.php index 2d989f6..634752d 100644 --- a/tests/wpunit/Regulator_Test.php +++ b/tests/wpunit/Regulator_Test.php @@ -10,6 +10,7 @@ use StellarWP\Shepherd\Tests\Tasks\Do_Action_Task; use StellarWP\Shepherd\Tests\Tasks\Do_Prefixed_Action_Task; use StellarWP\Shepherd\Tests\Traits\With_AS_Assertions; +use Exception; class Regulator_Test extends WPTestCase { use With_Uopz; @@ -219,4 +220,111 @@ public function it_should_schedule_cleanup_task_when_tables_are_registered(): vo $this->assertEquals( $current_count + 1, did_action( 'shepherd_' . $prefix . '_cleanup_task_scheduled' ), 'Cleanup task should not be scheduled when tables are not registered' ); } + + /** + * @test + */ + public function it_should_use_custom_dispatch_handler_when_provided_via_filter(): void { + $prefix = Config::get_hook_prefix(); + $regulator = Config::get_container()->get( Regulator::class ); + + $custom_handler_called = false; + $handler_received_task = null; + $handler_received_delay = null; + + add_filter( "shepherd_{$prefix}_dispatch_handler", function( $handler, $task, $delay ) use ( &$custom_handler_called, &$handler_received_task, &$handler_received_delay ) { + return function( $task, $delay ) use ( &$custom_handler_called, &$handler_received_task, &$handler_received_delay ) { + $custom_handler_called = true; + $handler_received_task = $task; + $handler_received_delay = $delay; + }; + }, 10, 3 ); + + $test_task = new Do_Action_Task(); + $delay = 60; + + $last_task_id = $regulator->get_last_scheduled_task_id(); + + $regulator->dispatch( $test_task, $delay ); + + $this->assertTrue( $custom_handler_called, 'Custom dispatch handler should have been called' ); + $this->assertSame( $test_task, $handler_received_task, 'Custom handler should receive the task' ); + $this->assertSame( $delay, $handler_received_delay, 'Custom handler should receive the delay' ); + + $this->assertSame( $last_task_id, $regulator->get_last_scheduled_task_id(), 'Task should not be scheduled when custom handler is used' ); + } + + /** + * @test + */ + public function it_should_fire_fail_action_when_throwing_exception_in_custom_dispatch_handler(): void { + $prefix = Config::get_hook_prefix(); + $regulator = Config::get_container()->get( Regulator::class ); + + add_filter( "shepherd_{$prefix}_dispatch_handler", function( $handler, $task, $delay ) { + return function( $task, $delay ) { + throw new Exception( 'Custom dispatch handler failed' ); + }; + }, 10, 3 ); + + $test_task = new Do_Action_Task(); + $delay = 60; + + $this->assertSame( 0, did_action( "shepherd_{$prefix}_task_scheduling_failed" ) ); + $regulator->dispatch( $test_task, $delay ); + $this->assertTrue( 0 < did_action( "shepherd_{$prefix}_task_scheduling_failed" ) ); + } + + /** + * @test + */ + public function it_should_do_default_when_returning_null(): void { + $prefix = Config::get_hook_prefix(); + $regulator = Config::get_container()->get( Regulator::class ); + + $called = false; + + // Add a custom dispatch handler via filter + add_filter( "shepherd_{$prefix}_dispatch_handler", function( $handler, $task, $delay ) use ( &$called ) { + return function( $task, $delay ) use ( &$called ) { + $called = true; + }; + }, 10, 3 ); + + // Add a custom dispatch handler via filter + add_filter( "shepherd_{$prefix}_dispatch_handler", function( $handler, $task, $delay ) { + return null; + }, 10, 3 ); + + $test_task = new Do_Action_Task(); + $delay = 60; + + $last_task_id = $regulator->get_last_scheduled_task_id(); + + $regulator->dispatch( $test_task, $delay ); + + $this->assertFalse( $called, 'Custom dispatch handler should have been called' ); + $this->assertNotSame( $last_task_id, $regulator->get_last_scheduled_task_id(), 'Task should be scheduled when custom handler is not used' ); + } + + /** + * @test + */ + public function it_should_do_default_when_returning_non_callable(): void { + $prefix = Config::get_hook_prefix(); + $regulator = Config::get_container()->get( Regulator::class ); + + + // Add a custom dispatch handler via filter + add_filter( "shepherd_{$prefix}_dispatch_handler", function( $handler, $task, $delay ) { + return 'not a callable'; + }, 10, 3 ); + + $test_task = new Do_Action_Task(); + $delay = 60; + + $regulator->dispatch( $test_task, $delay ); + + $this->assertNotNull( $regulator->get_last_scheduled_task_id(), 'Task should be scheduled when custom handler is not callable' ); + } }