Dealing with scheduling in PHP

A common requirement for web applications is the ability to schedule something for the future. By this I mean, calculate when the next time something should happen.

For example, if you want a round up email to be delivered on a particular day of the week, or if you need to notify your users about something after a predetermined amount of time.

In today’s tutorial I’m going to be looking at calculating that amount of time, rather than the technical aspects of using a Cron Job or a Message Queue.

Simple addition

The easiest way to schedule something for the future is to simply add to an existing date time object.

For example, if you are using Carbon you could do this:

$now = Carbon::now();

$now->addWeek()  

This would grab the current date and time, and then add 1 week.

Carbon has a number of helpful methods for adding to a timestamp:

$now->addSeconds(30);  
$now->addMinutes(3);  
$now->addHours(2);  
$now->addDays(5);  
$now->addWeeks(3);  
$now->addMonths(11);  
$now->addYears(23);  

I would say that for most internal use cases, simply adding a predetermined amount to a date time object is probably going to cover your requirements

The problem occurs when you need to allow the user to set the time in which they want to be reminded.

Date Time Durations

When you allow the user to choose their own schedule, things can get a bit more tricky.

We need a way to allow user input through a form, and a way to store that data.

For example, if we allow the user to choose options of “every day”, “every 2 days”, or “every 7 days”, how would we store that?

Storing 1 day, 2 days, etc and then passing to a Carbon instance is nasty.

Fortunately, this is already a solved problem!

Under ISO 8601 there is a specification for durations (see here). This defines a formula for representing durations.

PHP has a DateInterval class for working with this specification.

For example, the following would represent 1 day

$interval = new DateInterval("P1D")  

Now that we have this DateInterval object we can pass that to a DateTime object to make the calculation:

$now = Carbon::now();

$now->add($interval);  

Storing the duration as a formula that matches the specification of ISO 8601 is much better than storing some home cooked way because it is entirely clear what the string represents.

This means we already have an international standard for expressing every type of duration that we might need. This is great because we are not defining our own formulas so any developer should be able to interpret the value without having to decipher what it means.

In your code you can also simply pass the string to the DateInterval object without having to do any type of parsing.

Date Time Intervals

A second thing you might want to do could be to calculate a particular set of dates between a start and an end at a given interval.

For example, if you wanted to grab the date of every 7 days for a particular year, you could use the DatePeriod class:

$start = new DateTime("2015-01-01");  
$end = new DateTime("2015-12-31");  
$interval = new DateInterval("P7D");  
$period = new DatePeriod($start, $interval, $end);  

This will give you a traversable list of DateTime objects at an interval of 7 days for the whole of 2015.

Recurrence Rules

Finally, sometimes you will want to give you users the options to specify recurring rules.

For example, if you were building a project management application, or a calendar application, this would be pretty much a certain requirement.

Fortunately, we already have a way to define these rules in the good old Internet Calendaring and Scheduling Core Object Specification.

This specification allows you to define a formula for recurring dates.

To get a better sense of this, take a look at this javascript demo that allows you to create recurrence rules.

As you can see, by defining the rule as a formula, we have an incredibly rich method of describing exactly what we want, and we have a universally accepted way of storing it.

PHP doesn’t have an inbuilt way of dealing with these rules, and so we need to turn to the wonderful world of open source, and in particular simshaun/recurr.

Here is an example of using this package

use Recurr\Rule;  
use Recurr\Transformer\ArrayTransformer;

$start = new Carbon("2015-01-01");  
$end = new Carbon("2015-12-31");  
$rules = new Rule("FREQ=WEEKLY;COUNT=30;WKST=MO", $start, $end);  
$transformer = new ArrayTransformer;  
$collection = $transformer->transform($rule);  

This will give you a collection of objects that represent the defined recurrence rule.

Conclusion

Calculating dates into the future is usually straight forward for internal processes. If you want to schedule something for the future you typically want to do it for a predefined space of time.

The tricky bit is when it comes to allowing your users to define their own schedules. A user defined schedule is never going to be straight forward.

Fortunately for us we don’t need to define our own formula for storing custom schedules, durations, or intervals. We can use recognised international standards that have already been defined to cover almost all edge cases.

You should always try to use recognised standards, rather than trying to invent the wheel on your own. These standards are usually well written and have taken into account a whole load of situations and edge cases you probably won’t think of for a long time.