Click to share! ⬇️

Scheduling Commands and Tasks In Laravel

As of Laravel 5, there is a great feature that allows you to set up automated tasks and commands in your Laravel application. This does assume you are running the application on a *nix flavored server since this technique employs the ability to control cron jobs via PHP. It would probably be a safe bet however that if you are using Laravel, you are almost certainly running on a Linux platform. Most likely this is Ubuntu, since this is what ships with the great Laravel Homestead. Cron jobs have often been a bit of a pain, let’s see how Laravel can help us with it’s Scheduler functionality.


Define the application’s command schedule

If we check out the Kernel class that is a part of the Console directory, then we will find the protected function schedule().
Define the applications command schedule

So what does this offer you? Well essentially, this is a new way to wrap your cron jobs. By that, we mean this new command is essentially a wrapper around linux cron jobs so that you can define recurring jobs or tasks to fire right inside your application. Memorizing all the different syntax formats is kind of tricky, so instead you can now do this with more friendly syntax. For example, in this table below, we have some common example cron jobs and how we can run them with their associated syntax in Laravel.

* * * * * * everyMinute()
*/5 * * * * * everyFiveMinutes()
*/10 * * * * * everyTenMinutes()
0,30 * * * * * everyThirtyMinutes()
0 * * * * * hourly()
0 0 * * * * daily()
0 0 * * 0 * weekly()
0 0 1 * * * monthly()
0 0 1 */3 * quarterly()
0 0 1 1 * * yearly()

You can do simple things like clean up your database, email reports, or you could even earn $4 million dollars per year with a cron job. Use your imagination to see what you could imagine applying a cron job to. As we can see from the table above, it is easier to remember the textual based method names than it is to memorize the syntax of cron jobs which is a bit cryptic. You can be more granular than the options listed here, but in most cases, these options should be more than enough to cover whatever you might like to do with a cron job.


Taking a closer look at schedule()

artisan commands

    protected function schedule(Schedule $schedule)
    {
        $schedule->command('inspire')
                 ->hourly();
    }

In this snippet, we can see that the schedule() function accepts a type hinted instance of the Schedule class. Then in the body of the schedule() function, we are able to define a schedule to fire artisan commands. The command above is commented out by default when Laravel is first installed. If we were to uncomment it, like we did above, it turns into the equivalent of running php artisan inspire at the command line every hour.

linux terminal commands

In addition to running artisan commands, you can run terminal commands using exec(). Here we will list the contents of a directory and send that output to a text file.


    protected function schedule(Schedule $schedule)
    {
        $schedule->exec('cd ~/Code/lpg && ls')
            ->everyMinute()
            ->sendOutputTo('/home/vagrant/Code/lpg/listing.txt');
    }

Configure The Scheduler

In order to make use of this nice syntax for cron jobs that Laravel provides to us, we do need to manually configure one cron job on the server ourselves. The way this works is that you hit one artisan command every minute. On each time this command runs, Laravel checks to see if there are any commands that need to be run, and if so it takes care of them for you. So basically, Laravel is checking for commands to fire every single minute, and when it needs to, it executes any commands in queue so to speak. So we write one cron job, then have the flexibility to define our scheduled tasks and commands how ever we like. Here is the cron job to configure, note that /path/to/artisan needs to be updated with your own environment configuration.

* * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1

If you wanted to test this out on your local server, such as homestead just to see how it works, we can do this. Although setting up cron jobs on a virtual machine you destroy and rebuild in a test environment will not accomplish a lot for you, it makes perfect sense for you to actually test out how this works before you try screwing up configuring your production server.

vagrant@homestead:~/Code/lpg$ crontab -e

You may get some output similar to what you see here.

# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h  dom mon dow   command

If you are prompted which text editor to use, we chose nano, so you can choose that option too. Then all you need to do is paste that cron we mentioned earlier into the file and save it. Now, you must first find the present working directory of where artisan lives for your project, and this will be what you paste into the cron file. Our little test project lives in a directory called lpg. So we ssh into our vagrant box, and then cd into Code/lpg. In here we run pwd and we are provided with the path of /home/vagrant/Code/lpg. Do not forget to append artisan to this path! Here is the original and updated cron job we will configure.

original

* * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1

updated

* * * * * php /home/vagrant/Code/lpg/artisan schedule:run >> /dev/null 2>&1

Now, we run crontab - e again, and enter the above string as the last line of the file and save.

Everything should be running at this point, and now we can test some actual commands and see if they work. This first one will list out the contents of our laravel directory and save them to a file called listing.txt.


    protected function schedule(Schedule $schedule)
    {
        $schedule->exec('cd ~/Code/lpg && ls')
            ->everyMinute()
            ->sendOutputTo('/home/vagrant/Code/lpg/listing.txt');
    }

