whatwedo/cron-bundle
A Symfony bundle for managing scheduled cron jobs in code. Define recurring tasks and run console commands on configurable schedules, keeping cron configuration versioned and integrated with your Symfony application.
Install and register
composer require whatwedo/cron-bundle
On Symfony Flex the bundle is auto-registered; otherwise add it to config/bundles.php. Then look at bin/console list whatwedo (or debug:container --tag=…) to confirm the bundle's runner and list commands are available — that tells you the exact command names for the version you pinned.
Where to look first
#[CronJob('*/5 * * * *')]).CronManager-style service and the console command that fans jobs out — this is the actual dispatcher.config/packages/whatwedo_cron.yaml.The core mental model (vs. Laravel)
Laravel's Kernel::schedule() is pull: one system cron hits schedule:run every minute and the framework decides what's due. This bundle is the same idea for Symfony: you keep one system crontab entry pointing at the bundle's run command, and schedule individual Symfony console commands declaratively by decorating them with a cron expression. If you're coming from Laravel, think "schedule:run + ->everyFiveMinutes()", except the schedule lives on the command class rather than in a central file.
First use case
Pick a harmless existing command — cache warmup, a report generator, a cleanup task — annotate it with a cron expression, run the bundle's "list" command to confirm it's discovered, then invoke the runner manually with -vvv before wiring it into crontab:
* * * * * cd /path/to/app && php bin/console <bundle>:cron:run >> /var/log/cron.log 2>&1
Attribute-per-command scheduling. The idiomatic use is to keep scheduling metadata next to the command it schedules. This scales well for domain commands (app:sync-orders, app:purge-sessions) because the cron expression is versioned with the code and reviewed in the same PR as the logic.
Single dispatcher, many jobs. Never create one crontab line per command. One minute-level system cron invoking the bundle's runner means jobs are centrally observable, testable via the CLI, and portable across hosts. This is the pattern that makes the bundle worth using over hand-written crontab entries.
Autoconfiguration / tags. Discovery depends on commands being registered in the container (usually via console.command tagging and autoconfigure). If a command is defined lazily or in a compiler pass, verify it appears in the bundle's list output — otherwise the attribute is silently ignored.
Overlap protection. Any job that can run longer than its interval needs a lock. Check whether the runner ships locking; if not, wrap the command with symfony/lock (LockableTrait or a LockFactory guard) so a slow run doesn't stack. In multi-container deployments this must be a shared lock store (Redis/DB), not filesystem.
Environment gating. Register the runner only in environments where a scheduler should exist, and consider disabling scheduling in test/dev via config so fixture-heavy test suites don't fire real jobs.
Output and observability. Cron jobs that swallow output are undebuggable. Pattern: have each command log a start/finish line with duration through Monolog, and let the run command stream to stdout so the system cron redirect captures failures. Pair with a heartbeat (healthchecks.io, Sentry cron monitor) on the dispatcher itself — if the dispatcher dies, nothing alerts you otherwise.
Coexistence with Symfony Scheduler. If you're on Symfony 6.3+, the native Scheduler component covers the same ground with first-class Messenger integration. A common migration pattern is to keep this bundle for legacy attribute-declared commands and move new schedules to the native scheduler incrementally, rather than doing a flag-day rewrite.
Testing. Exercise the CronManager/dispatcher directly in a kernel test: inject a frozen clock, assert which commands are considered due at a given timestamp rather than shelling out to crontab.
composer.json, read the dispatcher source once (it's small), and be prepared to vendor it. For a greenfield project on modern Symfony, prefer the native Scheduler component.-vvv → run the individual command directly. If the command works standalone but not via the dispatcher, the problem is scheduling/discovery, not your command.symfony/scheduler + Messenger, cron/cron, and dragonmantank/cron-expression (which most of these bundles depend on) before committing — the ecosystem around this bundle is thin, and the same cron-parsing capability is one composer line away if you want to build the dispatcher yourself in ~50 lines.How can I help you explore Laravel packages today?