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.
- PHP >= 8.2
- Laravel >= 11.0
You can install the package via composer:
composer require directorytree/cadenceThen, 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/recurrPublish and run the migrations:
php artisan vendor:publish --provider="DirectoryTree\Cadence\CadenceServiceProvider"
php artisan migrateThis 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
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;
}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'));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);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.
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.
Cadence uses a driver-based architecture. Drivers are automatically registered when their backing library is installed.
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 9amRequires 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');Requires simshaun/recurr:
use DirectoryTree\Cadence\Drivers\RecurrSchedule;
new RecurrSchedule('FREQ=WEEKLY;BYDAY=MO,WE,FR');
new RecurrSchedule('FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1');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);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);