An example project that shows how to build a custom PHP attribute in Laravel: a #[Schedule] attribute you place on an Artisan command class so the command declares its own cron frequency, instead of declaring it in a central scheduling file or bootstrap/app.php.
- PHP 8.5+
- Composer
- Docker (for Laravel Sail)
- Node.js and npm (used by the
setupscript for asset compilation)
Five dummy Artisan commands live in app/Console/Commands/Scheduler/. They only print a line — what matters is how they get scheduled.
| Command | Cadence | Options |
|---|---|---|
shop:cancel-unpaid-orders |
daily | without overlapping, on one server |
shop:expire-coupons |
hourly at minute 5 | on one server |
shop:refresh-catalog-cache |
every 30 minutes | without overlapping, in background |
shop:report-low-stock |
daily at 08:00 Europe/Madrid | on one server |
shop:archive-delivered-orders |
cron 0 2 * * 1 |
without overlapping, on one server, production only |
The framework default. Every command is registered in a central withSchedule() closure:
->withSchedule(function (Schedule $schedule): void {
$schedule->command('shop:cancel-unpaid-orders')
->daily()
->withoutOverlapping()
->onOneServer();
$schedule->command('shop:archive-delivered-orders')
->cron('0 2 * * 1')
->withoutOverlapping()
->onOneServer()
->when(fn (): bool => app(ArchiveDeliveredOrdersCommand::class)->shouldArchive());
// ...
})It works, but:
- The cadence lives far from the command it belongs to — you read the class and still don't know when it runs.
- The file grows with every new command and becomes a merge-conflict magnet.
- Deleting a command leaves an orphan registration behind.
- The signature is repeated as a string, so a rename silently breaks the schedule.
The command declares its own cadence, and App\Providers\ScheduleAttributeServiceProvider discovers it by reflecting over Artisan::all():
#[Signature('shop:cancel-unpaid-orders')]
#[Schedule(
frequency: 'daily',
withoutOverlapping: true,
onOneServer: true,
)]
final class CancelUnpaidOrdersCommand extends Command { /* ... */ }App\Attributes\Schedule accepts a frequency method name (daily, dailyAt, …) with args, or a raw cron expression — anything containing a space is treated as cron. withoutOverlapping, onOneServer, runInBackground, timezone and when (a method name on the command) map to the matching Event calls. The attribute is repeatable, so a command can run on several cadences.
php artisan schedule:list prints the same five entries on both branches — that equality is the point of the example.
This project is open-sourced software licensed under the MIT license.