jmose/command-scheduler-bundle
Symfony bundle for scheduling console commands with cron expressions. Define jobs in YAML, run due tasks via a single scheduler command, and track execution status and logs. A lightweight alternative to system cron for Symfony applications.
Reality check first: this is a Symfony bundle, not a Laravel package. composer require jmose/command-scheduler-bundle will not boot inside a Laravel app — it relies on Symfony's bundle registration, Doctrine entity mapping, and its own console runner. A Laravel developer gets value from it in two cases only: (a) you maintain a Symfony service alongside your Laravel app, or (b) you want to copy its DB-driven scheduling model into Laravel. Laravel already ships an equivalent execution engine (schedule:run + dragonmantank/cron-expression); what it lacks natively is the admin-editable task table.
If you're on the Symfony side, minimal path (current release is 3.0.1, Symfony 5 / PHP >= 7.2):
composer require jmose/command-scheduler-bundle:"^3.0.1"
config/bundles.php: Jmose\CommandSchedulerBundle\CommandSchedulerBundle::class => ['all' => true].config/packages/command_scheduler.yaml (the bundle exposes entity-manager and log-path options; copy the sample from the README rather than guessing).scheduled_command table — generate a Doctrine migration or doctrine:schema:update --force in dev.config/routes.yaml (they land under a /command-scheduler-style prefix).* * * * * cd /var/www/app && php bin/console scheduler:run >/dev/null 2>&1
Where to look first: read two files and you understand the entire bundle — the ScheduledCommand entity (the schema: command, arguments, cron expression, priority, disabled, locked, last run, last return code, output/log path) and the scheduler:run command (selects due rows via cron-expression, executes, writes result back).
First use case: schedule something low-risk like a nightly cache:clear or a report command, add it through the admin UI (or scheduler:add), then force it once with scheduler:execute <id> to confirm the plumbing before trusting the cron.
The pattern worth stealing: DB-driven schedules. Code-defined schedules require a deploy to change; an ops team often wants to add "run this at 03:15" without one. The bundle's model:
command, arguments, cron_expression, priority, disabled, locked, last_run, last_return_code, log_fileCronExpression::isValidExpression() / isDue(), sorts by priority, takes a lock, runs, records exit code and outputPort to Laravel without leaving the framework:
// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
$tasks = Cache::remember('scheduled_tasks', 60, fn () =>
ScheduledTask::where('enabled', true)->orderBy('priority')->get()
);
foreach ($tasks as $task) {
$schedule->command($task->command, $task->arguments ?? [])
->cron($task->expression)
->withoutOverlapping($task->lock_minutes ?? 60)
->onOneServer()
->appendOutputTo(storage_path("logs/scheduled/{$task->id}.log"))
->onSuccess(fn () => $task->markSuccess())
->onFailure(fn () => $task->markFailure());
}
}
Key detail: in Laravel you do not add a second cron entry. php artisan schedule:run every minute is the dispatcher; the loop above just re-registers events each minute and Laravel evaluates isDue() itself.
The expression semantics now match. 3.0.0 replaced the abandoned mtdowling/cron-expression with dragonmantank/cron-expression — the exact library Laravel's scheduler uses. That removes a real porting hazard: an expression validated in the Symfony admin UI will now evaluate identically under Schedule::cron(), including the same edge cases (day-of-week/day-of-month OR semantics, @daily-style macros where supported). Don't rewrite expression handling separately for the two sides.
Admin panel analogue. The bundle's selling point is the UI. In Laravel that's a Filament/Nova resource over ScheduledTask — validate with CronExpression::isValidExpression($input) on save (otherwise one bad row throws every minute, forever), and flush scheduled_tasks from cache in the model's saved/deleted events.
Symfony-side day-to-day workflows (if you're actually operating this):
bin/console scheduler:add, passing the command name, cron expression, and flags for name/arguments/priority.scheduler:list; check lastReturnCode and the per-task log file rather than the runner's own exit status.scheduler:execute <id> — it bypasses the expression entirely, which is exactly what you want when the expression is the suspect.priority to order tasks that fall due in the same minute (e.g., migrations before cache warmers).Command::SUCCESS / Command::FAILURE) and run non-interactively; the admin panel only knows what the exit code tells it.\DateTimeInterface::ATOM. If you scrape the per-task log files into a dashboard, that format is now stable and parseable — but any regex written against the old default DateTime::__toString() output will break.Mixed-shop integration. If Symfony schedules and Laravel does the work, don't shell out to php artisan from a Symfony task — you inherit two environments' paths, PHP versions, and .env files. Expose a signed, idempotent HTTP endpoint in Laravel and have the Symfony task hit it, or consolidate to one scheduler per application.
^3.0.1, not ^3.0. v3.0.0 was pulled from Packagist almost immediately — its composer.json wrongly declared PHP >= 7.1 while the code needs 7.2, so a composer require on PHP 7.1 installed a broken combination. 3.0.1 corrects the constraint to PHP >= 7.2. If you have a lockfile or internal mirror that resolved 3.0.0, update deliberately.merge() removal), and missing return types. Pin versions or vendor a fork; don't assume master is maintained.crontab -l under the web/PHP user's account, and check the bundle's log path is writable by that user.locked flag set and the task never runs again. Check the lock state in the admin UI / locked column; ensure your own port uses withoutOverlapping() with a TTL longer than the longest job, backed by a lock-capable cache (redis/database, not array).date.timezone explicitly in php.ini and remember a 30 2 * * * task runs twice or not at all on DST boundaries.arguments field is a shell-ish string; nested quotes and = in values are a recurring source of "works manually, fails scheduled". In a Laravel port, store arguments as JSON and pass an array.> not >>) for chatty commands.scheduler:run succeeds if it ran; per-task health lives in last_return_code. Alert on that column, not on cron mail.Bottom line: 3.0.1 is a maintenance release — Symfony 5 support, the dragonmantank/cron-expression swap, and a corrected PHP floor. Nothing in it changes the verdict for a Laravel developer: still unusable directly in a Laravel app, still a single-maintainer bundle frozen in 2020, still capped at Symfony 5. Its real day-to-day value is the architecture: DB-backed task table, one-minute dispatcher, per-task locks and result columns. That's roughly 200 lines to reproduce on top of Illuminate\Console\Scheduling\Schedule — and since the bundle now validates expressions with the same library Laravel uses, the port is even more of a straight copy than before.
How can I help you explore Laravel packages today?