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+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy