feat(actions): add ScheduleServiceProvider — wires DB-backed actions into scheduler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-12 12:42:59 +00:00
parent d1598882bb
commit 633fbeb559
3 changed files with 151 additions and 0 deletions

View file

@ -0,0 +1,73 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Actions;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
/**
* Reads scheduled_actions table and wires enabled actions into Laravel's scheduler.
*
* This provider runs in console context only. It queries the database for enabled
* scheduled actions and registers them with the Laravel Schedule facade.
*
* The scheduled_actions table is populated by the `schedule:sync` command,
* which discovers #[Scheduled] attributes on Action classes.
*/
class ScheduleServiceProvider extends ServiceProvider
{
public function boot(): void
{
if (! $this->app->runningInConsole()) {
return;
}
// Guard against table not existing (pre-migration)
if (! Schema::hasTable('scheduled_actions')) {
return;
}
$this->app->booted(function () {
$schedule = $this->app->make(Schedule::class);
$actions = ScheduledAction::enabled()->get();
foreach ($actions as $action) {
$class = $action->action_class;
if (! class_exists($class)) {
continue;
}
$event = $schedule->call(function () use ($class, $action) {
$class::run();
$action->markRun();
})->name($class);
// Apply frequency
$method = $action->frequencyMethod();
$args = $action->frequencyArgs();
$event->{$method}(...$args);
// Apply options
if ($action->without_overlapping) {
$event->withoutOverlapping();
}
if ($action->timezone) {
$event->timezone($action->timezone);
}
}
});
}
}

View file

@ -35,6 +35,8 @@ class Boot extends ServiceProvider
return;
}
$this->app->register(\Core\Actions\ScheduleServiceProvider::class);
$this->fireConsoleBooting();
}

View file

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Core\Tests\Feature;
use Core\Actions\ScheduledAction;
use Core\Actions\ScheduleServiceProvider;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Schema;
use Orchestra\Testbench\TestCase;
class ScheduleServiceProviderTest extends TestCase
{
use RefreshDatabase;
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__.'/../../database/migrations');
}
protected function defineEnvironment($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
}
protected function getPackageProviders($app): array
{
return [
ScheduleServiceProvider::class,
];
}
public function test_provider_registers_enabled_actions_with_scheduler(): void
{
ScheduledAction::create([
'action_class' => 'Core\\Tests\\Fixtures\\Mod\\Scheduled\\Actions\\EveryMinuteAction',
'frequency' => 'everyMinute',
'is_enabled' => true,
]);
ScheduledAction::create([
'action_class' => 'Core\\Tests\\Fixtures\\Mod\\Scheduled\\Actions\\DailyAction',
'frequency' => 'dailyAt:09:00',
'timezone' => 'Europe/London',
'is_enabled' => false,
]);
// Re-boot the provider to pick up the new rows
$provider = new ScheduleServiceProvider($this->app);
$provider->boot();
$schedule = $this->app->make(Schedule::class);
$events = $schedule->events();
// Should have at least the enabled action
$this->assertNotEmpty($events);
}
public function test_provider_skips_when_table_does_not_exist(): void
{
// Drop the table
Schema::dropIfExists('scheduled_actions');
// Should not throw
$provider = new ScheduleServiceProvider($this->app);
$provider->boot();
$this->assertTrue(true);
}
}