Repository navigation
Quick Start
From installation to a weekly meeting whose occurrences you can list, edit and cancel. Follow the steps in order in a Laravel 13 application; each step shows what you should see.
composer require boysfromthefactory/groundhogphp artisan vendor:publish --tag="groundhog-migrations"
php artisan migrateResult: two new tables, groundhog_recurrences (one rule per series) and groundhog_exclusions
(edited and cancelled occurrences). Occurrences themselves are never stored. Your own tables
need no new columns.
Upgrading from 0.1: run the same two commands. They publish a second migration that drops the
groundhog_occurrences table and the materialized_until column 0.1 created.
Optionally publish the configuration to change the defaults described in Configuration:
php artisan vendor:publish --tag="groundhog-config"Groundhog works with any model that has a start and an end column, named starts_at and
ends_at by default. If you do not have such a model yet, create one:
// database/migrations/2026_03_01_000000_create_meetings_table.php
Schema::create('meetings', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('location')->nullable();
$table->dateTime('starts_at');
$table->dateTime('ends_at');
$table->timestamps();
});Then add the HasRecurrence trait, and list recurrence_rule in $fillable:
<?php
namespace App\Models;
use BoysFromTheFactory\Groundhog\Concerns\HasRecurrence;
use Illuminate\Database\Eloquent\Model;
class Meeting extends Model
{
use HasRecurrence;
protected $fillable = ['title', 'location', 'starts_at', 'ends_at', 'recurrence_rule'];
protected function casts(): array
{
return [
'starts_at' => 'datetime',
'ends_at' => 'datetime',
];
}
}Run php artisan migrate again if you added the migration.
A record becomes a series when you give it a recurrence rule. Its own start and end are the first occurrence:
Meeting::create([
'title' => 'Standup',
'location' => 'Room A',
'starts_at' => '2026-03-02 09:00',
'ends_at' => '2026-03-02 10:00',
'recurrence_rule' => 'FREQ=WEEKLY;BYDAY=MO',
]);Result: one meetings row and its rule, "every Monday, 09:00–10:00, starting 2 March 2026".
Query the model exactly as you would any other:
$meetings = Meeting::whereBetween('starts_at', ['2026-03-01', '2026-03-31 23:59:59'])
->orderBy('starts_at')
->paginate(25);
$meetings->total(); // => 5
foreach ($meetings as $meeting) {
echo $meeting->starts_at->format('D j M H:i').'–'.$meeting->ends_at->format('H:i')
.' '.$meeting->title.', '.$meeting->location.PHP_EOL;
}
// Mon 2 Mar 09:00–10:00 Standup, Room A
// Mon 9 Mar 09:00–10:00 Standup, Room A
// Mon 16 Mar 09:00–10:00 Standup, Room A
// Mon 23 Mar 09:00–10:00 Standup, Room A
// Mon 30 Mar 09:00–10:00 Standup, Room AEach item is a full Meeting with the series' attributes and its own start and end. It is not
stored: $meeting->exists is false and $meeting->getKey() is null.
Change an occurrence and save it like any model:
$meeting = Meeting::whereBetween('starts_at', ['2026-03-16', '2026-03-16 23:59:59'])->sole();
$meeting->location = 'Room B';
$meeting->save();
$meeting->exists; // => true
$meeting->isOccurrenceException(); // => trueResult: the 16 March meeting is now stored as an exception of the series. The series itself and the other occurrences are unchanged.
Meeting::whereBetween('starts_at', ['2026-03-23', '2026-03-23 23:59:59'])->sole()->delete();Result: the 23 March meeting is cancelled. Nothing is stored for it; it simply stops being generated.
Run the query from step 5 again:
Meeting::whereBetween('starts_at', ['2026-03-01', '2026-03-31 23:59:59'])
->orderBy('starts_at')
->get()
->map(fn (Meeting $m) => $m->starts_at->format('j M').' '.$m->location.($m->exists ? ' (stored)' : ''));
// => ['2 Mar Room A', '9 Mar Room A', '16 Mar Room B (stored)', '30 Mar Room A']That is the whole cycle: one series, five generated occurrences, one edited, one cancelled, all through ordinary Eloquent calls.
- Declaring Recurring Models: other column names, models without an end, soft deletes
- Recurrence Rules: every way to write a rule, time zones, validation
- Querying Occurrences: open-ended rules, relationships, joins
- Editing Occurrences and Cancelling Occurrences: moving occurrences, events
- Managing a Series: what happens when the rule itself changes
Documents Groundhog v0.2.0 · Repository · README · Changelog