mtdowling

Cron Expression Parsing in PHP

As a PHP developer, I’ve often been faced with the task of ensuring something happens on a recurring schedule or determining the next date in time an event will occur. At my previous job, we needed to run scheduled Gearman jobs on a recurring basis. We chose to use cron as the serialization format of our schedules, and implemented a database driven system for storing these schedules. Storing the cron schedules for these recurring jobs in the database allowed us to have an easy to maintain and durable data store for our recurring jobs, and it allowed us to deploy a very simple crontab to our job servers. The crontab contained a single cron job that ran a Gearman job every minute that checked if any of the recurring cron schedules matched the current time. When a schedule matched the current time, the job would run. This proved to be a very easy to maintain solution and allowed us to easily enable and disable recurring jobs if we were in a maintenance window, a job was failing, or if a job was producing erroneous results.

When faced with the task of creating the cron expression parsing part of this system, I searched high and low for an existing implementation in PHP that implemented the full feature set of a modern cron expression. Based on the context of this article, you probably guessed that I didn’t find one. I posted the original code I came up with to StackOverflow and eventually open sourced the project.

Cron-Expression, a cron expression library for PHP

The PHP cron expression parser I wrote can parse a CRON expression, determine if it is due to run, calculate the next run date of the expression, and calculate the previous run date of the expression. You can calculate dates far into the future or past by skipping n number of matching dates.

The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9), lists (e.g. 1,2,3), W to find the nearest weekday for a given day of the month, L to find the last day of the month, L to find the last given weekday of a month, and hash (#) to find the nth weekday of a given month.

You can clone the cron-expression library from the github page, install it with composer, or simply download the phar file and include it in your scripts.

Brief introduction to cron expressions

Cron utilizes cron expressions for representing recurring schedules. Cron expressions are made up of several fields, and each field represents a measurement of time. The fields in a cron expression are as follows: minute, hour, day of month, month, day of week, and an optional year. Here’s a an example cron expression that runs every minute, and below the expression are the positional fields.

*    *    *    *    *    *
-    -    -    -    -    -
|    |    |    |    |    |
|    |    |    |    |    + year [optional]
|    |    |    |    +----- day of week (0 - 7) (Sunday=0 or 7)
|    |    |    +---------- month (1 - 12)
|    |    +--------------- day of month (1 - 31)
|    +-------------------- hour (0 - 23)
+------------------------- min (0 - 59)

There are several special characters that modify the schedule of a cron expression, and some modifiers behave differently in different fields. You can find a list of all of the available special characters on cron’s Wikipedia page.

Cron expression use cases

Let’s say that you’re building a special promotion system into your e-commerce website. You want a special promotion to occur on a schedule. For the sake of this example, let’s say you want the promotion to occur every second Friday of every other month. This cron expression can be represented using 0 0 0 ? 1/2 FRI#2 *.

Calculate the next run date of a cron expression

So now that we’ve determined the schedule of our promotion, let’s write a snippet of code to check and see if the promotion should be in effect for the current date. This example assumes that you are using a phar file to include the library.

<?php

require 'cron.phar';

$cron = Cron\CronExpression::factory('0 0 0 ? 1/2 FRI#2 *');

if ($cron->isDue()) {
    // The promotion should be enabled!
}

Awesome! Now we know when the promotion should be enabled. But now our buyers are complaining that they have no idea when the promotion will run next. They suggest that you build an admin page that will show them the next 5 dates that the promotion will run.

Calculate the next X run dates of a cron expression

You can calculate the next run date of a cron expression using the cron-expression library using the getNextRunDate() method:

<?php

require 'cron.phar';

$cron = Cron\CronExpression::factory('0 0 0 ? 1/2 FRI#2 *');

// The getNextRunDate() method returns a \DateTime object
echo $cron->getNextRunDate()->format('Y-m-d H:i:s');

This will show the buyers the next date that the promotion will be enabled. But our buyers want to be able to plan a little in advance, so they need to know the next 5 dates that the promotion will run. You can get multiple next run dates using the getMultipleRunDates() method.

<?php

require 'cron.phar';

$cron = Cron\CronExpression::factory('0 0 0 ? 1/2 FRI#2 *');

foreach ($cron->getMultipleRunDates(5) as $date) {
    echo $date->format('Y-m-d H:i:s') . PHP_EOL;
}

Great! Now we know if the promotion should run on the current date and the next 5 times the promotion will run. But now our buyers are complaining that they need to know the previous dates that the promotion ran so that they can figure out all the fancy number projections they do when determining the sell-through of a product.

Calculate the last run date of a cron expression

You can get the last run date of a cron expression using the getPreviousRunDate() method.

<?php

require 'cron.phar';

$cron = Cron\CronExpression::factory('0 0 0 ? 1/2 FRI#2 *');

// Remember, most methods return a DateTime object
echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s');

Awesome! Our buyers still need to know the last 5 times the promotion ran.

If you want to know the last 5 run dates, you can use the getMultipleRunDates() method and set the $invert argument to true:

<?php

require 'cron.phar';

$cron = Cron\CronExpression::factory('0 0 0 ? 1/2 FRI#2 *');

foreach ($cron->getMultipleRunDates(5, 'now', true) as $date) {
    echo $date->format('Y-m-d H:i:s') . PHP_EOL;
}

That will display a list of 5 previous run dates, each going further back in time.

Now our buyers are asking if the promotion ran or will run on a specific day. Instead of having to field there emails every day and tell them whether or not the promotion ran, you decide to create an admin page so that they can enter a date and the page will tell the buyer if the promotion ran that day.

Check if the cron expression matches a specific date

You can see if a cron expression matches a specific date by calling isDue() with a specific date.

<?php

require 'cron.phar';

$cron = Cron\CronExpression::factory('0 0 0 ? 1/2 FRI#2 *');

if ($cron->isDue('January 5, 2012')) {
    echo 'The cron expression ran on this date :)';
} else {
    echo 'The cron expression did not run on this date :(';
}

Celebrate!

You’ve now successfully implemented all of the scheduling needs of your promotion! You can determine whether or not the promotion should be running, buyers can see when the promotion last ran, determine if the promotion will run on a specific date, and they can plan out their buying strategies based on when the promotion will run next.

Conclusion

As you can see, cron expressions are very useful for scheduling events. With the PHP Cron-Expression parser library, PHP developers now have access to the advanced scheduling capabilities of cron.

Comments