Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2a02f2c
Ensure we always return int while scheduling an action
dpanta94 Sep 1, 2025
f388c98
Fix typo of version string
dpanta94 Sep 1, 2025
9a4424f
Ensure that shepherd tables are up before using shepherd. Shepherd ta…
dpanta94 Sep 1, 2025
44c11ed
Update docs
dpanta94 Sep 1, 2025
c07e4a0
Fix phpcs
dpanta94 Sep 1, 2025
0dfe133
remove not needed param doc comment
dpanta94 Sep 1, 2025
643d6be
Use more suitable hooks
dpanta94 Sep 3, 2025
aa3a4e0
Exclude ActionScheduler_NullAction from pending actions
dpanta94 Sep 3, 2025
1c3a8b0
Updated docs
dpanta94 Sep 3, 2025
f8264c4
Updated docs
dpanta94 Sep 3, 2025
eaf839a
Fixed changelog
dpanta94 Sep 3, 2025
3483158
Patch offset generator
dpanta94 Sep 4, 2025
159e8ae
Updated changelog
dpanta94 Sep 4, 2025
1048bf3
Updated docs
dpanta94 Sep 4, 2025
b69be6b
Fix offset placement in query
dpanta94 Sep 4, 2025
2756df1
make register_regulator an anonymous function
dpanta94 Sep 7, 2025
0c027ab
Merge 2756df1132fd7f2214dc403cd0732a7d9110275d into d93bcedf3a28fee87…
dpanta94 Sep 7, 2025
8309ce7
chore: autopublish 2025-09-07T13:02:08Z
github-actions[bot] Sep 7, 2025
74d9651
Update doc block
dpanta94 Sep 7, 2025
4c434d9
Merge remote-tracking branch 'origin/fix/as-schedule-action-and-table…
dpanta94 Sep 7, 2025
067f9c5
Updated docblocks
dpanta94 Sep 7, 2025
0ca8cab
Fix tests
dpanta94 Sep 7, 2025
b93c555
added test coverage
dpanta94 Sep 8, 2025
96c39a0
Update CHANGELOG.md
dpanta94 Sep 8, 2025
d160818
Update src/Action_Scheduler_Methods.php
dpanta94 Sep 8, 2025
c282fb2
Allow removal of register_regulator action
dpanta94 Sep 8, 2025
46b88ab
Instead of doing_it_wrong, process the task syncronous when tables ar…
dpanta94 Sep 8, 2025
a3d4b1e
Notify library users that they should listen for table failure
dpanta94 Sep 8, 2025
8936385
Test coverage for triggering doing_it_wrong
dpanta94 Sep 8, 2025
e194358
Updated docs
dpanta94 Sep 8, 2025
7ca5617
Test coverage for sync task dispatch when tables unavailable
dpanta94 Sep 8, 2025
f5c7e97
Ensure provider is not registering multiple times with the same conta…
dpanta94 Sep 8, 2025
ffe8e1b
Reduced mocking
dpanta94 Sep 8, 2025
8134d06
added one more assertion
dpanta94 Sep 8, 2025
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

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.7] 2025-09-04
Comment thread
dpanta94 marked this conversation as resolved.
Outdated

* Fix - Ensure the regulator is registered only when the tables are created/updated successfully.
* Fix - When scheduling an action, return 0 if the action ID is not an integer.
* Fix - Fix fetch_all Custom_Table_Query_Methods batch generator by properly incrementing the offset.
* Tweak - Update the schema version of the Tasks table to 0.0.3 to fix a typo in the version string.
* Tweak - Update the get_pending_actions_by_ids method to also exclude null actions.
* Tweak - Use the hook `action_scheduler_init` to determine if Action Scheduler is initialized instead of the `init` hook.

[0.0.7]: https://github.com/stellarwp/shepherd/releases/tag/0.0.7

## [0.0.6] 2025-08-26

