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.
What about cancelling scheduled messages?
Say you dispatch a ReminderBecameDue message, but then the date of the original thing changed? You then need to modify the ReminderBecameDue message to be emitted on a different date.
How do you cancel a scheduled message (let's say a user under trial has transitioned to a paid plan before reaching the deadline)?
In the second code sample, where does $schedule come from?
I think there is a fundamental misconception about the component.
It is not intended to dispatch a scheduled message for an individual event / an individual user. You will still have to use a "normal" message for that.
I am reasonably sure this component is intended to replace "external" cron job management. Previously you had to write a Command and then use some third party tool (most likely crontab) to manage the schedule and execution of the job. Now you can configure all that in your Symfony app directly and execute it the same way how the messenger component works already.
Daniel is right. You can think of the Scheduler component as a cron replacement on steroids (one-second precision, distributed workers, overlap handling, dynamic schedules, and more).
I've just updated the blog post to avoid more confusion.
This should be
return (new Schedule())->add(
@Christian there was an error in the post that was fixed as @Kevin suggested. Thanks!
It is very cool! I the long-awaited component has appeared
Thank you for this new component that comes with a great DX!
Do I need to configure the messenger somehow additionally?
When I try to run the task, I get an error: [Symfony\Component\Console\Exception\RuntimeException] The receiver "scheduler_default" does not exist. Valid receivers are: async.
Be alittle aware that the scheduler uses the database timezone and not the server timezone.
So with a difference between those and using cron expressions it can be a little difference if you just copy the cron directly from the server.
Great Addition! The video is currently not available. 500 Error.
@sven It works now.
@Martin can be this the reason for that issue? https://github.com/symfony/symfony/issues/50764