8000 [Messenger][Scheduler] Add AsCronTask & AsPeriodicTask attributes · symfony/symfony@ed27b20 · GitHub
[go: up one dir, main page]

Skip to content

Commit ed27b20

Browse files
valtzufabpot
authored andcommitted
[Messenger][Scheduler] Add AsCronTask & AsPeriodicTask attributes
1 parent 30a35e4 commit ed27b20

File tree

11 files changed

+279
-3
lines changed

11 files changed

+279
-3
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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@
145145
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer;
146146
use Symfony\Component\RemoteEvent\RemoteEvent;
147147
use Symfony\Component\Routing\Loader\AnnotationClassLoader;
148+
use Symfony\Component\Scheduler\Attribute\AsCronTask;
149+
use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;
148150
use Symfony\Component\Scheduler\Attribute\AsSchedule;
149151
use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory;
150152
use Symfony\Component\Security\Core\AuthenticationEvents;
@@ -701,6 +703,26 @@ public function load(array $configs, ContainerBuilder $container)
701703
$container->registerAttributeForAutoconfiguration(AsSchedule::class, static function (ChildDefinition $definition, AsSchedule $attribute): void {
702704
$definition->addTag('scheduler.schedule_provider', ['name' => $attribute->name]);
703705
});
706+
foreach ([AsPeriodicTask::class, AsCronTask::class] as $taskAttributeClass) {
707+
$container->registerAttributeForAutoconfiguration(
708+
$taskAttributeClass,
709+
static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribute, \ReflectionClass|\ReflectionMethod $reflector): void {
710+
$tagAttributes = get_object_vars($attribute) + [
711+
'trigger' => match ($attribute::class) {
712+
AsPeriodicTask::class => 'every',
713+
AsCronTask::class => 'cron',
714+
},
715+
];
716+
if ($reflector instanceof \ReflectionMethod) {
717+
if (isset($tagAttributes['method'])) {
718+
throw new LogicException(sprintf('%s attribute cannot declare a method on "%s::%s()".', $attribute::class, $reflector->class, $reflector->name));
719+
}
720+
$tagAttributes['method'] = $reflector->getName();
721+
}
722+
$definition->addTag('scheduler.task', $tagAttributes);
723+
}
724+
);
725+
}
704726

705727
if (!$container->getParameter('kernel.debug')) {
706728
// 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

src/Symfony/Component/Messenger/Message/RedispatchMessage.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,22 @@
1313

1414
use Symfony\Component\Messenger\Envelope;
1515

16-
final class RedispatchMessage
16+
final class RedispatchMessage implements \Stringable
1717
{
1818
/**
19-
* @param object|Envelope $message The message or the message pre-wrapped in an envelope
19+
* @param object|Envelope $envelope The message or the message pre-wrapped in an envelope
2020
* @param string[]|string $transportNames Transport names to be used for the message
2121
*/
2222
public function __construct(
2323
public readonly object $envelope,
2424
public readonly array|string $transportNames = [],
2525
) {
2626
}
27+
28+
public function __toString(): string
29+
{
30+
$message = $this->envelope instanceof Envelope ? $this->envelope->getMessage() : $this->envelope;
31+
32+
return sprintf('%s via %s', $message instanceof \Stringable ? (string) $message : $message::class, implode(', ', (array) $this->transportNames));
33+
}
2734
}
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 AsCronTask
21+
{
22+
public function __construct(
23+
public readonly string $expression,
24+
public readonly ?string $timezone = null,
25+
public readonly ?int $jitter = null,
26+
public readonly array|string|null $arguments = null,
27+
public readonly string $schedule = 'default',
28+
public readonly ?string $method = null,
29+
public readonly array|string|null $transports = null,
30+
) {
31+
}
32+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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|string|null $arguments = null,
28+
public readonly string $schedule = 'default',
29+
public readonly ?string $method = null,
30+
public readonly array|string|null $transports = null,
31+
) {
32+
}
33+
}

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@
1111

1212
namespace Symfony\Component\Scheduler\DependencyInjection;
1313

14+
use Symfony\Component\Console\Messenger\RunCommandMessage;
1415
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1516
use Symfony\Component\DependencyInjection\ContainerBuilder;
1617
use Symfony\Component\DependencyInjection\Definition;
18+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1719
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\Messenger\Message\RedispatchMessage;
1821
use Symfony\Component\Messenger\Transport\TransportInterface;
22+
use Symfony\Component\Scheduler\Messenger\ServiceCallMessage;
23+
use Symfony\Component\Scheduler\RecurringMessage;
24+
use Symfony\Component\Scheduler\Schedule;
1925

