directorytree/cadence
Cadence adds model-based scheduling to Laravel. Attach one or more cron or RRULE schedules to any Eloquent model, track due runs, and dispatch events when schedules trigger. Driver-based design supports cron, php-rrule, Recurr, or custom drivers.
composer require directorytree/cadence dragonmantank/cron-expression
php artisan vendor:publish --provider="DirectoryTree\Cadence\CadenceServiceProvider"
php artisan migrate
HasSchedules trait to a model (e.g., Report):
use DirectoryTree\Cadence\HasSchedules;
use DirectoryTree\Cadence\Schedulable;
class Report extends Model implements Schedulable
{
use HasSchedules;
}
$report = Report::find(1);
$report->addSchedule(new \DirectoryTree\Cadence\Drivers\CronSchedule('0 12 * * *'));
$report->save();
schedules:run command in routes/console.php:
Schedule::command('schedules:run')->everyMinute();
// app/Listeners/GenerateReport.php
public function handle(ScheduleTriggered $event)
{
$event->schedulable->generate();
}
// Schedule a weekly report at 8 AM
$report->addSchedule(new CronSchedule('0 8 * * 1'));
$schedule = new CronSchedule('0 9 * * *', 'America/New_York');
$user->addSchedule($schedule);
$schedule = new RruleSchedule('FREQ=MONTHLY;BYDAY=FR;BYSETPOS=3');
$campaign->addSchedule($schedule);
php-rrule or Recurr libraries (choose based on feature needs).ScheduleTriggered events:
// app/Listeners/ProcessOrder.php
public function handle(ScheduleTriggered $event)
{
if ($event->schedulable instanceof Order) {
$event->schedulable->processPayment();
}
}
ShouldQueue listeners).// app/Models/User.php
public function boot()
{
static::created(function ($user) {
$user->addSchedule(new CronSchedule('0 10 * * *', $user->timezone));
});
}
class BusinessDaySchedule extends Schedule
{
protected function resolveNextOccurrence(CarbonInterface $after): ?CarbonInterface
{
$next = $after->copy()->addDays(3);
return $next->isWeekend() ? $next->addDays(2) : $next;
}
}
AppServiceProvider:
\DirectoryTree\Cadence\Schedule::driver('business_day', BusinessDaySchedule::class);
// app/Models/Newsletter.php
public function addDefaultSchedules()
{
$this->addSchedule(new CronSchedule('0 7 * * 1', 'America/Chicago')); // Weekly at 7 AM CT
$this->addSchedule(new RruleSchedule('FREQ=DAILY;BYHOUR=12')); // Daily at noon UTC
}
php artisan schedules:run
(Or let Laravel’s scheduler handle it automatically.)php artisan tinker
>>> $user = User::find(1);
>>> $user->schedules()->first()->next_run_at
php artisan schedules:run to simulate due schedules.ScheduleTriggered events fire correctly.schedules:run is registered in routes/console.php and scheduled (e.g., every minute).ScheduleTriggered listener).phpunit.xml:
<env name="DB_CONNECTION" value="sqlite_memory"/>
php artisan migrate --env=testing
// tests/Feature/SchedulesTest.php
public function test_schedule_triggers()
{
$user = User::factory()->create();
$user->addSchedule(new CronSchedule('* * * * *')); // Every minute for testing
$this->artisan('schedules:run')
->expectsOutput('Schedule triggered');
$this->assertDatabaseHas('schedules', [
'schedulable_id' => $user->id,
'next_run_at' => now()->addMinute(),
]);
}
ShouldQueue listeners to offload work to queues:
public function handle(ScheduleTriggered $event)
{
dispatch(new ProcessSchedule($event->schedulable, $event->schedule));
}
$user->addSchedule(new CronSchedule('0 9 * * *', $user->timezone));
config('app.timezone')).// app/Http/Controllers/SchedulesController.php
public function update(UpdateScheduleRequest $request, $id)
{
$model = $this->findModel($request->type, $id);
$model->updateSchedule($request->expression, $request->timezone);
return response()->json(['success' => true]);
}
schedules table to support soft deletes:
// app/Models/Schedule.php
use Illuminate\Database\Eloquent\SoftDeletes;
class Schedule extends Model
{
use SoftDeletes;
}
schedules:run:
// app/Console/Commands/RunSchedules.php
$schedules = Schedule::whereNull('deleted_at')->get();
ScheduleTriggered events in tests:
$this->fake(ScheduleTriggered::class);
$this->artisan('schedules:run');
$this->assertNotFaked(ScheduleTriggered::class);
travel() to test time-based logic:
use Carbon\Carbon;
Carbon::setTestNow(Carbon::now()->addDay());
$this->artisan('schedules:run');
next_run_at Inaccuracyschedules:run is not executed frequently enough, next_run_at may become stale.schedules:run every minute (or more often for high-frequency schedules). Use withoutOverlapping() to avoid duplicate runs:
Schedule::command('schedules:run
How can I help you explore Laravel packages today?