kmj/cronbundle
KMJ CronBundle is a Symfony bundle for defining and managing cron jobs in PHP applications. It provides console commands and configuration to schedule recurring tasks, run them from your codebase, and keep cron-based automation organized and maintainable.
Prerequisites you cannot skip. This is a Symfony 2.x bundle, not a Laravel package. It registers via app/AppKernel.php, uses the Symfony DI container and app/console, and targets PHP 5.x. There is no service provider, no container alias, and no compatible extension point in Laravel. If you are on Laravel, stop here and read this section as a description of a reference implementation you will re-create with schedule:run — do not attempt to install it.
For a live Symfony2 app:
Install and register.
composer require kmj/cronbundle
// app/AppKernel.php
public function registerBundles()
{
return [
// ...
new KMJ\CronBundle\KMJCronBundle(),
];
}
Read DependencyInjection/Configuration.php before the README. With three stars and thin docs, the config tree is the authoritative schema: it tells you the root key, the required fields of a job definition (name, interval/expression, command), and defaults. Next, skim Command/ for the console command that performs the crontab write — it is typically under kmj:cron:*. That pair of files tells you the whole workflow.
Back up the live crontab before the bundle touches anything.
crontab -l > ~/crontab.backup.$(date +%F)
Migrate exactly one job first. Move a single harmless line (nightly import, cache warm) into the bundle config, run the update command, and diff:
crontab -l | diff ~/crontab.backup.$(date +%F) -
If the diff shows exactly one line added inside a clearly delimited managed block, you understand the model. Migrate the rest incrementally, diffing each time.
Confirm the execution user. The command runs under whichever user invokes app/console (deploy user, cron user, or your shell). That user owns the crontab the bundle rewrites. Note it down — it is the single most important operational fact about this package.
First real use case to target: version-controlled, deploy-time crontab sync for a single-host VM. Keep schedule definitions in the repo, generate the crontab on deploy, and diff it against expectations. Everything else (dynamic per-tenant schedules, autoscaled fleets) is out of scope for this package.
Declarative job definitions. The whole value proposition is moving cron lines out of a hand-edited box and into config next to the code. Define each job with a stable name and an explicit interval/expression, then let the update command install it. Treat the config file as the source of truth and the live crontab as a generated artifact.
Managed-block pattern. The safe shape for a crontab rewriter is: read the current file, strip everything between delimiters you own, re-emit your block, and leave foreign lines untouched. When you read this package's command, verify it does exactly this. If it does not, the first integration task is to wrap or replace that emission so backup/monitoring/log-rotation entries on the same user are never clobbered.
Deploy-step workflow. The realistic daily pattern:
app/config/.app/console kmj:cron:* (name per the Command/ directory) as the service user.crontab -l against the generated expectation and fails the deploy on drift.This is the pattern that actually prevents production incidents; the bundle only provides the emission, so most of your work is the diff and the idempotency guarantee around it.
Idempotency guard. Before trusting any deploy, run the update command twice and confirm the crontab is byte-identical the second time. Non-idempotent writers are the most common failure mode of crontab managers — every deploy accumulates duplicate lines until jobs fire twice or thrice.
Per-environment schedules. Keep separate configs per environment (dev/staging/prod) and require the correct file to be selected at deploy time, rather than a flag inside the config. The crontab has no notion of "environment," so what is installed is whatever the deploy selected — make that selection auditable.
Locking long-running jobs. The bundle schedules; it does not prevent overlap. Wrap any job longer than its interval:
flock -n /var/lock/myjob.lock /path/to/app/console my:job
Output capture. Crontab swallows output unless you redirect it. Every generated line should redirect to a log or /dev/null deliberately:
cd /path/to/app && app/console my:job >> /var/log/myjob.log 2>&1
Adopt-as-reference pattern. For most teams the correct "implementation" is to read this bundle's job-definition schema and its crontab-emission command, then re-implement both in the framework's native scheduler: declare jobs in code, install a single schedule:run line via IaC, and use the scheduler's overlap and single-server guards. The ergonomics of declaring named jobs with intervals is the transferable idea; the crontab rewriting is not.
It is not a Laravel package. symfony/symfony ~2.x constraints are unsatisfiable alongside any supported Laravel's ^6/^7 Symfony components. There is no adapter to write; forcing it via --ignore-platform-reqs produces a dependency graph that breaks on every minor upgrade.
Symfony 2.x is EOL since November 2018. No upstream security patches exist even for the framework it targets, and the bundle has no CVE process or maintainer responsiveness. If you adopt it, you are the maintainer of record — plan the fork and the patch budget.
The privileged write path is the main security surface. Rewriting a crontab from application code requires crontab rights for the web/queue/deploy user. Scope this to a dedicated least-privilege user; a compromised app then gains persistence via cron rather than immediate root. Audit the shell-out in the command before trusting it — quoting and escaping bugs in generated lines are a classic RCE vector.
Debugging a job that "didn't run" — work in this order: (1) crontab -l to prove the line exists; (2) crontab -l | diff ~/crontab.backup - to spot duplicates; (3) grep the cron system log (/var/log/cron or syslog) to confirm cron attempted it; (4) run the exact command manually as the same user with env -i to reproduce the stripped environment. Cron's PATH, HOME, and shell environment are the usual culprits, not the bundle.
Environment variables are not inherited. Generated commands must be self-contained: absolute paths, cd into the app root, explicitly set PATH/APP_ENV if the app needs them. Relative paths in job definitions will "work" in your shell and fail under cron.
Escape %. Unescaped % in a crontab line is interpreted as a newline by cron — quote or backslash it in generated commands.
Duplicates accumulate silently. Run the update command twice in a staging VM before shipping and confirm idempotency. There is no built-in dedupe you can rely on.
No alerting on missed runs. The bundle schedules but does not tell you when a job stops firing. Add a heartbeat: a job that pings an external monitor (or writes a timestamp file) each cycle, and an alert if it goes stale. Do this before you migrate off the old mechanism.
Crontab semantics break on autoscaled fleets. N instances each installing the same crontab means every job fires N times, or instances race on the file. This is a structural limit — do not try to engineer around it inside the bundle. If you scale horizontally, move scheduling out of the application.
Extension points are self-maintained. The config tree in DependencyInjection/Configuration.php and the command classes in Command/ are where customisation lives, but any change is a fork you own. Prefer wrapping the command (your own deploy script that invokes it, diffs the result, and fails on drift) over patching the bundle — the wrapper survives an eventual replacement, the patch does not.
Ramp-up cost is operational, not API. The API surface is small and quick to read; the cost is that new engineers must learn an out-of-band scheduling mechanism that exists nowhere else in the stack. Budget for it explicitly, or avoid it by using the framework's native scheduler from the start.
How can I help you explore Laravel packages today?