From 8d0b2b64ec5da35d09ae25c42733b075a6efb30d Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 12 Mar 2026 12:23:33 +0000 Subject: [PATCH] feat(actions): add #[Scheduled] attribute for Action classes Co-Authored-By: Claude Opus 4.6 --- src/Core/Actions/Scheduled.php | 46 ++++++++++++++++++ tests/Feature/ScheduledAttributeTest.php | 62 ++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/Core/Actions/Scheduled.php create mode 100644 tests/Feature/ScheduledAttributeTest.php diff --git a/src/Core/Actions/Scheduled.php b/src/Core/Actions/Scheduled.php new file mode 100644 index 0000000..1aeaecd --- /dev/null +++ b/src/Core/Actions/Scheduled.php @@ -0,0 +1,46 @@ +everyMinute() + * - 'dailyAt:09:00' → ->dailyAt('09:00') + * - 'weeklyOn:1,09:00' → ->weeklyOn(1, '09:00') + * - 'hourly' → ->hourly() + * - 'monthlyOn:1,00:00' → ->monthlyOn(1, '00:00') + * + * Usage: + * #[Scheduled(frequency: 'dailyAt:09:00', timezone: 'Europe/London')] + * class PublishDigest + * { + * use Action; + * public function handle(): void { ... } + * } + * + * Discovered by ScheduledActionScanner, persisted to scheduled_actions table + * via `php artisan schedule:sync`, and executed by ScheduleServiceProvider. + */ +#[Attribute(Attribute::TARGET_CLASS)] +class Scheduled +{ + public function __construct( + public string $frequency, + public ?string $timezone = null, + public bool $withoutOverlapping = true, + public bool $runInBackground = true, + ) {} +} diff --git a/tests/Feature/ScheduledAttributeTest.php b/tests/Feature/ScheduledAttributeTest.php new file mode 100644 index 0000000..048512b --- /dev/null +++ b/tests/Feature/ScheduledAttributeTest.php @@ -0,0 +1,62 @@ +assertSame('dailyAt:09:00', $attr->frequency); + $this->assertNull($attr->timezone); + $this->assertTrue($attr->withoutOverlapping); + $this->assertTrue($attr->runInBackground); + } + + public function test_attribute_accepts_all_parameters(): void + { + $attr = new Scheduled( + frequency: 'weeklyOn:1,09:00', + timezone: 'Europe/London', + withoutOverlapping: false, + runInBackground: false, + ); + + $this->assertSame('weeklyOn:1,09:00', $attr->frequency); + $this->assertSame('Europe/London', $attr->timezone); + $this->assertFalse($attr->withoutOverlapping); + $this->assertFalse($attr->runInBackground); + } + + public function test_attribute_targets_class_only(): void + { + $ref = new ReflectionClass(Scheduled::class); + $attrs = $ref->getAttributes(\Attribute::class); + + $this->assertNotEmpty($attrs); + $instance = $attrs[0]->newInstance(); + $this->assertSame(\Attribute::TARGET_CLASS, $instance->flags); + } + + public function test_attribute_can_be_read_from_class(): void + { + $ref = new ReflectionClass(ScheduledAttributeTest_Stub::class); + $attrs = $ref->getAttributes(Scheduled::class); + + $this->assertCount(1, $attrs); + $instance = $attrs[0]->newInstance(); + $this->assertSame('everyMinute', $instance->frequency); + } +} + +#[Scheduled(frequency: 'everyMinute')] +class ScheduledAttributeTest_Stub +{ +}