New in Symfony 6.3: Scheduler Component
Contributed by
Sergey Rabochiy
and Fabien Potencier
in #47112.
A common need when using the Messenger component is to send some messages at
a certain date/time. For example, imagine a project that allows users to sign up
for a free trial. You probably want to send a message in 30 days to remind them
about the end of the trial.
You can do that today by adding metadata to messages:
$message = new EndOfTrialMessage($userId);
$endOfTrial = (new DateTimeImmutable())->modify(‚+30 days‘);
$message = Envelope::wrap($msg)->with(DelayStamp::delayUntil($endOfTrial));
This works but it looks convoluted. That’s why in Symfony 6.3 we’re introducing
a new Scheduler component that integrates tightly with the Messenger component.
This component allows you to create messages that should be handled multiple
times on a predefined schedule.
Inside a Symfony application, you first define a new schedule provider that
creates the messages and defines how often they should be handled. In this
example, the schedule will send a reminder message every 2 days for all orders
that have been created but not paid:
use SymfonyComponentSchedulerAttributeAsSchedule;
use SymfonyComponentSchedulerRecurringMessage;
// …
#[AsSchedule(‚default‘)]
class DefaultScheduleProvider implements ScheduleProviderInterface
{
public function getSchedule(): Schedule
{
return $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:
# 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.
If you try to send messages too fast (they take more time to generate than the
scheduled frequency) some messages will be dropped. To avoid these issues, you
can make the schedules stateful and you can also use locks:
#[AsSchedule(‚default‘)]
class DefaultScheduleProvider implements ScheduleProviderInterface
{
public function __construct(
private CacheInterface $cache,
private LockFactory $lockFactory,
) {
}
public function getSchedule(): Schedule
{
$schedule = (new Schedule())
// …
->stateful($this->cache)
->lock($this->lockFactory->createLock(‚default-scheduler‘))
;
}
}
The message frequency can be defined in many different ways:
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 recent SymfonyLive Paris 2023 conference (the
video is in French and the slides are in English).
Symfony Blog