A scheduling and occurrence engine for Laravel.
This package is not a calendar UI. It handles the domain model behind recurring events: defining schedules as RRULE strings, pre-generating concrete occurrence records, and giving you a clean query interface to drive any calendar or scheduling feature you build.
- PHP 8.2+
- Laravel 10, 11, or 12
composer require crumbls/timeline
php artisan vendor:publish --tag=timeline-migrations
php artisan migrateEvent — the conceptual thing. "Laravel Meetup" or "Board Meeting". Not a specific date.
Rule — defines when an Event occurs. Stores an RFC 5545 RRULE string. One Event can have multiple Rules.
Occurrence — a concrete scheduled instance. "Laravel Meetup on June 5th." This is the primary query model.
OccurrenceException — a one-off modification to a single Occurrence (cancel, reschedule, modify). Never modifies the parent Rule.
Location — a reusable venue attached to Occurrences.
Writing RRULE strings by hand is error-prone. Use the fluent builder:
use Crumbls\Timeline\Support\RRuleBuilder;
// Every Tuesday
RRuleBuilder::make()->weekly()->onDays('TU')->toString();
// First Friday of the month
RRuleBuilder::make()->monthly()->onNthWeekday(1, 'FR')->toString();
// Every other Monday, 20 times
RRuleBuilder::make()->weekly()->every(2)->onDays('MO')->count(20)->toString();
// Human-readable description of any RRULE
RRuleBuilder::describe('FREQ=WEEKLY;BYDAY=MO,WE,FR');
// "weekly on Monday, Wednesday and Friday"See IMPLEMENTATION.md for the full builder reference.
use Crumbls\Timeline\Models\Event;
use Crumbls\Timeline\Models\Rule;
use Crumbls\Timeline\Enums\EventStatus;
// Create an event
$event = Event::create([
'name' => 'Laravel Meetup',
'timezone' => 'America/Denver',
'status' => EventStatus::Published,
]);
// Add a weekly rule — occurrences generate automatically
Rule::create([
'event_id' => $event->id,
'starts_at' => '2025-06-03 18:00:00',
'ends_at' => '2025-06-03 20:00:00',
'rrule' => 'FREQ=WEEKLY;BYDAY=TU',
]);
// Query occurrences
use Crumbls\Timeline\Models\Occurrence;
use Carbon\Carbon;
$feed = Occurrence::between(Carbon::parse('2025-06-01'), Carbon::parse('2025-06-30'))
->scheduled()
->with(['event', 'location'])
->orderBy('starts_at')
->get();Occurrence::upcoming()->get();
Occurrence::today()->get();
Occurrence::between($start, $end)->get();
Occurrence::scheduled()->get();
Occurrence::forEvent($event->id)->get();
Occurrence::atLocation($location->id)->get();Scopes chain freely:
Occurrence::scheduled()->upcoming()->forEvent($event->id)->paginate(20);// config/timeline.php
return [
'table_prefix' => 'timeline_',
'occurrence_generation_months' => 12,
'default_timezone' => 'UTC',
];All table names are resolved through table_prefix. Change it before running migrations.
When a Rule is saved, GenerateOccurrencesJob is dispatched. The OccurrenceGenerator service expands the RRULE into concrete dates for the configured window (default: 12 months), then:
- Creates new Occurrence records for dates not already present
- Updates
ends_aton existing records if the duration changed - Deletes future
Scheduledoccurrences no longer in the expansion - Leaves
CancelledandCompletedoccurrences untouched
You can also trigger generation manually:
use Crumbls\Timeline\Services\OccurrenceGenerator;
app(OccurrenceGenerator::class)->generate($rule);composer test56 Pest tests covering events, rules, occurrences, generator logic, and exceptions.
See IMPLEMENTATION.md for a full implementation guide including calendar feed construction, location usage, exception handling, queue setup, and an RRULE reference table.
MIT