* Fix - Update Email task to properly handle multiple email recipients separated by commas.
Expand Down
11 changes: 11 additions & 0 deletions docs/advanced-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ When dispatching a duplicate task:

Prevents accidental duplication and is enabled by default.

## Task Dispatching Requirements (Since 0.0.7)

When dispatching tasks, Shepherd performs several checks:

1. **Table Registration**: Verifies that Shepherd's database tables are registered
2. **Action Scheduler**: Ensures Action Scheduler is initialized

If Action Scheduler is not yet initialized when you dispatch a task, Shepherd will automatically queue it and dispatch once Action Scheduler is ready via the `action_scheduler_init` hook.

If tables are not registered, a `_doing_it_wrong` notice will be triggered. Listen for the `shepherd_{prefix}_tables_registered` action to ensure tables are ready before dispatching tasks.

## Logging

Comprehensive logging tracks the complete task lifecycle.
Expand Down
76 changes: 74 additions & 2 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ The main orchestrator for task scheduling and processing.

#### Methods

##### `dispatch( Task $task, int $delay = 0 ): void`
##### `dispatch( Task $task, int $delay = 0 ): self`

Schedules a task for execution.

- **Parameters:**
- `$task` - The task instance to schedule
- `$delay` - Delay in seconds before execution (default: 0)
- **Returns:** The Regulator instance for method chaining
- **Throws:** and **Catches:** `ShepherdTaskAlreadyExistsException` if duplicate task exists
- **Throws:** and **Catches:** `RuntimeException` if task fails to be scheduled or inserted into the database.
- **Validation:** As of version 0.0.7, checks if Shepherd tables are registered before dispatching. Will trigger `_doing_it_wrong` if tables are not registered.
- **Hook Integration:** As of version 0.0.7, uses `action_scheduler_init` hook instead of `init` to ensure Action Scheduler is ready.
- You can listen for those errors above, by listening to the following actions:
- `shepherd_{prefix}_task_scheduling_failed`
- `shepherd_{prefix}_task_already_exists`
Expand Down Expand Up @@ -106,6 +109,8 @@ Service provider for dependency injection and initialization.

Initializes Shepherd and registers all components.

- **Since version 0.0.7:** The Regulator is only registered after tables are successfully created/updated via the `shepherd_{prefix}_tables_registered` action.

##### `set_container( ContainerInterface $container ): void`

Sets the dependency injection container.
Expand Down Expand Up @@ -133,6 +138,64 @@ Automatically removes task data when Action Scheduler deletes an action.

---

### `Action_Scheduler_Methods`

Wrapper class for Action Scheduler integration (since 0.0.1).

#### Methods

##### `has_scheduled_action( string $hook, array $args = [], string $group = '' ): bool`

Checks if an action is scheduled.

- **Parameters:**
- `$hook` - The hook of the action
- `$args` - The arguments of the action
- `$group` - The group of the action
- **Returns:** Whether the action is scheduled

##### `schedule_single_action( int $timestamp, string $hook, array $args = [], string $group = '', bool $unique = false, int $priority = 10 ): int`

Schedules a single action.

- **Parameters:**
- `$timestamp` - The timestamp when the action should run
- `$hook` - The hook of the action
- `$args` - The arguments of the action
- `$group` - The group of the action
- `$unique` - Whether the action should be unique
- `$priority` - The priority of the action (0-255)
- **Returns:** The action ID, or 0 if scheduling failed (since 0.0.7)

##### `get_action_by_id( int $action_id ): ActionScheduler_Action`

Gets an action by its ID.

- **Parameters:**
- `$action_id` - The action ID
- **Returns:** The action object
- **Throws:** `RuntimeException` if the action is not found

##### `get_actions_by_ids( array $action_ids ): array`

Gets multiple actions by their IDs.

- **Parameters:**
- `$action_ids` - Array of action IDs
- **Returns:** Array of ActionScheduler_Action objects keyed by ID
- **Throws:** `RuntimeException` if any action is not found

##### `get_pending_actions_by_ids( array $action_ids ): array`

Gets pending actions by their IDs, excluding finished and null actions.

- **Parameters:**
- `$action_ids` - Array of action IDs
- **Returns:** Array of pending ActionScheduler_Action objects
- **Since 0.0.7:** Also excludes `ActionScheduler_NullAction` instances

---

### `Email` Task

Built-in task for sending emails asynchronously.
Expand Down Expand Up @@ -416,6 +479,13 @@ Table name: `shepherd_{prefix}_task_logs`

### Actions

- `shepherd_{prefix}_tables_registered` - Fired when Shepherd tables are successfully registered (since 0.0.7)
- No parameters
- Used internally to ensure safe initialization of the Regulator

- `shepherd_{prefix}_tables_error` - Fired when database table creation/update fails (since 0.0.7)
- Parameters: `$exception` (DatabaseQueryException)

- `shepherd_{prefix}_task_scheduling_failed` - Fired when a task fails to be scheduled
- Parameters: `$task`, `$exception`

Expand All @@ -442,4 +512,6 @@ Table name: `shepherd_{prefix}_task_logs`

### Filters

Currently, Shepherd does not provide any filters.
- `shepherd_{prefix}_should_log` - Filter to control whether logging should occur (since 0.0.5)
- Parameters: `$should_log` (bool, default: true)
- Return false to disable logging
13 changes: 13 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,22 @@ The tasks table is created automatically when you call `Provider::register()`.

The logs table is only created if you're using the `DB_Logger`. When using the default `ActionScheduler_DB_Logger`, logs are stored in Action Scheduler's existing `actionscheduler_logs` table.

**Since version 0.0.7:**

- Tables are created/updated before the Regulator is initialized
- The `shepherd_{prefix}_tables_registered` action is fired upon successful table registration
- The `shepherd_{prefix}_tables_error` action is fired if table creation fails
- The Regulator will only be registered after tables are successfully created

## Action Scheduler Configuration

Shepherd uses Action Scheduler for task scheduling. You can configure Action Scheduler settings separately:

**Since version 0.0.7:**

- Shepherd now uses the `action_scheduler_init` hook to ensure Action Scheduler is ready before dispatching tasks
- Tasks dispatched before Action Scheduler initialization are automatically queued and dispatched once it's ready

### Custom Action Scheduler Tables

Action Scheduler uses its own tables. If you need custom table names, configure Action Scheduler before loading Shepherd.
Expand Down Expand Up @@ -161,6 +173,7 @@ $container->get( Provider::class )->register();
2. **Use Consistent Prefixes**: Keep your hook prefix consistent across your application
3. **Container Singleton**: Always register Provider as a singleton
4. **Check Registration**: If you are not sure whether Shepherd is registered, you can check it using `Provider::is_registered()` before accessing Shepherd
5. **Table Initialization** (since 0.0.7): Listen to `shepherd_{prefix}_tables_registered` if you need to perform actions after tables are ready

