directorytree / cadence
Model-based scheduling for Laravel
Fund package maintenance!
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- dragonmantank/cron-expression: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^8.22|^9.0|^10.0|^11.0
- pestphp/pest: ^2.0|^3.0|^4.0
- pestphp/pest-plugin-arch: ^2.0|^3.0|^4.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0|^4.0
- rlanvin/php-rrule: ^2.0
- simshaun/recurr: ^5.0
- spatie/laravel-ray: ^1.35
Suggests
- dragonmantank/cron-expression: Required to use the CronSchedule driver (^3.0).
- rlanvin/php-rrule: Required to use the RruleSchedule driver (^2.0).
- simshaun/recurr: Required to use the RecurrSchedule driver (^5.0).
This package is auto-updated.
Last update: 2026-05-05 13:03:41 UTC
README
Model-based scheduling for Laravel.
Cadence provides a driver-based scheduling system for your Eloquent models using cron expressions or RRULE recurrence patterns. Attach one or many schedules to any model, and Cadence will track and dispatch events when they're due.
Index
Requirements
- PHP >= 8.2
- Laravel >= 11.0
Installation
You can install the package via composer:
composer require directorytree/cadence
Then, install at least one schedule driver:
# Cron (recommended for simple schedules) composer require dragonmantank/cron-expression # RRULE via php-rrule composer require rlanvin/php-rrule # RRULE via Recurr composer require simshaun/recurr
Publish and run the migrations:
php artisan vendor:publish --provider="DirectoryTree\Cadence\CadenceServiceProvider"
php artisan migrate
This creates a schedules table with the following columns:
schedulable_type/schedulable_id— polymorphic relation to your modeltype— the driver type (e.g.cron,rrule,recurr)expression— the schedule expressiontimezone— optional timezone for the schedulenext_run_at— precomputed next occurrence for efficient queryinglast_run_at— timestamp of the last run
Setup
Implement the Schedulable interface and use the HasSchedules trait on any model you want to schedule:
// app/Models/Report.php namespace App\Models; use DirectoryTree\Cadence\HasSchedules; use DirectoryTree\Cadence\Schedulable; use Illuminate\Database\Eloquent\Model; class Report extends Model implements Schedulable { use HasSchedules; }
Usage
Adding Schedules
Create a driver instance and add it to your model:
use DirectoryTree\Cadence\Drivers\CronSchedule; $report = Report::find(1); // Every day at noon $report->addSchedule(new CronSchedule('0 12 * * *')); // Every Monday at 9am $report->addSchedule(new CronSchedule('0 9 * * 1'));
With RRULE expressions:
use DirectoryTree\Cadence\Drivers\RruleSchedule; // Every weekday $report->addSchedule(new RruleSchedule('FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR')); // Monthly on the 15th, starting from a specific date $report->addSchedule(new RruleSchedule('DTSTART=20260101T000000;FREQ=MONTHLY;BYMONTHDAY=15'));
Timezones
Schedules can be timezone-aware. Pass the timezone as the second argument:
// Every day at 9am Eastern $report->addSchedule(new CronSchedule('0 9 * * *', 'America/New_York')); // Or set it via method $driver = new CronSchedule('0 9 * * *'); $driver->setTimezone('America/New_York'); $report->addSchedule($driver);
Running Due Schedules
Register the schedules:run command in your application's scheduler to run every minute:
// routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('schedules:run') ->withoutOverlapping() ->everyMinute();
This command queries all schedules where next_run_at <= now(), dispatches a ScheduleTriggered event for each, and advances next_run_at to the next occurrence.
Listening for Triggered Schedules
Listen for the ScheduleTriggered event to perform work when a schedule fires:
// app/Listeners/HandleScheduleTriggered.php namespace App\Listeners; use DirectoryTree\Cadence\Events\ScheduleTriggered; class HandleScheduleTriggered { public function handle(ScheduleTriggered $event): void { $schedule = $event->schedule; // Access the parent model $model = $schedule->schedulable; // Perform work based on the model type if ($model instanceof \App\Models\Report) { $model->generate(); } } }
Register it in your EventServiceProvider or use event discovery.
Drivers
Cadence uses a driver-based architecture. Drivers are automatically registered when their backing library is installed.
Cron
Requires dragonmantank/cron-expression:
use DirectoryTree\Cadence\Drivers\CronSchedule; new CronSchedule('0 12 * * *'); // Every day at noon new CronSchedule('*/15 * * * *'); // Every 15 minutes new CronSchedule('0 9 * * 1-5'); // Weekdays at 9am
RRULE (php-rrule)
Requires rlanvin/php-rrule:
use DirectoryTree\Cadence\Drivers\RruleSchedule; new RruleSchedule('FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR'); new RruleSchedule('FREQ=MONTHLY;BYMONTHDAY=1;COUNT=12');
RRULE (Recurr)
Requires simshaun/recurr:
use DirectoryTree\Cadence\Drivers\RecurrSchedule; new RecurrSchedule('FREQ=WEEKLY;BYDAY=MO,WE,FR'); new RecurrSchedule('FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1');
Custom Drivers
Create a class that extends the base Schedule driver:
namespace App\Drivers; use Carbon\CarbonInterface; use DirectoryTree\Cadence\Drivers\Schedule; class CustomSchedule extends Schedule { protected function resolveNextOccurrence(CarbonInterface $after): ?CarbonInterface { // Your recurrence logic here } }
Then register it in your AppServiceProvider:
use App\Drivers\CustomSchedule; use DirectoryTree\Cadence\Schedule; Schedule::driver('custom', CustomSchedule::class);
Customizing Drivers
Each driver exposes a static tap method to configure the underlying library instance before it's used:
use Cron\CronExpression; use DirectoryTree\Cadence\Drivers\CronSchedule; CronSchedule::tap(function (CronExpression $cron) { // Configure the CronExpression instance });
use Recurr\Rule; use Recurr\Transformer\ArrayTransformer; use Recurr\Transformer\ArrayTransformerConfig; use DirectoryTree\Cadence\Drivers\RecurrSchedule; RecurrSchedule::tap(function (Rule $rule, ArrayTransformer $transformer) { $transformer->setConfig( (new ArrayTransformerConfig)->enableLastDayOfMonthFix() ); });
Pass null to clear the callback:
RecurrSchedule::tap(null);