Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cron Bundle Laravel Package

okvpn/cron-bundle

Symfony bundle for scheduling and managing cron jobs in PHP. Define tasks in configuration or annotations, run console commands on schedules, and use locking, logging, and process management to prevent overlaps and track execution.

View on GitHub
Deep Wiki
Context7

Getting Started

Mental model: this bundle is not a daemon. It's a single console entry point (okvpn:cron:run) that you poke every minute from the host crontab, a Docker sidecar, or a Kubernetes CronJob. On each tick it evaluates each registered job's cron expression and runs the ones that are due. If you know Laravel's scheduler ($schedule->command(...)->everyFiveMinutes() plus the one schedule:run crontab line), this is the same shape — Symfony-flavoured, with the crontab living outside the app.

  1. Install and register the bundle.

    composer require okvpn/cron-bundle
    

    Symfony Flex registers it; otherwise add Okvpn\Bundle\CronBundle\OkvpnCronBundle to config/bundles.php.

  2. Look here first, in this order:

    • bin/console okvpn:cron:list — prints every registered job, its expression and next run time. This is your ground truth when "nothing ran".
    • config/packages/okvpn_cron.yaml — lock directory, parallelism, queue settings.
    • The CronJobInterface and the #[CronJob] attribute class — these define how a job is declared.
  3. Add the one crontab line. Everything depends on it:

    * * * * * cd /app && php bin/console okvpn:cron:run --env=prod >> var/log/cron.log 2>&1
    

    In Docker, either bake crond into a tiny sidecar image or have the host / orchestrator invoke docker exec app php bin/console okvpn:cron:run. The bundle doesn't ship a scheduler daemon, which is exactly why it calls itself "Docker friendly".

  4. First real job — schedule an existing command without touching its class:

    use Okvpn\Bundle\CronBundle\Attribute\CronJob;
    
    #[AsCommand(name: 'app:reports:prune')]
    #[CronJob(schedule: '0 3 * * *', description: 'Drop report exports older than 30 days')]
    class PruneReportsCommand extends Command { /* unchanged */ }
    

    Or a standalone service when the work isn't naturally a command (heavier DI, no console output needed):

    class ReindexProductsJob implements CronJobInterface
    {
        public function __construct(private SearchIndex $index) {}
    
        public function run(): void
        {
            $this->index->reindexStale();
        }
    }
    

    Tag it okvpn.cron with schedule: '*/15 * * * *' (or declare it under okvpn_cron.jobs in YAML).

The Laravel → Symfony mapping, since that's where your instincts will go:

Laravel Here
Schedule::command('x')->daily() #[CronJob(schedule: '0 0 * * *')]
withoutOverlapping() lock / singleton flag on the job
onOneServer() shared lock directory across hosts (not automatic)
runInBackground() / dispatch() parallel execution or Messenger queue mode
schedule:run crontab line okvpn:cron:run crontab line

Implementation Patterns

1. Attribute-on-command is the default choice. It keeps the job next to the code it schedules, survives refactors, and means okvpn:cron:list is a complete inventory. Use a standalone CronJobInterface class only when there is no command to decorate or when you need to inject services the command doesn't have.

2. Declarative config for jobs you don't own. If you're scheduling a vendored command (or want all schedules visible in one file for ops review), declare them under okvpn_cron.jobs:

okvpn_cron:
    jobs:
        app_cache_warm:
            command: 'app:cache:warm'
            schedule: '*/10 * * * *'
            description: 'Warm the catalog cache'

This is the closest analogue to routes/console.php and is the pattern to prefer when non-developers need to read/adjust the schedule.

3. Lock long jobs, deliberately. Any job that can exceed its own interval (a report export, an index rebuild) must be marked non-concurrent so a second tick doesn't start a duplicate while the first is still running. Pick the timeout you actually want to tolerate and set it explicitly — the default is usually "wait forever", which silently stops the job forever after one hang.

4. Multi-host deployments need a shared lock store. On a single container this is a non-issue. On three replicas behind a load balancer, every replica runs its own okvpn:cron:run, so a job without a shared lock fires three times. Point the lock directory at a volume all replicas see (NFS, EFS) or verify whether your installed version supports a Symfony Lock adapter — test this before you scale, it's the single most common production surprise with this class of bundle.

