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

rikudou/cron-bundle

A lightweight Symfony bundle for defining and managing cron jobs in PHP. Register scheduled tasks as services, trigger them from a single system cron entry, and keep recurring application jobs like cleanup, sync, and notifications organized and easy to maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

Start here, because nothing else works until you accept it: rikudou/cron-bundle is a Symfony 6 bundle. There is no service provider, no config/cron.php to publish, no php artisan vendor:publish target, and nothing that will register itself when Composer autoloads it in a Laravel app. composer require rikudou/cron-bundle will succeed, and then you will have a package sitting in vendor/ that Laravel's container never boots. If you're on Laravel 10/11, your "getting started" is to read the source as a reference implementation and then close the tab — Laravel's first-party scheduler already does this, with more community support and better observability.

The one-line reality check: in a Laravel app, the equivalent of this entire bundle is already present:

// routes/console.php (Laravel 11) or app/Console/Kernel.php::schedule() (Laravel 10)
Schedule::command('reports:generate')->cron('*/5 * * * *');

That's the whole value proposition — cron expression plus command invocation — already shipped, already tested, already documented.

Where to look if you're reading it as a design reference (a 30-minute exercise, not more):

  1. src/Attribute/Cron.php — the #[Cron('* * * * *')] attribute. This is the declarative hook.
  2. src/Command/ — the single cron:run-style command that iterates registered jobs and dispatches the due ones. This is the entire execution loop.
  3. The DI wiring — how the bundle collects attributed services into a registry. This is the pattern worth understanding if you eventually build dynamic scheduling.

The first use case (in a real Symfony 6 app) — since that's the only place this actually runs — is replacing a hand-edited crontab with DI-aware classes:

composer require rikudou/cron-bundle
# Register RikudouSage\CronBundle\RikudouCronBundle in config/bundles.php (Flex does this)
use RikudouSage\CronBundle\Attribute\Cron;

#[Cron('*/5 * * * *')]
class SyncInventory
{
    public function __construct(private InventoryClient $client) {}

    public function __invoke(): void
    {
        $this->client->sync();
    }
}

Then exactly one system crontab entry, ever:

* * * * * php /path/to/bin/console cron:run >> /var/log/cron.log 2>&1

In Laravel, do this instead. The equivalent pattern, with the same DI/testability benefits:

// app/Console/Commands/SyncInventory.php
class SyncInventory extends Command
{
    protected $signature = 'inventory:sync';
    public function __construct(private InventoryClient $client) { parent::__construct(); }
    public function handle(): int { $this->client->sync(); return self::SUCCESS; }
}

And one crontab entry:

* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1

If you specifically need dynamic, DB-backed schedules (the one thing Laravel's scheduler doesn't do out of the box) — customer-defined report cadences, per-tenant polling — do not reach for this bundle. Reach for dragonmantank/cron-expression, which is already installed as a transitive dependency of illuminate/console:

use Cron\CronExpression;

$due = CronExpression::factory($schedule->cron_expression)->isDue();
if ($due) { Artisan::call($schedule->command); }

Implementation Patterns

Pattern 1 — Collapse many crontab entries into one. This is the bundle's core selling point, and Laravel shares it. Whether Symfony or Laravel, the rule is: the host crontab has exactly one entry that triggers the framework's scheduler tick, and the schedule itself lives in version-controlled code. Never let anyone add a second * * * * * php ... line to the host — the whole point is one entry.

Pattern 2 — One class/service per job, constructor-injected. The bundle encourages a #[Cron]-attributed service per logical task. Port this discipline directly:

// Symfony: one attributed service = one entry
#[Cron('0 3 * * *')] class NightlyReport {}
#[Cron('*/15 * * * *')] class PollWebhooks {}

// Laravel: one Command / invokable per job, scheduled in one place
Schedule::command('reports:nightly')->dailyAt('03:00');
Schedule::call(new PollWebhooks)->everyFifteenMinutes();

The discipline matters more than the mechanism: keep run() / handle() short, inject collaborators, unit-test the job body independently of the schedule.

Pattern 3 — Cron expression is configuration, not a literal. The bundle lets you set expressions in YAML/PHP config. Mirror that in Laravel by pulling intervals from env where staging needs to be faster or slower than production:

Schedule::command('inventory:sync')
    ->cron(env('INVENTORY_CRON', '*/5 * * * *'))
    ->timezone(env('SCHEDULER_TZ', 'UTC'));

Or, for per-tenant dynamic scheduling, store (cron_expression, command, payload, enabled) in a table and have a single scheduled command iterate the rows, filter by CronExpression::isDue(), and dispatch what's due:

Schedule::call(function () {
    TenantSchedule::where('enabled', true)->each(function ($row) {
        if (CronExpression::factory($row->cron_expression)->isDue()) {
            Artisan::call($row->command, $row->payload ?? []);
        }
    });
})->everyMinute()->withoutOverlapping();

That's roughly 150–300 lines to build internally, and it's the honest scope of what this bundle actually does.

Pattern 4 — Single scheduling loop, not many. The cron:run command in the bundle evaluates all registered jobs on each tick. In Laravel, php artisan schedule:run is the analogous loop — it iterates Schedule's event list, checks each expression, and dispatches due events. Both approaches are identical in shape; do not be tempted to spawn per-job cron lines "because they're simpler" — you lose the single source of truth and the testability.

Pattern 5 — Observability is on you. Neither the bundle nor Laravel's scheduler gives you meaningful run history out of the box. If you care about "did the nightly job actually run," you're bolting on:

  • Laravel: spatie/laravel-schedule-monitor (real maintenance, real adoption), or emit ->onSuccess() / ->onFailure() callbacks to your metrics/logging stack.
  • Symfony: symfony/scheduler (first-party, maintained, Symfony 6.3+) is honestly the better answer than this bundle for anything new.

Gotchas and Tips

  • It does not install into Laravel. This is the gotcha. No service provider registers, no bundle boots, no DI attribute scanning happens. You will import namespaced classes fine (autoload works) but the runtime machinery — the cron:run command, the attribute-to-registry wiring — won't exist. Don't try to bridge it; you're building half the bundle yourself to make it work, at which point you've reimplemented Laravel's scheduler with extra steps.

  • Symfony 6 is a hard ceiling. The package targets Symfony 6 explicitly. When the app migrates to Symfony 7, this becomes a migration liability. Assume the maintainer is gone; 3 stars, 0 dependents, single maintainer means no upgrade path and no security response. Vendor it if you adopt it.

  • Overlap protection is not the bundle's job. Slow jobs will double-run on the next tick. In Laravel, use ->withoutOverlapping(). Multi-node deployments need ->onOneServer() (requires a cache driver all nodes share — Redis, database, memcached — not array or file). The bundle has nothing built-in for this; if you were going to adopt it for that, don't.

  • Timezone and DST are where cron implementations rot. The bundle evaluates expressions against server time unless configured otherwise. Laravel lets you pin per-task timezone with ->timezone('America/New_York'). Be explicit — "runs at 3am" means some timezone, and DST transitions silently skip or double a job. Pick a canonical scheduler TZ (usually UTC) and convert deliberately.

  • Missed runs are silent by default. If the host was down at the trigger minute, the tick catches up only within the same minute window — there is no catch-up semantics in either the bundle or Laravel's scheduler. If you need "run this even if we missed it," that's on your application logic, not the scheduler.

  • cron:run / schedule:run must actually run every minute. If your cron daemon is paused, misconfigured, or the user doesn't have the entry, nothing happens silently. Verify with a manual bin/console cron:run -v (Symfony) / php artisan schedule:run -v (Laravel) and a known due expression before trusting the host crontab.

  • Extension point that matters: if you need per-tenant scheduling, the correct design is a scheduler job that reads schedule definitions from persistence and dispatches dynamically. Neither this bundle nor vanilla Laravel's schedule() covers it out of the box — this is the ~200-line internal build. dragonmantank/cron-expression is the primitive; it's already in your composer.lock via illuminate/console, so you're not adding a dependency, just using one you ship.

  • Debugging a job that "didn't run": in order — (1) is the host crontab line present and the daemon running? (2) does -v output on a manual trigger actually list the job? (3) does the expression match now in the configured timezone? (4) is withoutOverlapping holding a stale lock after a crash? (Laravel clears it after ->withoutOverlapping(expiresAfter: N) minutes; if you don't set the expiry, a crashed job can wedge the lock and silently skip forever — a real footgun.)

  • Do not install it in a Laravel codebase "just to see." Composer will pull it fine, and then you'll have an unexercised Symfony bundle in your dependency tree, a maintainer you can't rely on, and zero production value. If you're curious, read the source on GitHub and delete the vendor directory.

Bottom line for a Laravel developer: this package's entire value — declarative cron expressions bound to DI-resolved callables, driven by a single scheduler tick — is present in Laravel's scheduler with better maintenance. The one gap (dynamic, DB-backed schedules) is a dragonmantank/cron-expression + Schedule::call implementation, not a dependency adoption. Read the bundle as a design reference in 30 minutes, then close it.

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.
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
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules