How To Implement Laravel Dynamic Scheduling
Last Updated on November 11, 2022
Introduction
Task Scheduling helps us run tasks in the background at a specific time or date in the future. We can have long and boring tasks run when the traffic on our servers is low.
Through scheduling, we can have our application carry out tasks on autopilot allowing us to automate most of the functionalities.
Laravel helps us schedule tasks such as sending emails and updating cached records with the help of the Laravel scheduler. We can set tasks to run on a specific day of the week if need be. What if we want to have control over the scheduling process? In this article, I am going to show you how to implement Laravel dynamic scheduling.
I am going to use a simple scenario of an e-commerce platform that wants to run promotions. The idea is that the e-commerce platform wants to have control over which promotions are to run and at what point.
To do so, I will have a start and end time for each promotion. I am going to use Carbon to manipulate the DateTime.
How can I create dynamic Cron jobs in Laravel?
To create a dynamic cron job in Laravel, we can follow these steps:
1. Create a Laravel Project
You can create a new Laravel project using composer or the Laravel installer
composer create-project --prefer-dist laravel/laravel laravel_dynamic_scheduling
OR
laravel new laravel_dynamic_scheduling
2. Create a Promotions Model and Migration
The next step is to create the promo model which will have the details of the promotions.
php artisan make:model Promotions -m
I am going to add the promotions schema to the promotions table. It will contain the promotion’s name, start time, end time and promotion status.
Schema::create('promotions', function (Blueprint $table) {
$table->id();
$table->string('promo_name');
$table->dateTime('start_time');
$table->dateTime('end_time');
$table->boolean('status')->default(0);
$table->timestamps();
});
3. Create New Artisan Commands
We then need to create the Artisan Commands that need to be scheduled. Since I am using the promotions as an example, I will create two commands by using make:command.
php artisan make:command StartPromoCommand
I will also create an identical command that will be responsible for ending currently running promotions.
php artisan make:command EndPromoCommand
This is going to create two class files in the app/Console/Commands folder in your Laravel application.
For the StartPromosCommand, I will add the following logic:
The command above contains the $signature and the $description variables that define the terminal command and the description of the command respectively. All I am doing is checking for every promotion in the database if the start time is the same as the current time and also if the promotion is inactive, I am updating the promotion status to active. With the help of Carbon and the isCurrentMinute function, I am able to do all the checks.
The same logic will apply for the end:promo command.
The next step is to update the $commands variable in the app/Console/Kernel.php file as shown.
If the variable is not present, you can add it and register your commands
protected $commands = [
'App\Console\Commands\Your Command'
];
To verify the command has been registered and available in the artisan command list, the following command can be run on the terminal.
php artisan list
4. Schedule Dynamic Cron Jobs
This is where it gets interesting. We need to instruct Laravel to run our commands dynamically. So how do we do that? It’s pretty easy. With the help of Carbon and the when() function available in the Schedule class, we can have Laravel dynamic scheduling implemented.
Instead of having it run at a specific time that we set statically, we can use the time stored in the database as the scheduled time.
In the schedule() method in the app/Console/Kernel.php file, I will add the following.
protected function schedule(Schedule $schedule)
{
$promos = Promotions::all();
foreach ($promos as $promo) {
$start_time = Carbon::parse($promo->start_time);
$end_time = Carbon::parse($promo->end_time);
if (!is_null($start_time) && $start_time->isCurrentMinute() && $promo->status = 0) {
$schedule->command('start:promo')->when($start_time->isCurrentMinute())->withoutOverlapping();
} elseif (!is_null($end_time) && $end_time->isCurrentMinute() && $promo->status = 1) {
$schedule->command('end:promo')->when($end_time->isCurrentMinute())->withoutOverlapping();
}
}
}
In this part, I am getting the start time and end time for each promotion and if the time stored in the database is the same as the current time, the respective command will be executed.
I am using the withoutOverlapping() function to have only one task instantiated every time. This helps in avoiding memory leaks in the server, especially when in production.
5. Running the Scheduler
Now that the commands are scheduled, It is now time to test the scheduler. There are two ways we can run the scheduler; locally or on a server(in production).
Locally
We can use the schedule:work command to run the Laravel scheduler.
php artisan schedule:work
In Production
To run background tasks in production, we will need to use the schedule:run command. Since in production we have no control over the scheduler, we need a way of starting the scheduler which will be responsible for running the background tasks in production.
Most production servers use Ubuntu as the OS and the good thing is that in Ubuntu it is easy to register a new cron entry. To do so, we will need to add the schedule:run command in a file known as crontab. To access the file all you need to type in the terminal is crontab -e. We can add the following command in the crontab file
* * * * * php /path-to-your-laravel-project/artisan schedule:run >> /dev/null 2>&1
Note: Please change path-to-your-laravel-project to match your laravel application’s path.
We now need to restart the cron service for the changes to take effect
sudo service cron restart
That’s pretty much it.
But wait. What if you have multiple servers in production?
This is a common scenario in multiple production environments where a codebase is replicated onto multiple servers and load balanced. To have the tasks run on one server, we can use the onOneServer() method provided in the Schedule class.
Note: For this to work, you need a central cache service for your stack; be it Redis or Memcached.
protected function schedule(Schedule $schedule)
{
$promos = Promotions::all();
foreach ($promos as $promo) {
$start_time = Carbon::parse($promo->start_time);
$end_time = Carbon::parse($promo->end_time);
if (!is_null($start_time) && $start_time->isCurrentMinute() && $promo->status = 0) {
$schedule->command('start:promo')->when($start_time->isCurrentMinute())->withoutOverlapping()->onOneServer();
} elseif (!is_null($end_time) && $end_time->isCurrentMinute() && $promo->status = 1) {
$schedule->command('end:promo')->when($end_time->isCurrentMinute())->withoutOverlapping()->onOneServer();
}
}
}
With that, we are done with the Laravel dynamic scheduling. Any Promotions added to the database will be started and stopped on autopilot.
Conclusion
In this article, you have learned how to implement Laravel dynamic scheduling with a simple example. To implement static scheduling, you can check this article. I hope this article sheds some light on how to implement dynamic scheduling in Laravel. Have questions? Let me know in the comments below. I’ll try my best to answer all of them.
copy code from images: love it
Glad you liked it
I’m just wondering about something. Does it mean each time the scheduler will run, all promotions will be retrieved from the database?
Perhaps there will not be much rows retrieved, but queries will be performed…
There are multiple ways we can refine the query made to the database. The first could be to retrieve the promotions based on their status and execute the task for the only retrieved records. The other way could be Caching the records and retrieve the cached records each time the scheduler runs. This way we can reduce the number of queries made to the database.
What exatly is the aim of starting/ending a promo while you can simply look at them and say that they are “started” or “ended”?
I agree you can update the status of the promo manually but I did use it as an example for this article. There are many more complex use-cases where we can use dynamic scheduling. An example could be an online test which is time-sensitive. You could have a test that runs for 2 hours but you want to offer flexibility to the users taking the test. In order to keep track of each user’s test, we could store the start time and the end time and then have the scheduler stop the test once the two hours are over.