5. Use parallel execution for breadth, not for one slow job. Running due jobs in parallel processes keeps one slow job from delaying the rest of the tick, but it multiplies memory. Enable it when you have many short jobs; for one heavy job, isolate it — either onto its own schedule or into the queue.

6. Queue mode for anything > a few seconds. Dispatching the job as a Messenger message gets you retries, backoff, failure transport, and worker autoscaling for free. The trade-off is that "ran successfully" now means "was dispatched"; the actual execution and its failures live in the worker logs and the failed transport.

7. Environment gating. Rather than conditionals inside jobs, keep separate okvpn_cron.yaml per environment and disable noisy jobs in dev/test. Verify that okvpn:cron:run is genuinely not wired up in those environments — a local sidecar that runs your crontab will happily DELETE against a shared staging DB.

8. Testing. Don't test the scheduler, test the job. Call run() on the job class directly in a unit test, or boot the kernel and invoke okvpn:cron:run <job> explicitly in a functional test to cover wiring, DI and tags. Assert the effect, not the console output.

9. Deployment smoke test. Run okvpn:cron:list as part of the deploy step. A malformed cron expression or a service tag typo then fails the deploy instead of surfacing as a silent 3 a.m. no-op.

Gotchas and Tips

The ticker is a single point of failure with no alarm of its own. If the container restarts and the sidecar doesn't come back, or the crontab entry is lost in an image change, nothing runs and nothing complains. Monitor it externally — a heartbeat job that pings a healthcheck service every minute is worth more than any amount of job-level logging.

Timezone is not what you think it is. Expressions are evaluated in the PHP/system timezone, which in Docker is almost always UTC. 0 3 * * * is 3 a.m. UTC, not local. Pin date.timezone explicitly and use the per-job timezone option if the bundle version supports it — otherwise write every expression in UTC and comment it.

Locks in var/cache are wiped by deploys. If the lock directory lives under a path that cache:clear or the build pipeline deletes, in-flight jobs lose their lock and a redeploy can cause a duplicate run mid-execution. Put locks somewhere stable and writable, and check permissions if you run as a different user than the web process.

Container shutdowns kill jobs mid-flight. SIGTERM to the PHP process is not a graceful drain. Write every job as idempotent and crash-safe: resumable batches, "claim then process" rows, no work that leaves the DB in a half-updated state if it dies at second 40 of a 60-second run.

Skipped runs are invisible by default. When a lock is held, the job is skipped — often with no log line. For anything business-critical, log both "started" and "skipped (locked)" so you can tell "never scheduled" from "always contended" from "crashed".

Job output goes to the crontab redirect, not your app logger. Wire jobs to a dedicated Monolog channel if you want searchable history; the >> var/log/cron.log redirect is fine for debugging and bad for auditing.

--force/manual invocation semantics differ. Manually running a job often bypasses the schedule check — verify whether it also bypasses the lock in your installed version before you put it in a runbook or an admin button.

One fatal error can take out the rest of the tick. In non-parallel mode a job that exhausts memory or throws an uncaught fatal may prevent later jobs in the same run from starting. Parallel mode, or separate okvpn:cron:run <job> invocations, gives you isolation.

Extension points: a custom lock backend, a compiler pass/job loader for auto-registration, and decorating the runner are the three places to hook in. They're also the three places most likely to break across versions — wrap any override in your own service and keep the coupling in one file.

Maturity is the real gotcha. 18 stars, last release August 2023, zero dependents means: no community to google for, no guarantees on the next Symfony major, and you will be reading the source to answer most questions. That's fine — it's a small, readable bundle doing one job — but treat it accordingly:

  • Pin the exact version in composer.json and read the changelog before bumping.
  • Keep the cron expressions and job registration thin; put all real logic in your own services so the bundle is a replaceable ~50 lines of glue.
  • Evaluate symfony/scheduler (first-party, Symfony 6.3+) if you're on a modern Symfony — it solves the same problem with a supported future and is the natural migration target. If you're already writing jobs as plain services implementing an interface, that migration is a day's work rather than a rewrite.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json