Skip to content

Commit fd2967e

Browse files
committed
Add AsCronTask & AsPeriodicTask attributes
1 parent 73a6b4b commit fd2967e

File tree

10 files changed

+218
-1
lines changed

10 files changed

+218
-1
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class UnusedTagsPass implements CompilerPassInterface
8484
'routing.loader',
8585
'routing.route_loader',
8686
'scheduler.schedule_provider',
87+
'scheduler.task',
8788
'security.authenticator.login_linker',
8889
'security.expression_language_provider',
8990
'security.remember_me_handler',

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@
151151
use Symfony\Component\RemoteEvent\RemoteEvent;
152152
use Symfony\Component\Routing\Loader\AnnotationClassLoader;
153153
use Symfony\Component\Routing\Loader\Psr4DirectoryLoader;
154+
use Symfony\Component\Scheduler\Attribute\AsCronTask;
154155
use Symfony\Component\Scheduler\Attribute\AsSchedule;
156+
use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;
155157
use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory;
156158
use Symfony\Component\Security\Core\AuthenticationEvents;
157159
use Symfony\Component\Security\Core\Exception\AuthenticationException;
@@ -716,6 +718,28 @@ public function load(array $configs, ContainerBuilder $container)
716718
$container->registerAttributeForAutoconfiguration(AsSchedule::class, static function (ChildDefinition $definition, AsSchedule $attribute): void {
717719
$definition->addTag('scheduler.schedule_provider', ['name' => $attribute->name]);
718720
});
721+
foreach ([AsPeriodicTask::class, AsCronTask::class] as $taskAttributeClass) {
722+
$container->registerAttributeForAutoconfiguration(
723+
$taskAttributeClass,
724+
static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribute, \ReflectionClass|\ReflectionMethod $reflector): void {
725+
$tagAttributes = get_object_vars($attribute) + [
726+
'trigger' => match (get_class($attribute)) {
727+
AsPeriodicTask::class => 'every',
728+
AsCronTask::class => 'cron',
729+
}
730+
];
731+
if ($reflector instanceof \ReflectionMethod) {
732+
if (isset($tagAttributes['method'])) {
733+
throw new LogicException(
734+
sprintf('AsPeriodicTask attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name)
735+
);
736+
}
737+
$tagAttributes['method'] = $reflector->getName();
738+
}
739+
$definition->addTag('scheduler.task', $tagAttributes);
740+
}
741+
);
742+
}
719743

