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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 28 additions & 3 deletions docs/advanced-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion shepherd.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions src/Regulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
108 changes: 108 additions & 0 deletions tests/wpunit/Regulator_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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' );
}
}
Loading