feat(actions): add #[Scheduled] attribute for Action classes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-12 12:23:33 +00:00
parent cb6d206c76
commit 8d0b2b64ec
2 changed files with 108 additions and 0 deletions

View file

@ -0,0 +1,46 @@
<?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 Attribute;
/**
* Mark an Action class for scheduled execution.
*
* The frequency string maps to Laravel Schedule methods:
* - 'everyMinute' ->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,
) {}
}

View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Core\Tests\Feature;
use Core\Actions\Scheduled;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
class ScheduledAttributeTest extends TestCase
{
public function test_attribute_can_be_instantiated_with_frequency(): void
{
$attr = new Scheduled(frequency: 'dailyAt:09:00');
$this->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
{
}