If you have ever had to implement recurring events in PHP, you know it is one of those features that sounds simple until you are knee deep in date math, timezone edge cases, and the realization that "every last Friday of the month" is genuinely hard to compute.

simshaun/recurr is the library I wish I had known about sooner. It handles the full iCalendar RRULE spec, turning recurrence rules into collections of DateTime objects you can actually work with.

What it does

Recurr takes an RRULE string (like FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10) and a start date, and generates all the matching dates. It also works the other way: build your rule programmatically and get the RRULE string out.

$rule = (new \Recurr\Rule)
    ->setStartDate(new \DateTime('2024-01-01'))
    ->setTimezone('Europe/Brussels')
    ->setFreq('WEEKLY')
    ->setByDay(['MO', 'WE', 'FR'])
    ->setCount(10);

echo $rule->getString();
// FREQ=WEEKLY;COUNT=10;BYDAY=MO,WE,FR

Once you have a rule, the ArrayTransformer converts it to a RecurrenceCollection:

$transformer = new \Recurr\Transformer\ArrayTransformer();
$occurrences = $transformer->transform($rule);

foreach ($occurrences as $occurrence) {
    echo $occurrence->getStart()->format('Y-m-d') . PHP_EOL;
}

Each item has getStart() and getEnd() methods that return DateTime objects. If the rule has no end date, getEnd() returns the same value as getStart().

There is also a built-in virtual limit of 732 occurrences by default. This prevents the transformer from looping forever on an open-ended rule. You can adjust it via ArrayTransformerConfig.

Constraints

The constraint system is what I find particularly well designed. You can limit which dates make it into the collection without touching the rule itself:

$constraint = new \Recurr\Transformer\Constraint\BetweenConstraint(
    new \DateTime('2024-03-01'),
    new \DateTime('2024-06-01')
);

$occurrences = $transformer->transform($rule, $constraint);

There is also AfterConstraint and BeforeConstraint. Clean separation between what the rule defines and what you actually want to display.

Post-transformation filters

RecurrenceCollection extends Doctrine's ArrayCollection and ships chainable helper methods for filtering after the fact:

$occurrences
    ->startsBetween(new \DateTime('2024-04-01'), new \DateTime('2024-05-01'))
    ->endsAfter(new \DateTime('2024-04-15'));

Useful when you already have a collection and want to slice it for display purposes, without re-running the transformer.

RRULE to human-readable text

Recurr ships a TextTransformer that converts a rule into a readable sentence. It supports multiple locales too:

$rule = new \Recurr\Rule('FREQ=YEARLY;INTERVAL=2;COUNT=3;', new \DateTime());

$textTransformer = new \Recurr\Transformer\TextTransformer(
    new \Recurr\Transformer\Translator('fr')
);

echo $textTransformer->transform($rule);
// toutes les 2 annees, 3 fois

Handy when you need to surface the recurrence description to end users without building your own translation layer.

Getting started

composer require simshaun/recurr

No unusual dependencies. Works with PHP 7.4+.

If you are building anything involving calendars, bookings, or scheduled events in PHP, Recurr handles the hard part of the problem cleanly. The RRULE spec is complex enough that rolling your own is a bad idea. This library has been around since 2013 and has 1,600+ stars for a reason.