```php
if ( ! Provider::is_registered() ) {
Expand Down
13 changes: 8 additions & 5 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,12 @@ shepherd()->dispatch( $my_task, 5 * MINUTE_IN_SECONDS ); // Execute after 5 minu

### What Happens Next?

1. Shepherd schedules your task with Action Scheduler
2. WordPress cron picks up the task
3. Your task's `process()` method executes
4. The lifecycle is logged in the database
5. Failed tasks may be retried based on configuration
1. Shepherd validates that its tables are registered (since 0.0.7)
2. Shepherd schedules your task with Action Scheduler
3. WordPress cron picks up the task
4. Your task's `process()` method executes
5. The lifecycle is logged in the database
6. Failed tasks may be retried based on configuration

Check `debug.log` for the message "Shepherd Task: Hello, World! with code 200".

Expand Down Expand Up @@ -176,3 +177,5 @@ If your tasks aren't running:
2. **Verify WP-Cron**: Ensure WordPress cron is running or set up a real cron job
3. **Check Logs**: Look for errors in your WordPress debug log
4. **Database Tables**: Ensure Shepherd's tables were created during registration
5. **Table Registration** (since 0.0.7): Check for `shepherd_{prefix}_tables_error` action if tables fail to create
6. **Initialization Order** (since 0.0.7): Ensure Action Scheduler is loaded before dispatching tasks
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.6
* Version: 0.0.7
* Author: StellarWP
* Author URI: https://stellarwp.com
* License: GPL-2.0-or-later
Expand Down
9 changes: 7 additions & 2 deletions src/Action_Scheduler_Methods.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use ActionScheduler;
use ActionScheduler_Action;
use ActionScheduler_FinishedAction;
use ActionScheduler_NullAction;
use RuntimeException;

/**
Expand Down Expand Up @@ -43,6 +44,7 @@ public static function has_scheduled_action( string $hook, array $args = [], str
* Schedules a single action.
*
* @since 0.0.1
* @since 0.0.7 Updated to return 0 if the action ID is not an integer.
*
* @param int $timestamp The timestamp of the action.
* @param string $hook The hook of the action.
Expand All @@ -54,7 +56,9 @@ public static function has_scheduled_action( string $hook, array $args = [], str
* @return int The action ID.
*/
public static function schedule_single_action( int $timestamp, string $hook, array $args = [], string $group = '', bool $unique = false, int $priority = 10 ): int {
return as_schedule_single_action( $timestamp, $hook, $args, $group, $unique, $priority );
$action_id = as_schedule_single_action( $timestamp, $hook, $args, $group, $unique, $priority );

return is_int( $action_id ) ? $action_id : 0;
}

/**
Expand Down Expand Up @@ -112,6 +116,7 @@ public static function get_actions_by_ids( array $action_ids ): array {
* Gets pending actions by their IDs.
*
* @since 0.0.1
* @since 0.0.7 Updated to filter out null actions.
*
* @param array $action_ids The action IDs.
*
Expand All @@ -120,6 +125,6 @@ public static function get_actions_by_ids( array $action_ids ): array {
public static function get_pending_actions_by_ids( array $action_ids ): array {
$actions = self::get_actions_by_ids( $action_ids );

return array_filter( $actions, fn( ActionScheduler_Action $action ) => ! $action instanceof ActionScheduler_FinishedAction );
return array_filter( $actions, fn( ActionScheduler_Action $action ) => ! $action instanceof ActionScheduler_FinishedAction && ! $action instanceof ActionScheduler_NullAction );
Comment thread
dpanta94 marked this conversation as resolved.
Outdated
}
}
14 changes: 12 additions & 2 deletions src/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Provider extends Provider_Abstract {
* Registers Shepherd's specific providers and starts core functionality
*
* @since 0.0.1
* @since 0.0.7 Updated to register the regulator after the tables are registered successfully.
*
* @return void The method does not return any value.
*/
Expand All @@ -78,8 +79,17 @@ public function register(): void {
$this->container->singleton( Logger::class, Config::get_logger() );
$this->container->singleton( Tables_Provider::class );
$this->container->singleton( Regulator::class );

$prefix = Config::get_hook_prefix();

add_action(
"shepherd_{$prefix}_tables_registered",
function () {
Comment thread
dpanta94 marked this conversation as resolved.
Outdated
$this->container->get( Regulator::class )->register();
}
);

$this->container->get( Tables_Provider::class )->register();
$this->container->get( Regulator::class )->register();

add_action( 'action_scheduler_deleted_action', [ $this, 'delete_tasks_on_action_deletion' ] );

Expand All @@ -90,7 +100,7 @@ public function register(): void {
* Requires Action Scheduler.
*
* @since 0.0.1
* @since 0.0.2
* @since 0.0.2 Look into multiple places for the action scheduler main file.
*
* @return void
*
Expand Down
16 changes: 13 additions & 3 deletions src/Regulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public function __construct( Container $container ) {
* Registers the regulator.
*
* @since 0.0.1
* @since 0.0.7 Updated to use the `wp_loaded` hook instead of the `init` hook to schedule the cleanup task.
*/
public function register(): void {
add_action( $this->process_task_hook, [ $this, 'process_task' ] );
Expand All @@ -97,7 +98,7 @@ public function register(): void {
add_action( 'action_scheduler_execution_ignored', [ $this, 'untrack_action' ], 1, 0 );
add_action( 'action_scheduler_failed_execution', [ $this, 'untrack_action' ], 1, 0 );
add_action( 'action_scheduler_after_process_queue', [ $this, 'handle_reschedule_of_failed_task' ], 1, 0 );
add_action( 'init', [ $this, 'schedule_cleanup_task' ], 20, 0 );
add_action( 'wp_loaded', [ $this, 'schedule_cleanup_task' ], 20, 0 );
}

/**
Expand Down Expand Up @@ -139,20 +140,29 @@ public function untrack_action(): void {
* Dispatches a task to be processed later.
*
* @since 0.0.1
* @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.
*
* @param Task $task The task to dispatch.
* @param int $delay The delay in seconds before the task is processed.
*
* @return self The regulator instance.
*/
public function dispatch( Task $task, int $delay = 0 ): self {
if ( did_action( 'init' ) || doing_action( 'init' ) ) {
$prefix = Config::get_hook_prefix();

if ( ! did_action( "shepherd_{$prefix}_tables_registered" ) ) {
_doing_it_wrong( __METHOD__, esc_html__( 'Shepherd tables are not yet registered.', 'stellarwp-shepherd' ), '0.0.7' );
Comment thread
dpanta94 marked this conversation as resolved.
Outdated
return $this;
}

if ( did_action( 'action_scheduler_init' ) || doing_action( 'action_scheduler_init' ) ) {
$this->dispatch_callback( $task, $delay );
return $this;
}

add_action(
'init',
'action_scheduler_init',
function () use ( $task, $delay ): void {
$this->dispatch_callback( $task, $delay );
},
Expand Down
29 changes: 26 additions & 3 deletions src/Tables/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use StellarWP\Schema\Register;
use StellarWP\Shepherd\Contracts\Logger;
use StellarWP\Shepherd\Loggers\DB_Logger;
use StellarWP\DB\Database\Exceptions\DatabaseQueryException;
use StellarWP\Shepherd\Config;

/**
* Shepherd Tables Service Provider
Expand All @@ -38,6 +40,7 @@ class Provider extends Provider_Abstract {
* Registers the service provider bindings.
*
* @since 0.0.1
* @since 0.0.7 Updated to catch DatabaseQueryException.
*
* @return void The method does not return any value.
*/
Expand All @@ -46,10 +49,30 @@ public function register(): void {
$this->container->singleton( Utility\Safe_Dynamic_Prefix::class );
$this->container->get( Utility\Safe_Dynamic_Prefix::class )->calculate_longest_table_name( $this->tables );

Register::table( Tasks::class );
$prefix = Config::get_hook_prefix();

if ( $this->container->get( Logger::class ) instanceof DB_Logger ) {
Register::table( Task_Logs::class );
try {
Register::table( Tasks::class );

if ( $this->container->get( Logger::class ) instanceof DB_Logger ) {
Register::table( Task_Logs::class );
}

/**
* Fires an action when the Shepherd tables are registered.
*
* @since 0.0.7
*/
do_action( "shepherd_{$prefix}_tables_registered" );
} catch ( DatabaseQueryException $e ) {
/**
* Fires an action when an error or exception happens in the context of Shepherd tables implementation AND the server runs PHP 7.0+.
*
* @since 0.0.7
*
* @param DatabaseQueryException $e The thrown error.
*/
do_action( "shepherd_{$prefix}_tables_error", $e );
}
}
}
Loading
Loading