Hey! You're still here?
Great! I have a bonus chapter for you.
One of our interns, Hugo, is complaining that he has to log in to our server
and run the booking reminders command, every night at midnight.
I don't know what the problem is - isn't that what interns are for?!
But... I guess to be more robust, we should automate this in case he's sick or forgets.
We could set up a CRON job...
but that wouldn't be nearly as cool or flexible as using the Symfony Scheduler component.
It's perfect for this.
At your terminal, run: composer require scheduler Think
of Symfony Scheduler as an add-on for Messenger.
It provides its own special transport that, instead of a queue,
determines if it's time to run a job.
Each job, or task, is a messenger message, so it requires a message handler.
You consume the schedule, like any messenger transport with the messenger:consume command.
Create a schedule with: symfony console make:schedule Transport name?
Use default.
Schedule name?
Use the default: MainSchedule.
Exciting! It's possible to have multiple schedules,
but for most apps, a single schedule is enough.
Check it out: src/Scheduler/MainSchedule.php.
It's a service that implements ScheduleProviderInterface and is marked
with the #[AsSchedule] attribute with the name default.
The maker automatically injected the cache, and we'll see why in a second.
The getSchedule() method is where we configure the schedule and add tasks.
This ->stateful() that we're passing $this->cache to is important.
If the process that's running this schedule goes down -
like our messenger workers stop temporarily during a server restart -
when it comes back online, it will know all the jobs it missed and run them.
If a task was supposed to run 10 times while it was down, it will run them all.
That might not be desired so add ->processOnlyLastMissedRun(true)
to only run the last one: Bulletproof!
For more complex apps, you might be consuming the same schedule on multiple workers.
Use ->lock() to configure a lock so that only one worker runs the task when its due.
Time to add our first task!
In ->add(), write RecurringMessage::.
There are a few different ways to trigger a task.
I like to use cron().
I want this task to run at midnight, every day,
so use 0 0 * * *. The second argument is the messenger message to dispatch.
We want to run the SendBookingRemindersCommand, but we can't add it here directly.
Instead, use new RunCommandMessage() and pass the command name:
app:send-booking-reminders (you can pass arguments and options here too):
At your terminal, list our schedule's tasks by running:
symfony console debug:schedule Oh, we have an error.
You cannot use "CronExpressionTrigger" as the
"cron expression" package is not installed Easy fix: copy the install command and run it:
composer require dragonmantank/cron-expression Cool name!
Now run the debug command again: Here we go, the output's a little wonky on this small screen,
but you can see the cron expression, the message (and command),
and the next runtime: tonight at midnight.
There's an alternate to schedule commands.
In MainSchedule::getSchedule(), delete the ->add().
Then jump over to our SendBookingRemindersCommand
and add another attribute: #[AsCronTask()] passing: 0 0 * * *: In your terminal,
debug the schedule again to make sure it's still listed: And it is, pretty neat.
If you have a lot of tasks scheduled at the same time, like midnight,
you might see a CPU spike at this time on your server.
Unless it's super important that tasks run at a very specific time, you should spread them out.
One way to do this of course, is to manually make sure they all have different cron
expressions but...
that's a bore.
For our app:send-booking-reminders command,
I don't care when it runs, just that it runs once a day.
We can use a hashed cron expression.
In our expression, replace the 0's with #'s.
The # means "pick a random, valid value for this part":
Debug the schedule again: It's set to run at 5:11am.
Run the command again: It's still 5:11am.
Ok, so it's not truly random, the values are calculated deterministically based
on the message details.
In our case, the string app:send-booking-reminders.
A different command with the same hash expression will have different values.
The Scheduler documentation has all the details on this.
There's even aliases for common hashes.
For instance, #mignight will pick a time between midnight and 3am.
Use that for our expression: and debug the schedule again: Oops, a typo, I'll fix that
and run again: It's now scheduled to run every day at 2:11am.
Cool! We've configured our schedule, but how do we run it?
Remember, schedules are just Messenger transports.
The transport name is scheduler_<schedule_name>, in our case, scheduler_default.
Run it with: symfony console messenger:consume scheduler_default On your production server,
configure this to run in the background just like a normal messenger worker.
Alright, that's a quick rundown of the Scheduler component.
Check out the documentation to learn more about it!
Happy coding and happy scheduling!