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.
Install the package and dependencies:
composer require directorytree/cadence dragonmantank/cron-expression
(Choose either rlanvin/php-rrule or simshaun/recurr for RRULE support.)
Publish migrations and run them:
php artisan vendor:publish --provider="DirectoryTree\Cadence\CadenceServiceProvider"
php artisan migrate
Implement the Schedulable interface on a model:
use DirectoryTree\Cadence\HasSchedules;
use DirectoryTree\Cadence\Schedulable;
class Report extends Model implements Schedulable
{
use HasSchedules;
}
Add a schedule to a model instance:
$report = Report::find(1);
$report->addSchedule(new \DirectoryTree\Cadence\Drivers\CronSchedule('0 12 * * *'));
$report->save();
Set up the schedules:run command in routes/console.php:
Schedule::command('schedules:run')->everyMinute();
Listen for triggered schedules:
// In EventServiceProvider or via event discovery
ScheduleTriggered::listen(function ($event) {
$event->schedulable->performAction();
});
Example: Schedule a daily report generation at noon for a specific Report model.
$report = Report::create(['name' => 'Daily Sales']);
$report->addSchedule(new \DirectoryTree\Cadence\Drivers\CronSchedule('0 12 * * *'));
$report->save();
// Later, in a listener:
ScheduleTriggered::listen(function ($event) {
if ($event->schedulable instanceof Report) {
$event->schedulable->generate();
}
});
Define Schedules: Attach schedules to models during creation or via admin interfaces. Use cron for simplicity or RRULE for complex patterns.
// Simple cron example
$user->addSchedule(new \DirectoryTree\Cadence\Drivers\CronSchedule('0 9 * * 1-5')); // Weekdays at 9 AM
// Complex RRULE example
$campaign->addSchedule(new \DirectoryTree\Cadence\Drivers\RruleSchedule(
'FREQ=MONTHLY;BYMONTHDAY=15;BYDAY=MO'
));
Timezone Handling: Always specify timezones for schedules to avoid ambiguity, especially in global apps.
$schedule = new \DirectoryTree\Cadence\Drivers\CronSchedule('0 9 * * *', 'America/New_York');
$user->addSchedule($schedule);
Event-Driven Execution:
Use ScheduleTriggered events to decouple scheduling from business logic. Queue listeners for async processing.
ScheduleTriggered::listen(function ($event) {
dispatch(new GenerateReportJob($event->schedulable));
});
Dynamic Scheduling: Update schedules programmatically (e.g., pause/resume during maintenance).
$schedule->disable(); // Temporarily pause
$schedule->enable(); // Resume
Batch Processing:
Run the schedules:run command in a queue worker or serverless environment (e.g., AWS Lambda) for scalability.
php artisan schedules:run
ScheduleTriggered events in unit tests:
$this->fake(ScheduleTriggered::class);
$this->assertFired(function ($event) {
return $event->schedulable->id === 1;
});
ScheduleTriggered::listen(function ($event) {
\Log::info('Schedule triggered', ['model' => $event->schedulable->class]);
});
class OrderFollowUpSchedule extends \DirectoryTree\Cadence\Drivers\Schedule
{
protected function resolveNextOccurrence(CarbonInterface $after): ?CarbonInterface
{
return $after->copy()->addDays(7);
}
}
Timezone Mismatches:
Carbon::now('America/New_York') for consistency.next_run_at in the database to verify timezone handling.Overlapping Schedules:
withoutOverlapping() in routes/console.php to prevent race conditions:
Schedule::command('schedules:run')->withoutOverlapping()->everyMinute();
RRULE Complexity:
try {
new \DirectoryTree\Cadence\Drivers\RruleSchedule('INVALID_RRULE');
} catch (\Exception $e) {
// Handle error
}
Database Locking:
schedules:run executions may cause deadlocks when updating next_run_at.$schedule->newQuery()->lockForUpdate()->update(['next_run_at' => $nextRun]);
Event Listener Order:
EventServiceProvider or use event discovery.next_run_at:
Query the schedules table to verify next_run_at values match expectations:
SELECT * FROM schedules WHERE schedulable_id = 1;
ScheduleTriggered listeners to trace execution:
ScheduleTriggered::listen(function ($event) {
\Log::debug('Schedule triggered', [
'model' => $event->schedulable->class,
'schedule_type' => $event->schedule->type,
'next_run' => $event->schedule->next_run_at,
]);
});
now():
Override Carbon’s now() in tests to simulate time progression:
use Carbon\Carbon;
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2023-01-01 12:00:00'));
});
Driver Registration:
dragonmantank/cron-expression, rlanvin/php-rrule, or simshaun/recurr).AppServiceProvider:
\DirectoryTree\Cadence\Schedule::driver('custom', \App\Drivers\CustomSchedule::class);
Migration Updates:
disabled_at column in v1.1.0):
php artisan migrate
Timezone Defaults:
$schedule->setTimezone('UTC');
Custom Drivers:
Extend \DirectoryTree\Cadence\Drivers\Schedule to create domain-specific logic:
class BusinessDaySchedule extends \DirectoryTree\Cadence\Drivers\Schedule
{
protected function resolveNextOccurrence(CarbonInterface $after): ?CarbonInterface
{
$next = $after->copy();
while (!$next->isBusinessDay()) { // Custom logic
$next->addDay();
}
return $next;
}
}
Event Customization:
Override the ScheduleTriggered event to add metadata:
class CustomScheduleTriggered extends \DirectoryTree\Cadence\Events\ScheduleTriggered
{
public function broadcastOn()
{
return [];
}
}
Query Scoping: Add global scopes to filter schedules (e.g., by model type):
use Illuminate\Database\Eloquent\Builder;
class ScheduleScope
{
public function
How can I help you explore Laravel packages today?