In the above snippet, we are able to hook right into the server and run a command as if we are at the terminal. First, we pass in the command we want to run as a string to exec(). Notice that we pass in cd ~/Code/lpg && ls. This says, we are going to change into the lpg directory, and once we are there also run an ls command. The next method tells Laravel to do this every minute. In addition to this, we want to send the output of this command to a file. We need to provide the path to the location of where we want to store the output of our command. We will pass in what amounts to the same exact directory, and place the output into a file called listing.txt.

If we wait a couple of minutes just to make sure we give the cron a chance to run, we can then check to see if our command worked. We navigate to our laravel directory in the terminal, and run the command cat listing.txt. Lo and behold, it worked! We can see the contents of our laravel directory are contained in the file we specified.

vagrant@homestead:~/Code/lpg$ cat listing.txt
app
artisan
bootstrap
composer.json
composer.lock
config
database
gulpfile.js
_ide_helper.php
listing.txt
package.json
phpunit.xml
public
readme.md
resources
server.php
storage
tests
vendor

So that is pretty cool, we got it running and that is the main thing. At this point you can try all the different methods available to you to see how they work. Once you flesh them out locally, you’ll feel more confident about putting things on a live server.


How Does This Magic Happen?

The process of scheduling tasks in Laravel is handled by files such as Kernel.php, Schedule.php, Event.php, ScheduleRunCommand.php and ScheduleServiceProvider.php. It starts in the Schedule class, and also with the command method.

    public function command($command, array $parameters = [])
    {
        $binary = ProcessUtils::escapeArgument((new PhpExecutableFinder)->find(false));

        if (defined('HHVM_VERSION')) {
            $binary .= ' --php';
        }

        if (defined('ARTISAN_BINARY')) {
            $artisan = ProcessUtils::escapeArgument(ARTISAN_BINARY);
        } else {
            $artisan = 'artisan';
        }

        return $this->exec("{$binary} {$artisan} {$command}", $parameters);
    }

When you write something like $schedule->command(‘inspire’)->hourly(); – you are making use of what amounts to a wrapper around a general exec() method where you can execute any kind of terminal command. If we have a look at where that method is defined, we’ll see that we are updating an array. We add a new item to an events array on the Schedule object via a new instance of the Event class. If you add 5 new commands, the array now has 5 items waiting to be processed.

    public function exec($command, array $parameters = [])
    {
        if (count($parameters)) {
            $command .= ' '.$this->compileParameters($parameters);
        }

        $this->events[] = $event = new Event($command);

        return $event;
    }

In looking at the command that runs every minute, we can investigate what is happening. In the ScheduleRunCommand.php class there is a fire() method which fetches the events that are ready to be fired. We then loop over each one of them and run the event.


    public function fire()
    {
        $events = $this->schedule->dueEvents($this->laravel);

        $eventsRan = 0;

        foreach ($events as $event) {
            if (! $event->filtersPass($this->laravel)) {
                continue;
            }

            $this->line('<info>Running scheduled command:</info> '.$event->getSummaryForDisplay());

            $event->run($this->laravel);

            ++$eventsRan;
        }

        if (count($events) === 0 || $eventsRan === 0) {
            $this->info('No scheduled commands are ready to run.');
        }
    }

We can see that the isDue() method in the Schedule class uses array_filter() to examine the events array that has been populated, and returns only the ones that are due. There might be 5 or 10 in the array, but only the ones that are due will be fired.

    public function dueEvents($app)
    {
        return array_filter($this->events, function ($event) use ($app) {
            return $event->isDue($app);
        });
    }

Laravel makes use of a third party package to actually inspect the events to determine if they are due. We can see that in this code here.


    public function isDue($app)
    {
        if (! $this->runsInMaintenanceMode() && $app->isDownForMaintenance()) {
            return false;
        }

        return $this->expressionPasses() &&
               $this->runsInEnvironment($app->environment());
    }

    /**
     * Determine if the Cron expression passes.
     *
     * @return bool
     */
    protected function expressionPasses()
    {
        $date = Carbon::now();

        if ($this->timezone) {
            $date->setTimezone($this->timezone);
        }

        return CronExpression::factory($this->expression)->isDue($date->toDateTimeString());
    }

Scheduling Commands and Tasks In Laravel Summary

There is a bit more to it than what we have covered so far, but this is certainly enough to be dangerous with the Scheduler. Ultimately the takeaway is that we can easily build up an array to hold events to be fired. This works almost like a queue. Then, Laravel evaluates that array and fires off events as they are due. The API is very user friendly, and you’ll be amazed at how nice it is to use once you get used to it. Like all other tutorials, continue to have a play with the various commands available to you until they become easy and familiar to you.

Click to share! ⬇️