720744
if (!$container->getParameter('kernel.debug')) {
721745
// remove tagged iterator argument for resource checkers

src/Symfony/Bundle/FrameworkBundle/Resources/config/scheduler.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@
1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

1414
use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory;
15+
use Symfony\Component\Scheduler\Messenger\ServiceCallMessageHandler;
1516

1617
return static function (ContainerConfigurator $container) {
1718
$container->services()
19+
->set('scheduler.messenger.service_call_message_handler', ServiceCallMessageHandler::class)
20+
->args([
21+
tagged_locator('scheduler.task'),
22+
])
23+
->tag('messenger.message_handler')
1824
->set('scheduler.messenger_transport_factory', SchedulerTransportFactory::class)
1925
->args([
2026
tagged_locator('scheduler.schedule_provider', 'name'),
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger;
4+
5+
use Symfony\Component\Scheduler\Attribute\AsCronTask;
6+
use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;
7+
8+
#[AsCronTask(expression: '* * * * *', arguments: [1], schedule: 'dummy')]
9+
#[AsCronTask(expression: '0 * * * *', timezone: 'Europe/Berlin', arguments: ['2'], schedule: 'dummy', method: 'method2')]
10+
#[AsPeriodicTask(frequency: 5, arguments: [3], schedule: 'dummy')]
11+
#[AsPeriodicTask(frequency: 'every day', from: '00:00:00', jitter: 60, arguments: ['4'], schedule: 'dummy', method: 'method4')]
12+
class DummyTask
13+
{
14+
public static array $calls = [];
15+
16+
#[AsPeriodicTask(frequency: 'every hour', from: '09:00:00', until: '17:00:00', arguments: ['b' => 6, 'a' => '5'], schedule: 'dummy')]
17+
#[AsCronTask(expression: '0 0 * * *', arguments: ['7', 8], schedule: 'dummy')]
18+
public function attributesOnMethod(string $a, int $b): void
19+
{
20+
self::$calls[__FUNCTION__][] = [$a, $b];
21+
}
22+
23+
public function __call(string $name, array $arguments)
24+
{
25+
self::$calls[$name][] = $arguments;
26+
}
27+
}

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Scheduler/config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ services:
1010
Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummySchedule:
1111
autoconfigure: true
1212

13+
Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyTask:
14+
autoconfigure: true
15+
1316
clock:
1417
synthetic: true
1518

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Symfony\Component\Scheduler\Attribute;
4+
5+
/**
6+
* A marker to call a service method from scheduler
7+
*
8+
* @author valtzu <valtzu@gmail.com>
9+
*/
10+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
11+
class AsCronTask
12+
{
13+
public function __construct(
14+
public readonly string $expression,
15+
public readonly ?string $timezone = null,
16+
public readonly ?int $jitter = null,
17+
public readonly array $arguments = [],
18+
public readonly string $schedule = 'default',
19+
public readonly ?string $method = null,
20+
) {
21+
}
22+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Scheduler\Attribute;
13+
14+
/**
15+
* A marker to call a service method from scheduler
16+
*
17+
* @author valtzu <valtzu@gmail.com>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
20+
class AsPeriodicTask
21+
{
22+
public function __construct(
23+
public readonly string|int $frequency,
24+
public readonly ?string $from = null,
25+
public readonly ?string $until = null,
26+
public readonly ?int $jitter = null,
27+
public readonly array $arguments = [],
28+
public readonly string $schedule = 'default',
29+
public readonly ?string $method = null,
30+
) {
31+
}
32+
}

src/Symfony/Component/Scheduler/DependencyInjection/AddScheduleMessengerPass.php

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
1616
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1718
use Symfony\Component\DependencyInjection\Reference;
1819
use Symfony\Component\Messenger\Transport\TransportInterface;
20+
use Symfony\Component\Scheduler\Messenger\ServiceCallMessage;
21+
use Symfony\Component\Scheduler\RecurringMessage;
22+
use Symfony\Component\Scheduler\Schedule;
1923

2024
/**
2125
* @internal
@@ -29,8 +33,45 @@ public function process(ContainerBuilder $container): void
2933
$receivers[$tags[0]['alias']] = true;
3034
}
3135

32-
foreach ($container->findTaggedServiceIds('scheduler.schedule_provider') as $tags) {
36+
$scheduleProviders = [];
37+
foreach ($container->findTaggedServiceIds('scheduler.schedule_provider') as $serviceId => $tags) {
3338
$name = $tags[0]['name'];
39+
$scheduleProviders[$name] = $container->getDefinition($serviceId);
40+
}
41+
42+
foreach ($container->findTaggedServiceIds('scheduler.task') as $serviceId => $tags) {
43+
foreach ($tags as $tagAttributes) {
44+
$scheduleName = $tagAttributes['schedule'] ?? 'default';
45+
$scheduleProviders[$scheduleName] ??= $container->setDefinition("scheduler.provider.$scheduleName", new Definition(Schedule::class))
46+
->addTag('scheduler.schedule_provider', ['name' => $scheduleName]);
47+
48+
$taskArguments = [
49+
'$message' => new Definition(ServiceCallMessage::class, [$serviceId, $tagAttributes['method'] ?? '__invoke', $tagAttributes['arguments'] ?? []])
50+
] + array_filter(match ($tagAttributes['trigger'] ?? throw new InvalidArgumentException("Tag 'scheduler.task' is missing attribute 'trigger' on service $serviceId")) {
51+
'every' => [
52+
'$frequency' => $tagAttributes['frequency'] ?? throw new InvalidArgumentException("Tag 'scheduler.task' is missing attribute 'frequency' on service $serviceId"),
53+
'$from' => $tagAttributes['from'] ?? null,
54+
'$until' => $tagAttributes['until'] ?? null,
55+
],
56+
'cron' => [
57+
'$expression' => $tagAttributes['expression'] ?? throw new InvalidArgumentException("Tag 'scheduler.task' is missing attribute 'expression' on service $serviceId"),
58+
'$timezone' => $tagAttributes['timezone'] ?? null,
59+
],
60+
}, fn ($value) => !is_null($value));
61+
62+
$taskDefinition = (new Definition(RecurringMessage::class))
63+
->setFactory([RecurringMessage::class, $tagAttributes['trigger']])
64+
->setArguments($taskArguments);
65+
66+
if ($tagAttributes['jitter'] ?? false) {
67+
$taskDefinition->addMethodCall('withJitter', [$tagAttributes['jitter']], true);
68+
}
69+
70+
$scheduleProviders[$scheduleName]->addMethodCall('add', [$taskDefinition]);
71+
}
72+
}
73+
74+
foreach (array_keys($scheduleProviders) as $name) {
3475
$transportName = 'scheduler_'.$name;
3576

3677
// allows to override the default transport registration
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Symfony\Component\Scheduler\Messenger;
4+
5+
6+
/**
7+
* Represents a service call
8+
*
9+
* @author valtzu <valtzu@gmail.com>
10+
*/
11+
class ServiceCallMessage implements \Stringable
12+
{
13+
public function __construct(
14+
private readonly string $serviceId,
15+
private readonly string $method = '__invoke',
16+
private readonly array $arguments = [],
17+
) {
18+
}
19+
20+
public function getServiceId(): string
21+
{
22+
return $this->serviceId;
23+
}
24+
25+
public function getMethod(): string
26+
{
27+
return $this->method;
28+
}
29+
30+
public function getArguments(): array
31+
{
32+
return $this->arguments;
33+
}
34+
35+
public function __toString(): string
36+
{
37+
return sprintf("ServiceCallMessage<%s>", "@$this->serviceId" . ($this->method !== '__invoke' ? "::$this->method" : ''));
38+
}
39+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Symfony\Component\Scheduler\Messenger;
4+
5+
use Psr\Container\ContainerInterface;
6+
7+
/**
8+
* Handler to call any service
9+
*
10+
* @author valtzu <valtzu@gmail.com>
11+
*/
12+
class ServiceCallMessageHandler
13+
{
14+
public function __construct(private readonly ContainerInterface $serviceLocator)
15+
{
16+
}
17+
18+
public function __invoke(ServiceCallMessage $message): void
19+
{
20+
$this->serviceLocator->get($message->getServiceId())->{$message->getMethod()}(...$message->getArguments());
21+
}
22+
}

0 commit comments

Comments
 (0)