Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

When Laravel Package

tplaner/when

tplaner/when is a PHP library for recurring dates and times. It generates schedules, calculates occurrences, and supports natural-language recurrence rules, making it useful for calendars, reminders, bookings, and event planning.

View on GitHub
Deep Wiki
Context7

Getting Started

tplaner/when is a standalone PHP library — there is no service provider, facade, or config file. Install it and use it directly:

composer require tplaner/when

First place to look is the README's fluent-API table: When::every($n)->{unit}() kicks off a rule, and you finish with a range (from()/to()/between()) plus a terminal call (getDates(), count(), getNextOccurrence()).

The canonical first use case — "list every second Tuesday for the next 6 months":

use When\When;

$dates = When::every(2)
    ->weeks()
    ->on(When::TUESDAY)
    ->from(now()->startOfDay()->toDateTime())
    ->to(now()->addMonths(6)->endOfDay()->toDateTime())
    ->getDates();          // array of \DateTime

For a Laravel app, drop that into a small App\Support\Recurrence wrapper (see below) rather than calling When straight from controllers — the fluent chain is easy to read but noisy to repeat, and you'll want a single place to normalise timezones and convert to Carbon.

Implementation Patterns

1. Wrap the fluent chain in a value object / service. Keeps controllers thin and gives you one seam for Carbon conversion and timezone policy:

namespace App\Support;

use When\When;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;

class RecurrenceRule
{
    public function __construct(
        protected int $interval,
        protected string $unit,
        protected array $constraints = [],
    ) {}

    public function occurrences(Carbon $from, Carbon $to): Collection
    {
        $when = When::every($this->interval)->{$this->unit}();

        foreach ($this->constraints as $method => $args) {
            $when->{$method}(...$args);
        }

        return collect(
            $when->from($from->toDateTime())->to($to->toDateTime())->getDates()
        )->map(fn (\DateTime $d) => Carbon::instance($d));
    }
}

Bind a factory in a service provider so app(RecurrenceFactory::class) builds rules from a stored RRULE or JSON payload.

2. Store the rule, materialise the dates. The most common production pattern is: a recurring_events table holds the rule (RRULE string or JSON of {interval, unit, constraints}), and a scheduled command writes concrete rows into an occurrences table for the next N days. Querying a real table beats computing getDates() on every request, and it lets you cancel a single instance.

// app/Console/Commands/MaterialiseOccurrences.php
foreach (RecurringEvent::where('active', true)->cursor() as $event) {
    $dates = $event->rule()->occurrences(now(), now()->addDays(90));

    Occurrence::upsert(
        $dates->map(fn ($d) => ['event_id' => $event->id, 'starts_at' => $d])->all(),
        ['event_id', 'starts_at'],
        []
    );
}

3. Feed it RRULE strings. When accepts iCal recurrence strings via recur(), which is the pragmatic bridge for calendar sync (Google Calendar, Outlook, ICS imports):

$when = new When();
$when->recur('FREQ=MONTHLY;BYDAY=1MO,3WE;COUNT=10');
$dates = $when->getDates();

Pair this with a validator (or spatie/regex-style rule) that rejects anything that doesn't parse, and store the original string so imports can be re-synced.

4. Use it for "next N occurrences" endpoints. getNextOccurrence() (or slicing getDates()) is ideal for a GET /events/next style route; cache the result keyed on the rule hash + date bucket with Cache::remember, since recurrence maths is deterministic.

5. Validate and preview on write. In the FormRequest that saves a recurring event, run the rule over a 60-day window and return the preview inline. This catches impossible rules (e.g. nth(When::FIFTH, ...) in a month that has four) before they reach the database.

6. Exclusion sets. Use notOn() / ifIs() / exclude() to skip holidays or blackout dates, or — usually cleaner — filter the resulting collection against an excluded_dates pivot after generation.

Gotchas and Tips

Never call getDates() without a bound. An every(1)->days() rule with no to() will happily try to generate forever. Always pair with ->to(...), ->between(...), or the count parameter of an RRULE. If you accept ranges from user input, clamp them (e.g. max 2 years) server-side.

Results are \DateTime[], not Carbon. Convert with Carbon::instance($dt) so you get ->diffForHumans(), ->isoFormat(), and serialisation for free. Don't Carbon::parse($dt) — you'll drop the timezone the library attached.

Timezone drift is the #1 bug source. When works on \DateTime objects and preserves their timezone. If you build from now() (app timezone) and then store in UTC, you'll get off-by-hours occurrences. Pick one policy — build the rule in the app timezone, then ->setTimezone('UTC') at the persistence boundary — and apply it inside your wrapper, not at each call site.

on() is overloaded and silently ambiguous: on(When::TUESDAY) = weekday, on(6, 15) = month/day, on(2025, 6, 15) = exact date. Arity is the only discriminator. Wrap these in named methods (->onWeekday(), ->onMonthDay()) in your own layer to keep call sites legible.

nth() vs firstOfMonth()/lastOfMonth(). ->nth(When::THIRD, When::FRIDAY) gives "third Friday"; ->lastOfMonth() gives the final day of the month regardless of weekday. Mixing them up produces rules that look plausible and are six weeks wrong.

Fifth weekday rules may produce nothing. In months without a fifth occurrence the rule yields no date — that's correct iCal behaviour, but your UI needs a fallback ("last Friday of the month") and your materialiser must tolerate empty arrays.

Performance. getDates() is O(iterations) and does date arithmetic in PHP, not SQL. Generating 5 years of daily occurrences is fine; 20 years of hourly is not. Push long-horizon work onto a queued job, and consider chunking by year rather than one giant call.

The library is old and unmaintained-feeling. Expect PHP 8.2+ deprecation notices (dynamic properties, implicit nullable params) from When internals. If you're on a recent PHP, pin the version, run your test suite with E_ALL, and consider suppressing deprecations for the When\ namespace in phpunit.xml rather than patching vendor code.

No iCal export. When parses RRULE but won't emit ICS. For calendar feeds you'll need sabre/vobject or eluceo/ical; store the RRULE your users author so it round-trips cleanly between the two.

Debugging tip: don't debug from a controller. Run the rule in php artisan tinker with a tight from/to window and dump count() before getDates() — if count() blows up you have an unbounded or impossible rule, not a formatting problem.

Testing: When ultimately reads the clock through the \DateTime you pass in, so Carbon::setTestNow() works only if you're careful to use now() for the bounds. Build a small dataset of known-good rules (2nd Tuesday, last weekday of month, every 3rd day) as a Pest dataset, and assert on ISO strings, not DateTime equality — it catches timezone regressions cheaply.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource