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);
}
}
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();
}
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);
}
}
}
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;
}
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());
}
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 ?? []);
}
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