2026
/**
2127
* @internal
@@ -29,8 +35,69 @@ public function process(ContainerBuilder $container): void
2935
$receivers[$tags[0]['alias']] = true;
3036
}
3137

32-
foreach ($container->findTaggedServiceIds('scheduler.schedule_provider') as $tags) {
38+
$scheduleProviderIds = [];
39+
foreach ($container->findTaggedServiceIds('scheduler.schedule_provider') as $serviceId => $tags) {
3340
$name = $tags[0]['name'];
41+
$scheduleProviderIds[$name] = $serviceId;
42+
}
43+
44+
$tasksPerSchedule = [];
45+
foreach ($container->findTaggedServiceIds('scheduler.task') as $serviceId => $tags) {
46+
foreach ($tags as $tagAttributes) {
47+
$serviceDefinition = $container->getDefinition($serviceId);
48+
$scheduleName = $tagAttributes['schedule'] ?? 'default';
49+
50+
if ($serviceDefinition->hasTag('console.command')) {
51+
$message = new Definition(RunCommandMessage::class, [$serviceDefinition->getClass()::getDefaultName().(empty($tagAttributes['arguments']) ? '' : " {$tagAttributes['arguments']}")]);
52+
} else {
53+
$message = new Definition(ServiceCallMessage::class, [$serviceId, $tagAttributes['method'] ?? '__invoke', (array) ($tagAttributes['arguments'] ?? [])]);
54+
}
55+
56+
if ($tagAttributes['transports'] ?? null) {
57+
$message = new Definition(RedispatchMessage::class, [$message, $tagAttributes['transports']]);
58+
}
59+
60+
$taskArguments = [
61+
'$message' => $message,
62+
] + array_filter(match ($tagAttributes['trigger'] ?? throw new InvalidArgumentException("Tag 'scheduler.task' is missing attribute 'trigger' on service $serviceId.")) {
63+
'every' => [
64+
'$frequency' => $tagAttributes['frequency'] ?? throw new InvalidArgumentException("Tag 'scheduler.task' is missing attribute 'frequency' on service $serviceId."),
65+
'$from' => $tagAttributes['from'] ?? null,
66+
'$until' => $tagAttributes['until'] ?? null,
67+
],
68+
'cron' => [
69+
'$expression' => $tagAttributes['expression'] ?? throw new InvalidArgumentException("Tag 'scheduler.task' is missing attribute 'expression' on service $serviceId."),
70+
'$timezone' => $tagAttributes['timezone'] ?? null,
71+
],
72+
}, fn ($value) => null !== $value);
73+
74+
$tasksPerSchedule[$scheduleName][] = $taskDefinition = (new Definition(RecurringMessage::class))
75+
->setFactory([RecurringMessage::class, $tagAttributes['trigger']])
76+
->setArguments($taskArguments);
77+
78+
if ($tagAttributes['jitter'] ?? false) {
79+
$taskDefinition->addMethodCall('withJitter', [$tagAttributes['jitter']], true);
80+
}
81+
}
82+
}
83+
84+
foreach ($tasksPerSchedule as $scheduleName => $tasks) {
85+
$id = "scheduler.provider.$scheduleName";
86+
$schedule = (new Definition(Schedule::class))->addMethodCall('add', $tasks);
87+
88+
if (isset($scheduleProviderIds[$scheduleName])) {
89+
$schedule
90+
->setFactory([new Reference('.inner'), 'getSchedule'])
91+
->setDecoratedService($scheduleProviderIds[$scheduleName]);
92+
} else {
93+
$schedule->addTag('scheduler.schedule_provider', ['name' => $scheduleName]);
94+
$scheduleProviderIds[$scheduleName] = $id;
95+
}
96+
97+
$container->setDefinition($id, $schedule);
98+
}
99+
100+
foreach (array_keys($scheduleProviderIds) as $name) {
34101
$transportName = 'scheduler_'.$name;
35102

36103
// allows to override the default transport registration
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\Messenger;
13+
14+
/**
15+
* Represents a service call.
16+
*
17+
* @author valtzu <valtzu@gmail.com>
18+
*/
19+
class ServiceCallMessage implements \Stringable
20+
{
21+
public function __construct(
22+
private readonly string $serviceId,
23+
private readonly string $method = '__invoke',
24+
private readonly array $arguments = [],
25+
) {
26+
}
27+
28+
public function getServiceId(): string
29+
{
30+
return $this->serviceId;
31+
}
32+
33+
public function getMethod(): string
34+
{
35+
return $this->method;
36+
}
37+
38+
public function getArguments(): array
39+
{
40+
return $this->arguments;
41+
}
42+
43+
public function __toString(): string
44+
{
45+
return "@$this->serviceId".('__invoke' !== $this->method ? "::$this->method" : '');
46+
}
47+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 Symfo B8AB ny\Component\Scheduler\Messenger;
13+
14+
use Psr\Container\ContainerInterface;
15+
16+
/**
17+
* Handler to call any service.
18+
*
19+
* @author valtzu <valtzu@gmail.com>
20+
*/
21+
class ServiceCallMessageHandler
22+
{
23+
public function __construct(private readonly ContainerInterface $serviceLocator)
24+
{
25+
}
26+
27+
public function __invoke(ServiceCallMessage $message): void
28+
{
29+
$this->serviceLocator->get($message->getServiceId())->{$message->getMethod()}(...$message->getArguments());
30+
}
31+
}

0 commit comments

Comments
 (0)
0