DEV Community

Zubair Mohsin
Zubair Mohsin

Posted on • Updated on • Originally published at zubair.dev

How Laravel uses dragonmantank/cron-expression package in Task Scheduling

From the About section of dragonmantank/cron-expression package on GitHub:

CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due.

This package helps in dealing with CRON jobs in following ways:

  • Determine if a cron job is due running?
  • When will a job run next?
  • When a cron job was previously ran?

Let’s take a look at implementation of these features in CronExpression class of this package.

<?php

namespace Cron;

class CronExpression
{
    /**
     * Determine if the cron is due to run based on the current date or a
     * specific date.  This method assumes that the current number of
     * seconds are irrelevant, and should be called once per minute.
     *
     * @param string|\DateTimeInterface $currentTime Relative calculation date
     * @param null|string               $timeZone    TimeZone to use instead of the system default
     *
     * @return bool Returns TRUE if the cron is due to run or FALSE if not
     */
    public function isDue($currentTime = 'now', $timeZone = null): bool
    {
        $timeZone = $this->determineTimeZone($currentTime, $timeZone);

        if ('now' === $currentTime) {
            $currentTime = new DateTime();
        } elseif ($currentTime instanceof DateTime) {
            $currentTime = clone $currentTime;
        } elseif ($currentTime instanceof DateTimeImmutable) {
            $currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
        } elseif (\is_string($currentTime)) {
            $currentTime = new DateTime($currentTime);
        }

        Assert::isInstanceOf($currentTime, DateTime::class);
        $currentTime->setTimezone(new DateTimeZone($timeZone));

        // drop the seconds to 0
        $currentTime->setTime((int) $currentTime->format('H'), (int) $currentTime->format('i'), 0);

        try {
            return $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp();
        } catch (Exception $e) {
            return false;
        }
    }

     /**
     * Get a next run date relative to the current date or a specific date
     *
     * @param string|\DateTimeInterface $currentTime      Relative calculation date
     * @param int                       $nth              Number of matches to skip before returning a
     *                                                    matching next run date.  0, the default, will return the
     *                                                    current date and time if the next run date falls on the
     *                                                    current date and time.  Setting this value to 1 will
     *                                                    skip the first match and go to the second match.
     *                                                    Setting this value to 2 will skip the first 2
     *                                                    matches and so on.
     * @param bool                      $allowCurrentDate Set to TRUE to return the current date if
     *                                                    it matches the cron expression.
     * @param null|string               $timeZone         TimeZone to use instead of the system default
     *
     * @throws \RuntimeException on too many iterations
     * @throws \Exception
     *
     * @return \DateTime
     */
    public function getNextRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
    {
        return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone);
    }

    /**
     * Get a previous run date relative to the current date or a specific date.
     *
     * @param string|\DateTimeInterface $currentTime      Relative calculation date
     * @param int                       $nth              Number of matches to skip before returning
     * @param bool                      $allowCurrentDate Set to TRUE to return the
     *                                                    current date if it matches the cron expression
     * @param null|string               $timeZone         TimeZone to use instead of the system default
     *
     * @throws \RuntimeException on too many iterations
     * @throws \Exception
     *
     * @return \DateTime
     *
     * @see \Cron\CronExpression::getNextRunDate
     */
    public function getPreviousRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
    {
        return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone);
    }
}
Enter fullscreen mode Exit fullscreen mode

The real meat is getRunDate() method. But its implementation is huge, it will only increase length of the blog. Here you can find full source code.

Usage in schedule:run command

Laravel leverages this package in Task Scheduling. Let’s say we make a command named say:hello and we schedule it hourly() inside Console\Kernel.php like below:

    /**
     * Define the application's command schedule.
     *
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('say:hello')->hourly();
    }
Enter fullscreen mode Exit fullscreen mode

Now, let’s see what happens when we run schedule:run command. Below is a ripped off version of schedule:run command’s handle method.

    public function handle(Schedule $schedule, ...)
    {
        foreach ($this->schedule->dueEvents($this->laravel) as $event) {
            if (! $event->filtersPass($this->laravel)) {
                $this->dispatcher->dispatch(new ScheduledTaskSkipped($event));

                continue;
            }

            if ($event->onOneServer) {
                $this->runSingleServerEvent($event);
            } else {
                $this->runEvent($event);
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

As we can see it loops through all due events , checks if given filters for the event do not pass, it skips that event, otherwise run that event.

Let’s take a look at dueEvents() method on Schedule class and understand what are events.

    /**
     * Get all of the events on the schedule that are due.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return \Illuminate\Support\Collection
     */
    public function dueEvents($app)
    {
        return collect($this->events)->filter->isDue($app);
    }

    /**
     * Get all of the events on the schedule.
     *
     * @return \Illuminate\Console\Scheduling\Event[]
     */
    public function events()
    {
        return $this->events;
    }
Enter fullscreen mode Exit fullscreen mode

dueEvents() return a collection of Illuminate\Console\Scheduling\Event objects, filtering these objects based on if they are due or not.

Now, let’s head over to Illuminate\Console\Scheduling\Event to see the implementation of isDue() method.

    /**
     * Determine if the given event should run based on the Cron expression.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return bool
     */
    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());
    }
Enter fullscreen mode Exit fullscreen mode

isDue() method checks for maintenance first, after that it calls expressionPasses() method, where we can finally see CronExpression class being used to determine if the cron expression of an event ( scheduled command ) is due .

Usage in schedule:list command

php artisan schedule:list command outputs a table on the command line like shown below. It contains information about all schedule commands, e.g. their interval, description and when a command is next due.

Let’s take a look at the handle method of this command.

    /**
     * Execute the console command.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     * @throws \Exception
     */
    public function handle(Schedule $schedule)
    {
        foreach ($schedule->events() as $event) {
            $rows[] = [
                $event->command,
                $event->expression,
                $event->description,
                (new CronExpression($event->expression))
                            ->getNextRunDate(Carbon::now())
                            ->setTimezone($this->option('timezone', config('app.timezone'))),
            ];
        }

        $this->table([
            'Command',
            'Interval',
            'Description',
            'Next Due',
        ], $rows ?? []);
    }
Enter fullscreen mode Exit fullscreen mode

As we can see, in order to get information about Next Due, Laravel utilises CronExpression class and it’s getNextRunDate() method.

Interesting facts about cron-expression package in Laravel context

  • It was first known as mtdowling/cron-expression , which was abandoned on 20 Dec, 2019 by Chris Tankersley
  • After that forked and maintained by Chris himself under dragonmantank/cron-expression
  • Taylor Otwell first used this package in Laravel v5.0 when he started working on scheduled commands back in 2014
  • Link to Taylor’s commit on GitHub when he first used this package in Laravel

I hope you find this blogpost useful. Next up we'll see how Laravel uses egulias/email-validator package. Stay in the loop by following me on Twitter

Top comments (0)