diff --git a/config/queue.php b/config/queue.php index 384c6d9a..43c3aa25 100644 --- a/config/queue.php +++ b/config/queue.php @@ -40,6 +40,16 @@ */ 'max_concurrent_batches' => environment( 'QUEUE_MAX_CONCURRENT_BATCHES', 1 ), + /* + |-------------------------------------------------------------------------- + | Delete failed or processed queue items after a set time + |-------------------------------------------------------------------------- + | + | Delete failed or processed queue items after a set time in seconds. + | + */ + 'delete_after' => environment( 'QUEUE_DELETE_AFTER', 60 * 60 * 24 * 7 ), + /* |-------------------------------------------------------------------------- | Enable the Queue Admin Interface diff --git a/src/mantle/application/class-app-service-provider.php b/src/mantle/application/class-app-service-provider.php index 2d409fd6..0831c182 100644 --- a/src/mantle/application/class-app-service-provider.php +++ b/src/mantle/application/class-app-service-provider.php @@ -42,7 +42,7 @@ public function __construct( Application $app ) { */ protected function boot_scheduler() { $this->app->singleton( - Schedule::class, + 'scheduler', fn ( $app ) => tap( new Schedule( $app ), fn ( Schedule $schedule ) => $this->schedule( $schedule ), diff --git a/src/mantle/application/class-application.php b/src/mantle/application/class-application.php index ee5ce37f..abba8cb3 100644 --- a/src/mantle/application/class-application.php +++ b/src/mantle/application/class-application.php @@ -414,6 +414,7 @@ protected function register_core_aliases() { 'request' => [ \Mantle\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class ], 'router' => [ \Mantle\Http\Routing\Router::class, \Mantle\Contracts\Http\Routing\Router::class ], 'router.entity' => [ \Mantle\Http\Routing\Entity_Router::class, \Mantle\Contracts\Http\Routing\Entity_Router::class ], + 'scheduler' => [ \Mantle\Scheduling\Schedule::class ], 'url' => [ \Mantle\Http\Routing\Url_Generator::class, \Mantle\Contracts\Http\Routing\Url_Generator::class ], 'view.loader' => [ \Mantle\Http\View\View_Finder::class, \Mantle\Contracts\Http\View\View_Finder::class ], 'view' => [ \Mantle\Http\View\Factory::class, \Mantle\Contracts\Http\View\Factory::class ], diff --git a/src/mantle/database/model/class-model.php b/src/mantle/database/model/class-model.php index 470b714c..8c412680 100644 --- a/src/mantle/database/model/class-model.php +++ b/src/mantle/database/model/class-model.php @@ -175,12 +175,14 @@ public function refresh() { } $instance = static::find( $this->get( 'id' ) ); + if ( ! $instance ) { return null; } $this->exists = true; $this->set_raw_attributes( $instance->get_raw_attributes() ); + return $this; } diff --git a/src/mantle/database/query/class-builder.php b/src/mantle/database/query/class-builder.php index 969ad838..991e847d 100644 --- a/src/mantle/database/query/class-builder.php +++ b/src/mantle/database/query/class-builder.php @@ -847,8 +847,8 @@ public function chunk_by_id( int $count, callable $callback, string $attribute = /** * Execute a callback over each item while chunking. * - * @param callable(\Mantle\Support\Collection): mixed $callback Callback to run on each chunk. - * @param int $count Number of items to chunk by. + * @param callable(TModel): mixed $callback Callback to run on each chunk. + * @param int $count Number of items to chunk by. * @return boolean */ public function each( callable $callback, int $count = 100 ) { @@ -866,9 +866,9 @@ public function each( callable $callback, int $count = 100 ) { /** * Execute a callback over each item while chunking by ID. * - * @param callable(\Mantle\Support\Collection): mixed $callback Callback to run on each chunk. - * @param int $count Number of items to chunk by. - * @param string $attribute Attribute to chunk by. + * @param callable(TModel): mixed $callback Callback to run on each chunk. + * @param int $count Number of items to chunk by. + * @param string $attribute Attribute to chunk by. * @return boolean */ public function each_by_id( callable $callback, int $count = 100, string $attribute = 'id' ) { diff --git a/src/mantle/database/query/class-post-query-builder.php b/src/mantle/database/query/class-post-query-builder.php index 50444e3b..7f683084 100644 --- a/src/mantle/database/query/class-post-query-builder.php +++ b/src/mantle/database/query/class-post-query-builder.php @@ -26,7 +26,7 @@ * @method \Mantle\Database\Query\Post_Query_Builder whereId( int $id ) * @method \Mantle\Database\Query\Post_Query_Builder whereName( string $name ) * @method \Mantle\Database\Query\Post_Query_Builder whereSlug( string $slug ) - * @method \Mantle\Database\Query\Post_Query_Builder whereStatus( string $status ) + * @method \Mantle\Database\Query\Post_Query_Builder whereStatus( string[]|string $status ) * @method \Mantle\Database\Query\Post_Query_Builder whereTitle( string $title ) * @method \Mantle\Database\Query\Post_Query_Builder whereType( string $type ) */ diff --git a/src/mantle/queue/class-queue-service-provider.php b/src/mantle/queue/class-queue-service-provider.php index da1fd4c1..289c2ae3 100644 --- a/src/mantle/queue/class-queue-service-provider.php +++ b/src/mantle/queue/class-queue-service-provider.php @@ -8,6 +8,7 @@ namespace Mantle\Queue; use Mantle\Contracts\Queue\Queue_Manager as Queue_Manager_Contract; +use Mantle\Queue\Console\Cleanup_Jobs_Command; use Mantle\Queue\Console\Run_Command; use Mantle\Queue\Dispatcher; use Mantle\Queue\Queue_Manager; @@ -32,7 +33,7 @@ public function register() { $this->app->singleton_if( 'queue', fn ( $app ) => tap( - // Register the Queue Manager with the supported providers when invoked. + // Register the Queue Manager with the supported providers when resolved. new Queue_Manager( $app ), fn ( Queue_Manager $manager ) => $this->register_providers( $manager ), ), @@ -49,6 +50,7 @@ public function register() { ); // Register queue console commands. + $this->add_command( Cleanup_Jobs_Command::class ); $this->add_command( Run_Command::class ); // Register the queue service providers. diff --git a/src/mantle/queue/class-queue-worker-job.php b/src/mantle/queue/class-queue-worker-job.php index ed79bf2c..e3dbf9b7 100644 --- a/src/mantle/queue/class-queue-worker-job.php +++ b/src/mantle/queue/class-queue-worker-job.php @@ -49,6 +49,13 @@ abstract public function get_job(): mixed; */ abstract public function failed( Throwable $e ): void; + /** + * Handle a completed job. + * + * @return void + */ + abstract public function completed(): void; + /** * Retry a job with a specified delay. * diff --git a/src/mantle/queue/class-worker.php b/src/mantle/queue/class-worker.php index cf9dd3ab..8cc45dcb 100644 --- a/src/mantle/queue/class-worker.php +++ b/src/mantle/queue/class-worker.php @@ -58,9 +58,8 @@ function( Queue_Worker_Job $job ) use ( $provider ) { $this->events->dispatch( new Job_Failed( $provider, $job, $e ) ); } finally { - // TODO: Don't delete after completion. if ( ! $job->has_failed() ) { - $job->delete(); + $job->completed(); } elseif ( $job->can_retry() ) { $job->retry( $job->get_retry_backoff() ); } diff --git a/src/mantle/queue/console/class-cleanup-jobs-command.php b/src/mantle/queue/console/class-cleanup-jobs-command.php new file mode 100644 index 00000000..4ac8ec61 --- /dev/null +++ b/src/mantle/queue/console/class-cleanup-jobs-command.php @@ -0,0 +1,49 @@ +whereStatus( [ Post_Status::FAILED->value, Post_Status::COMPLETED->value ] ) + ->olderThan( now()->subSeconds( (int) $this->container['config']->get( 'queue.delete_after', 60 ) ) ) + ->take( -1 ) + ->each_by_id( + fn ( Queue_Record $record ) => $record->delete( true ), + 100, + ); + } +} diff --git a/src/mantle/queue/providers/wordpress/admin/class-queue-job-admin-page.php b/src/mantle/queue/providers/wordpress/admin/class-queue-job-admin-page.php index 58205130..6a798522 100644 --- a/src/mantle/queue/providers/wordpress/admin/class-queue-job-admin-page.php +++ b/src/mantle/queue/providers/wordpress/admin/class-queue-job-admin-page.php @@ -8,7 +8,7 @@ namespace Mantle\Queue\Providers\WordPress\Admin; use Mantle\Queue\Providers\WordPress\Post_Status; -use Mantle\Queue\Providers\WordPress\Queue_Job; +use Mantle\Queue\Providers\WordPress\Queue_Record; use Mantle\Queue\Providers\WordPress\Queue_Worker_Job; /** @@ -37,7 +37,7 @@ public function render(): void { * @param int $job_id The job ID. */ protected function render_single_job( int $job_id ): void { - $job = Queue_Job::find( $job_id ); + $job = Queue_Record::find( $job_id ); if ( empty( $job ) ) { wp_die( esc_html__( 'Invalid job ID.', 'mantle' ) ); @@ -60,7 +60,7 @@ protected function render_action( int $job_id ): void { } $action = sanitize_text_field( wp_unslash( $_GET['action'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $job = Queue_Job::find( $job_id ); + $job = Queue_Record::find( $job_id ); $message = ''; if ( empty( $job ) ) { diff --git a/src/mantle/queue/providers/wordpress/admin/class-queue-jobs-table.php b/src/mantle/queue/providers/wordpress/admin/class-queue-jobs-table.php index e6f563bb..ed2cd5ba 100644 --- a/src/mantle/queue/providers/wordpress/admin/class-queue-jobs-table.php +++ b/src/mantle/queue/providers/wordpress/admin/class-queue-jobs-table.php @@ -11,7 +11,7 @@ use Mantle\Database\Query\Post_Query_Builder; use Mantle\Queue\Providers\WordPress\Post_Status; use Mantle\Queue\Providers\WordPress\Provider; -use Mantle\Queue\Providers\WordPress\Queue_Job; +use Mantle\Queue\Providers\WordPress\Queue_Record; use Mantle\Queue\Providers\WordPress\Queue_Worker_Job; use WP_List_Table; @@ -126,7 +126,7 @@ public function prepare_items() { $active_queue_filter = sanitize_text_field( wp_unslash( $_GET['queue'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = (int) ( $_GET['paged'] ?? 1 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $query = Queue_Job::query() + $query = Queue_Record::query() ->orderBy( 'date', 'asc' ) // Allow the query to be filtered by status. ->when( @@ -146,7 +146,7 @@ public function prepare_items() { // TODO: Refactor with found_posts later. $this->items = $query->get()->map( - function ( Queue_Job $model ) { + function ( Queue_Record $model ) { $worker = new Queue_Worker_Job( $model ); $job = $worker->get_job(); diff --git a/src/mantle/queue/providers/wordpress/admin/template/single.php b/src/mantle/queue/providers/wordpress/admin/template/single.php index e13e34bc..f3b00eee 100644 --- a/src/mantle/queue/providers/wordpress/admin/template/single.php +++ b/src/mantle/queue/providers/wordpress/admin/template/single.php @@ -5,7 +5,7 @@ * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound * * @package Mantle - * @var \Mantle\Queue\Providers\WordPress\Queue_Job $job The queue job. + * @var \Mantle\Queue\Providers\WordPress\Queue_Record $job The queue job. */ use Carbon\Carbon; diff --git a/src/mantle/queue/providers/wordpress/class-provider.php b/src/mantle/queue/providers/wordpress/class-provider.php index 07d63ae3..1fa4e6d8 100644 --- a/src/mantle/queue/providers/wordpress/class-provider.php +++ b/src/mantle/queue/providers/wordpress/class-provider.php @@ -21,8 +21,6 @@ /** * WordPress Cron Queue Provider - * - * @todo Add support for one off cron events that should have their own cron scheduled. */ class Provider implements Provider_Contract { /** @@ -123,7 +121,7 @@ public function push( mixed $job ): bool { ->lower() ->slug(); - $object = new Queue_Job( + $object = new Queue_Record( [ 'post_name' => "mantle_queue_{$job_name}_" . time(), 'post_status' => Post_Status::PENDING->value, @@ -181,13 +179,13 @@ public function pop( string $queue = null, int $count = 1 ): Collection { ->take( $count * $max_concurrent_batches ) ->get() // Filter out any jobs that are locked. - ->filter( fn ( Queue_Job $job ) => ! $job->is_locked() ) + ->filter( fn ( Queue_Record $record ) => ! $record->is_locked() ) ->map( - fn ( Queue_Job $model ) => tap( - new Queue_Worker_Job( $model ), + fn ( Queue_Record $record ) => tap( + new Queue_Worker_Job( $record ), // Lock the job until the configured timeout or 10 minutes. - fn ( Queue_Worker_Job $queue_job ) => $model->set_lock_until( - $queue_job->get_job()->timeout ?? 600 + fn ( Queue_Worker_Job $job ) => $record->set_lock_until( + $job->get_job()->timeout ?? 600 ), ), ) @@ -209,10 +207,10 @@ public function pending_count( string $queue = null ): int { * Construct the query builder for the queue. * * @param string|null $queue Queue name, optional. - * @return Post_Query_Builder + * @return Post_Query_Builder */ protected function query( string $queue = null ): Post_Query_Builder { - return Queue_Job::where( 'post_status', Post_Status::PENDING->value ) + return Queue_Record::where( 'post_status', Post_Status::PENDING->value ) ->whereTerm( static::get_queue_term_id( $queue ), static::OBJECT_NAME ) ->orderBy( 'post_date', 'asc' ); } @@ -225,7 +223,7 @@ protected function query( string $queue = null ): Post_Query_Builder { * @return bool */ public function in_queue( mixed $job, string $queue = null ): bool { - return Queue_Job::where( 'post_status', Post_Status::PENDING->value ) + return Queue_Record::where( 'post_status', Post_Status::PENDING->value ) ->whereDate( now()->toDateTimeString(), '>=' ) ->whereTerm( static::get_queue_term_id( $queue ), static::OBJECT_NAME ) ->whereMeta( Meta_Key::JOB->value, maybe_serialize( $job ) ) diff --git a/src/mantle/queue/providers/wordpress/class-queue-job.php b/src/mantle/queue/providers/wordpress/class-queue-record.php similarity index 90% rename from src/mantle/queue/providers/wordpress/class-queue-job.php rename to src/mantle/queue/providers/wordpress/class-queue-record.php index c1b371fa..6aab0e09 100644 --- a/src/mantle/queue/providers/wordpress/class-queue-job.php +++ b/src/mantle/queue/providers/wordpress/class-queue-record.php @@ -1,6 +1,6 @@ model->save( + [ + 'post_status' => Post_Status::COMPLETED->value, + ] + ); + + $job = $this->get_job(); + + if ( method_exists( $job, 'completed' ) ) { + $job->completed(); + } + } + /** * Delete the job from the queue. */ diff --git a/src/mantle/queue/providers/wordpress/class-service-provider.php b/src/mantle/queue/providers/wordpress/class-service-provider.php index e981b5ae..4fb9d07e 100644 --- a/src/mantle/queue/providers/wordpress/class-service-provider.php +++ b/src/mantle/queue/providers/wordpress/class-service-provider.php @@ -9,9 +9,11 @@ namespace Mantle\Queue\Providers\WordPress; +use Mantle\Queue\Console\Cleanup_Jobs_Command; use Mantle\Queue\Events; use Mantle\Support\Attributes\Action; use Mantle\Support\Service_Provider as Base_Service_Provider; +use Mantle\Scheduling\Schedule; /** * WordPress Queue Service Provider Scheduler @@ -29,11 +31,26 @@ public function register(): void { /** * Register the WordPress queue provider's post type and taxonomies. + * + * Registers the cleanup command with the application task scheduler to run + * daily (by default) to remove old queue jobs from the database. */ public function boot() { if ( did_action( 'init' ) ) { $this->register_data_types(); } + + $this->app->resolving( + 'scheduler', + fn ( Schedule $scheduler ) => $scheduler->command( Cleanup_Jobs_Command::class )->cron( + /** + * Filter the schedule for the queue cleanup job. + * + * @param string $schedule Schedule cron expression. Defaults to daily at midnight. + */ + (string) apply_filters( 'mantle_queue_cleanup_schedule', '0 0 * * *' ), + ) + ); } /** diff --git a/src/mantle/queue/providers/wordpress/enum-post-status.php b/src/mantle/queue/providers/wordpress/enum-post-status.php index 1384b0f9..3b8537a4 100644 --- a/src/mantle/queue/providers/wordpress/enum-post-status.php +++ b/src/mantle/queue/providers/wordpress/enum-post-status.php @@ -11,7 +11,8 @@ * Post Statuses for jobs on the queue */ enum Post_Status: string { - case PENDING = 'queue_pending'; - case RUNNING = 'queue_running'; - case FAILED = 'queue_failed'; + case PENDING = 'queue_pending'; + case RUNNING = 'queue_running'; + case FAILED = 'queue_failed'; + case COMPLETED = 'queue_completed'; } diff --git a/src/mantle/scheduling/class-event.php b/src/mantle/scheduling/class-event.php index 8bbfffaa..3091e226 100644 --- a/src/mantle/scheduling/class-event.php +++ b/src/mantle/scheduling/class-event.php @@ -21,7 +21,7 @@ use Throwable; /** - * Schedulable Event + * Schedule-able Event */ class Event { use Macroable, Manages_Frequencies; @@ -31,56 +31,49 @@ class Event { * * @var string */ - public $expression = '* * * * *'; + public string $expression = '* * * * *'; /** * The list of environments the command should run under. * - * @var array + * @var string[] */ - public $environments = []; - - /** - * Indicates if the command should not overlap itself. - * - * @var bool - */ - public $without_overlapping = false; + public array $environments = []; /** * The array of filter callbacks. * - * @var array + * @var callable[] */ - protected $filters = []; + protected array $filters = []; /** * The array of reject callbacks. * - * @var array + * @var callable[] */ - protected $rejects = []; + protected array $rejects = []; /** * The array of callbacks to be run before the event is started. * - * @var array + * @var callable[] */ - protected $before_callbacks = []; + protected array $before_callbacks = []; /** * The array of callbacks to be run after the event is finished. * - * @var array + * @var callable[] */ - protected $after_callbacks = []; + protected array $after_callbacks = []; /** * The human readable description of the event. * * @var string */ - public $description; + public string $description; /** * The exit status code of the command. @@ -88,14 +81,14 @@ class Event { * * @var int|null */ - public $exit_code; + public ?int $exit_code; /** * Exception thrown for the command. * * @var \Throwable */ - public $exception; + public \Throwable $exception; /** * Create a new event instance. diff --git a/src/mantle/scheduling/class-schedule.php b/src/mantle/scheduling/class-schedule.php index 88897fb2..e7eca077 100644 --- a/src/mantle/scheduling/class-schedule.php +++ b/src/mantle/scheduling/class-schedule.php @@ -94,7 +94,7 @@ public static function schedule_cron_event() { /** * Run the scheduled events that are due to run. */ - public function run_due_events() { + public function run_due_events(): void { $this ->due_events( $this->container ) ->each( fn ( Event $event ) => $event->run( $this->container ) ); @@ -103,9 +103,9 @@ public function run_due_events() { /** * Add a new command event. * - * @param string $command Command class to run. - * @param array $arguments Arguments for the command. - * @param array $assoc_args Assoc. arguments for the command. + * @param class-string<\Mantle\Console\Command> $command Command class to run. + * @param array $arguments Arguments for the command. + * @param array $assoc_args Assoc. arguments for the command. * @return Event * * @throws RuntimeException Thrown on missing command. @@ -130,8 +130,8 @@ public function command( string $command, array $arguments = [], array $assoc_ar /** * Add a new job event. * - * @param string $job Job class to run. - * @param array $arguments Arguments for the command. + * @param class-string $job Job class to run. + * @param array $arguments Arguments for the command. * @return Event * * @throws RuntimeException Thrown on missing command. @@ -156,11 +156,11 @@ public function job( string $job, array $arguments = [] ): Event { /** * Add a callback event. * - * @param string $callback Callback to run. - * @param array $arguments Arguments for the callback. + * @param callable $callback Callback to run. + * @param array $arguments Arguments for the callback. * @return Event */ - public function call( $callback, array $arguments = [] ): Event { + public function call( callable $callback, array $arguments = [] ): Event { $event = new Event( $callback, $arguments, $this->get_timezone() ); $this->events[] = $event; @@ -185,7 +185,7 @@ public function due_events( Application $app ): Collection { * * @return Event[] */ - public function events() { + public function events(): array { return $this->events; } } diff --git a/src/mantle/testing/concerns/trait-assertions.php b/src/mantle/testing/concerns/trait-assertions.php index 3c4bec91..2d9a8f8c 100644 --- a/src/mantle/testing/concerns/trait-assertions.php +++ b/src/mantle/testing/concerns/trait-assertions.php @@ -9,7 +9,9 @@ namespace Mantle\Testing\Concerns; +use BackedEnum; use Mantle\Contracts\Database\Core_Object; +use Mantle\Contracts\Support\Arrayable; use Mantle\Database\Model\Post; use Mantle\Database\Model\Term; use Mantle\Database\Model\User; @@ -17,6 +19,7 @@ use WP_Post; use WP_Term; +use function Mantle\Support\Helpers\collect; use function Mantle\Support\Helpers\get_term_object; /** @@ -270,12 +273,12 @@ public static function assertQueriedObjectNull(): void { * @param array $arguments Arguments to query against. */ public function assertPostExists( array $arguments ): void { - $arguments = array_merge( + $arguments = $this->serialize_arguments( + $arguments, [ 'fields' => 'ids', 'posts_per_page' => 1, ], - $arguments ); PHPUnit::assertNotEmpty( @@ -290,12 +293,12 @@ public function assertPostExists( array $arguments ): void { * @param array $arguments Arguments to query against. */ public function assertPostDoesNotExists( array $arguments ): void { - $arguments = array_merge( + $arguments = $this->serialize_arguments( + $arguments, [ 'fields' => 'ids', 'posts_per_page' => 1, ], - $arguments ); PHPUnit::assertEmpty( @@ -310,13 +313,13 @@ public function assertPostDoesNotExists( array $arguments ): void { * @param array $arguments Arguments to query against. */ public function assertTermExists( array $arguments ): void { - $arguments = array_merge( + $arguments = $this->serialize_arguments( + $arguments, [ 'fields' => 'ids', 'count' => 1, 'hide_empty' => false, ], - $arguments ); PHPUnit::assertNotEmpty( @@ -331,13 +334,13 @@ public function assertTermExists( array $arguments ): void { * @param array $arguments Arguments to query against. */ public function assertTermDoesNotExists( array $arguments ): void { - $arguments = array_merge( + $arguments = $this->serialize_arguments( + $arguments, [ 'fields' => 'ids', 'count' => 1, 'hide_empty' => false, ], - $arguments ); PHPUnit::assertEmpty( @@ -352,12 +355,12 @@ public function assertTermDoesNotExists( array $arguments ): void { * @param array $arguments Arguments to query against. */ public function assertUserExists( array $arguments ) { - $arguments = array_merge( + $arguments = $this->serialize_arguments( + $arguments, [ 'fields' => 'ids', 'count' => 1, ], - $arguments ); PHPUnit::assertNotEmpty( @@ -372,12 +375,12 @@ public function assertUserExists( array $arguments ) { * @param array $arguments Arguments to query against. */ public function assertUserDoesNotExists( array $arguments ): void { - $arguments = array_merge( + $arguments = $this->serialize_arguments( + $arguments, [ 'fields' => 'ids', 'count' => 1, ], - $arguments ); PHPUnit::assertEmpty( @@ -458,4 +461,46 @@ public function assertPostNotHasTerm( Post|WP_Post|int $post, Term|WP_Term|int $ public function assertPostsDoesNotHaveTerm( Post|WP_Post|int $post, Term|WP_Term|int $term ): void { $this->assertPostNotHasTerm( $post, $term ); } + + /** + * Serialize arguments for use in assertions. + * + * Convert string-backed enums to an array of all possible values from an enumeration. + * + * @param array $arguments Arguments to serialize. + * @param array $defaults Default values. + * @return array + */ + protected function serialize_arguments( array $arguments, array $defaults = [] ): array { + $arguments = array_merge( $defaults, $arguments ); + + foreach ( $arguments as $key => $value ) { + if ( $value instanceof Arrayable ) { + $arguments[ $key ] = $value->to_array(); + } + + // Check for PHP 8.1+ support. + if ( interface_exists( BackedEnum::class ) ) { + // Convert an enum to an array of all possible values. + if ( is_string( $value ) && enum_exists( $value ) && is_subclass_of( $value, BackedEnum::class ) ) { + $arguments[ $key ] = collect( $value::cases() )->pluck( 'value' )->all(); + } + + // Convert an enum object to its value. + if ( is_object( $value ) && $value instanceof BackedEnum ) { + $arguments[ $key ] = $value->value; + } + + // Convert an array of enum objects to an array of their values. + if ( is_array( $value ) ) { + $arguments[ $key ] = array_map( + fn ( $item ) => is_object( $item ) && $item instanceof BackedEnum ? $item->value : $item, + $value, + ); + } + } + } + + return $arguments; + } } diff --git a/src/mantle/testing/concerns/trait-create-application.php b/src/mantle/testing/concerns/trait-create-application.php index 56785637..67efca47 100644 --- a/src/mantle/testing/concerns/trait-create-application.php +++ b/src/mantle/testing/concerns/trait-create-application.php @@ -73,6 +73,7 @@ protected function get_application_config(): array { \Mantle\Filesystem\Filesystem_Service_Provider::class, \Mantle\Database\Pagination\Paginator_Service_Provider::class, \Mantle\Cache\Cache_Service_Provider::class, + \Mantle\Application\App_Service_Provider::class, ], ], 'queue' => [ diff --git a/tests/Queue/WorkerTest.php b/tests/Queue/WorkerTest.php index 9d3ae6da..afbfa70a 100644 --- a/tests/Queue/WorkerTest.php +++ b/tests/Queue/WorkerTest.php @@ -101,7 +101,7 @@ protected function get_mock_job( $id, $should_run = true ) { if ( $should_run ) { $mock_job->shouldReceive( 'fire' )->once()->andReturn( true ); - $mock_job->shouldReceive( 'delete' )->once(); + $mock_job->shouldReceive( 'completed' )->once(); $mock_job->shouldReceive( 'has_failed' )->once()->andReturn( false ); $mock_job->shouldNotReceive( 'failed' ); } diff --git a/tests/Queue/providers/WordPressCronQueueTest.php b/tests/Queue/providers/WordPressCronQueueTest.php index 885cb6c7..d788056c 100644 --- a/tests/Queue/providers/WordPressCronQueueTest.php +++ b/tests/Queue/providers/WordPressCronQueueTest.php @@ -8,6 +8,7 @@ use Mantle\Queue\Providers\WordPress\Meta_Key; use Mantle\Queue\Providers\WordPress\Post_Status; use Mantle\Queue\Providers\WordPress\Provider; +use Mantle\Queue\Providers\WordPress\Queue_Record; use Mantle\Queue\Providers\WordPress\Scheduler; use Mantle\Queue\Queueable; use Mantle\Scheduling\Schedule; @@ -52,7 +53,7 @@ public function test_job_dispatch() { // Assert that the underlying queue post exists. $this->assertPostExists( [ 'post_type' => Provider::OBJECT_NAME, - 'post_status' => Post_Status::PENDING->value, + 'post_status' => Post_Status::PENDING, ] ); // Force the cron to be dispatched which will execute the queued job. @@ -61,9 +62,9 @@ public function test_job_dispatch() { $this->assertTrue( $_SERVER['__example_job'] ); // Ensure that the queued job post was deleted. - $this->assertPostDoesNotExists( [ + $this->assertPostExists( [ 'post_type' => Provider::OBJECT_NAME, - 'post_status' => 'any', + 'post_status' => Post_Status::COMPLETED, ] ); } @@ -79,7 +80,7 @@ public function test_job_dispatch_now() { $this->assertPostDoesNotExists( [ 'post_type' => Provider::OBJECT_NAME, - 'post_status' => 'any', + 'post_status' => Post_Status::class, ] ); } @@ -102,7 +103,7 @@ public function test_job_failure() { // Ensure that the post does not exist. $this->assertPostExists( [ 'post_type' => Provider::OBJECT_NAME, - 'post_status' => Post_Status::FAILED->value, + 'post_status' => Post_Status::FAILED, ] ); } @@ -132,8 +133,8 @@ public function test_job_dispatch_anonymous() { 'meta_value' => 'SerializableClosure', 'meta_compare' => 'LIKE', 'post_status' => [ - Post_Status::PENDING->value, - Post_Status::FAILED->value, + Post_Status::PENDING, + Post_Status::FAILED, ], ] ); } @@ -153,7 +154,7 @@ public function test_job_dispatch_anonymous_failure() { // Assert the serialize queue post exists. $this->assertPostExists( [ - 'post_status' => Post_Status::PENDING->value, + 'post_status' => Post_Status::PENDING, 'post_type' => Provider::OBJECT_NAME, 'meta_key' => '_mantle_queue', 'meta_value' => 'SerializableClosure', @@ -166,7 +167,7 @@ public function test_job_dispatch_anonymous_failure() { $this->assertTrue( $_SERVER['__failed_run'] ); $this->assertPostExists( [ - 'post_status' => Post_Status::FAILED->value, + 'post_status' => Post_Status::FAILED, 'post_type' => Provider::OBJECT_NAME, 'meta_key' => '_mantle_queue', 'meta_value' => 'SerializableClosure', @@ -183,7 +184,7 @@ public function test_dispatch_job_delay() { $this->assertPostExists( [ 'post_date' => $start->toDateTimeString(), - 'post_status' => Post_Status::PENDING->value, + 'post_status' => Post_Status::PENDING, 'post_type' => Provider::OBJECT_NAME, 'meta_key' => '_mantle_queue', ] ); @@ -194,7 +195,7 @@ public function test_dispatch_job_delay() { $this->assertPostExists( [ 'post_date' => $start->toDateTimeString(), - 'post_status' => Post_Status::PENDING->value, + 'post_status' => Post_Status::PENDING, 'post_type' => Provider::OBJECT_NAME, 'meta_key' => '_mantle_queue', ] ); @@ -237,7 +238,7 @@ public function test_failed_job() { $this->assertPostExists( [ 'post_type' => Provider::OBJECT_NAME, - 'post_status' => Post_Status::FAILED->value, + 'post_status' => Post_Status::FAILED, ] ); $this->dispatch_queue(); @@ -293,6 +294,33 @@ public function test_schedule_next_run_after_complete() { Scheduler::schedule_on_shutdown(); $this->assertNotInCronQueue( Scheduler::EVENT, null ); } + + public function test_cleanup_completed_jobs() { + $this->app['config']->set( 'queue.delete_after', 60 * 60 * 24 ); + + $record = Queue_Record::create( [ + 'post_status' => Post_Status::COMPLETED->value, + 'post_date' => now()->subMonth()->format( 'Y-m-d H:i:s' ), + ] ); + + // Create a valid queue job that shouldn't be deleted. + Example_Job::dispatch(); + + + // Perform the scheduled cleanup manually. + $this->command( 'mantle queue:cleanup' ); + + // Ensure that the expired queue job was deleted. + $this->assertEmpty( get_post( $record->ID ) ); + + // Assert that the queue post still exists. + $this->assertPostExists( [ + 'post_type' => Provider::OBJECT_NAME, + 'post_status' => Post_Status::PENDING, + ] ); + + $this->assertInCronQueue( Example_Job::class ); + } } class Example_Job implements Job, Can_Queue {