Sergey Rabochiy Fabien Potencier
Contributed by Sergey Rabochiy and Fabien Potencier in #47112

A common need for a project is to do tasks on a regular basis like sending end of trial emails. In the Unix world, you can use "cron" for such recurring tasks. Coupled with Symfony commands, you have everything you need.

But crons come with limitations. That's why in Symfony 6.3 we're introducing a new Scheduler component. This component allows you to trigger messages that should be sent on a predefined schedule. It reuses the Messenger concepts you're already familiar with.

Inside a Symfony application, you first define a new schedule provider that creates the messages and defines how often they should be sent. In this example, the schedule will send a message every 2 days that will signal your application to do something about orders that have been created but not paid yet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
// ...

#[AsSchedule('default')]
class DefaultScheduleProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return (new Schedule())->add(
            RecurringMessage::every('2 days', new PendingOrdersMessage())
        );
    }
}

Then, create the PendingOrdersMessage and its handler, in the same way as any other messages and handlers in Messenger. Finally, run the message consumer associated to this schedule (e.g. via a command console run in a worker) to generate the messages:

1
2
3
# the '_default' suffix in the scheduler name is
# the value you defined before in the #[AsSchedule] attribute
$ symfony console messenger:consume -v scheduler_default

And that's all. Internally, each schedule is transformed into a Messenger transport. Transports generate the messages (i.e. they are not dispatched) and those messages are handled immediately (like the sync transport).

One of the best features of this component is that it's based on Messenger infrastructure. Reusing the same concepts (messages, handlers, stamps, etc.) allows you to learn it fast. Besides, reusing the same worker as Messenger means that you can use the same time limits, memory management, signal handling, etc.

The message frequency can be defined in many different ways:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
RecurringMessage::every('10 seconds', $msg)
RecurringMessage::every('1 day', $msg)

RecurringMessage::every('next tuesday', $msg)
RecurringMessage::every('first monday of next month', $msg)

# run at a very specific time every day
RecurringMessage::every('1 day', $msg, from: '13:47')
# you can pass full date/time objects too
RecurringMessage::every('1 day', $msg,
    from: new \DateTimeImmutable('13:47', new \DateTimeZone('Europe/Paris'))
)

# define the end of the handling too
RecurringMessage::every('1 day', $msg, until: '2023-09-21')

# you can even use Cron expressions
RecurringMessage::cron('0 12 * * 1', $msg) // every Monday at 12:00
RecurringMessage::cron('#midnight', $msg)
RecurringMessage::cron('#weekly', $msg)

We're still writing the docs for this new component and we hope to have them ready soon after the Symfony 6.3 release. Meanwhile, you can watch for free the keynote that Fabien delivered during the SymfonyOnline June 2023 conference.

Published in #Living on the edge