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

Schedule Bundle Laravel Package

eonx-com/schedule-bundle

Brings Laravel's command scheduling to Symfony: register scheduled commands via autoconfigured schedule providers with the full Laravel scheduling API, then run them with the schedule:run console command.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and register manually. There is no Symfony Flex recipe, so composer require eonx-com/schedule-bundle does not do everything. You must add the bundle yourself:

// config/bundles.php
return [
    // ...
    EonX\ScheduleBundle\ScheduleBundle::class => ['all' => true],
];

2. Confirm your dependency graph resolves before writing any code. The bundle pulls Laravel's scheduling internals (illuminate/*) into a Symfony app. Run composer why-not eonx-com/schedule-bundle against your symfony/framework-bundle and PHP constraints, and note the exact illuminate/* versions that landed in composer.lock. If this step fights back, stop — that is your signal to use Symfony's native Scheduler instead.

3. Where to look first (in this order):

  • README.md — it is the only documentation; read it in full, it is short.
  • src/Interfaces/ScheduleProviderInterface.php — the one method you implement.
  • src/Interfaces/ScheduleInterface.php — the actual API surface. This is the authoritative list of what was ported. The README links Laravel 5.8 docs, but do not assume every method exists; diff the interface against it.
  • The schedule:run console command class in src/ — this tells you how providers are collected, how output is written, and what exit code you get.
  • vendor/eonx-com/schedule-bundle/ for everything else, because there is no other reference material.

4. Write your first provider. Start with a single, harmless, observable command so you can prove the wiring works end to end:

// src/Schedule/HealthCheckScheduleProvider.php
namespace App\Schedule;

use EonX\ScheduleBundle\Interfaces\ScheduleProviderInterface;
use EonX\ScheduleBundle\Interfaces\ScheduleInterface;

final class HealthCheckScheduleProvider implements ScheduleProviderInterface
{
    public function schedule(ScheduleInterface $schedule): void
    {
        $schedule
            ->command('app:heartbeat')
            ->everyMinute()
            ->setMaxLockTime(120);
    }
}

No tagging is required: autoconfigure picks up anything implementing ScheduleProviderInterface. Verify it registered before trusting it:

php bin/console debug:container --tag=  # or grep the compiled container
php bin/console schedule:run -v

5. Run it manually, then cron it. php bin/console schedule:run executes due tasks once and exits — it is not a daemon. The bundle does not install the cron entry; you do:

* * * * * cd /var/www/app && php bin/console schedule:run >> /var/log/schedule.log 2>&1

First use case to pick: a nightly or five-minute housekeeping job (cache warm, report generation, stale-record cleanup) that currently lives in someone's crontab. It is low-blast-radius, easy to verify, and demonstrates the whole value proposition: the schedule is now in src/Schedule/, under code review.


Implementation Patterns

One provider per bounded context, not per command. Group related jobs so the file reads like a schedule, not a registry:

final class BillingScheduleProvider implements ScheduleProviderInterface
{
    public function schedule(ScheduleInterface $schedule): void
    {
        $schedule->command('billing:charge-recurring')->dailyAt('02:00')->setMaxLockTime(3600);
        $schedule->command('billing:reconcile')->dailyAt('04:00')->setMaxLockTime(1800);
    }
}

This is also the unit you test and the unit you can delete when a feature is retired.

Pass arguments as an array, flags included. The second argument to command() is the argv tail, exactly like Laravel:

$schedule->command('app:sync', ['--region=eu', '-v'])->hourly();

Because these are resolved through Symfony's console application, the command must be a registered Symfony command — the bundle schedules existing commands, it does not define them. Write and test the command normally first.

Overlap protection is the whole reason to use this over raw cron. Two mechanisms are visible in the README/interface; use them deliberately:

  • setMaxLockTime(seconds) — bounding how long a lock may be held.
  • withoutOverlapping() (Laravel parity, if present in ScheduleInterface) — skip a run while a previous one is still going.

Use overlap protection on any job that mutates shared state (billing, inventory reconciliation, digest sends). For a single-host app a filesystem/cache lock is fine; for a multi-host app you must confirm the lock store is shared (Redis/DB), or you have per-host locks and no protection at all.

Guard environments inside the provider. Keep staging and prod on the same code path but different behavior:

if ($this->kernel->getEnvironment() !== 'prod') {
    return;
}

Or use when()/skip() predicates if they were ported. Either way, the decision lives in version control rather than in a sysadmin's head.

Local development pattern. Do not run schedule:run from cron locally. Run it by hand when you need it, and keep the schedule's cadence coarse enough that a manual run is meaningful. For a "does my provider even register" check, schedule:run -v plus a log line inside your command is faster than reading container dumps.

Test providers as plain objects. The provider is a pure function of ScheduleInterface, so a spy/fake schedule is the cheapest test:

$schedule = $this->createMock(ScheduleInterface::class);
$schedule->expects($this->once())->method('command')->with('app:heartbeat', []);
(new HealthCheckScheduleProvider())->schedule($schedule);

This only works if the fluent methods return $this — verify that on the fake, because a broken fluent chain will fail at runtime, not in the test.

Wrap the runner for observability. The bundle schedules commands; it does not give you run history, durations, or failure alerting. Add it at the boundary:

  • A console.terminate listener or a decorator around the run command that logs scheduled command names, start/end, exit code.
  • Alert on absence of runs, not just failures. The most dangerous failure mode here is silence.

If you commit to it, fork and pin. Given the archived status, the realistic day-to-day pattern is: vendor the package under an internal namespace, pin illuminate/* to exact versions, and add an explicit owner. Because the API surface is one method plus a fluent builder, migrating off later is cheap — that reversibility is worth preserving, so keep provider files thin and free of package-specific cleverness.


Gotchas and Tips

Silent empty schedule if autoconfigure is off. Providers are collected only via Symfony's autoconfigure. If any bundle or global config disables autoconfigure for your src/ namespace, your providers vanish and schedule:run happily executes zero tasks and exits 0. There is no error. Always verify with a first run against a command that visibly logs, and re-verify after any DI refactor.

No recipe, no CI-visible compatibility matrix. There is no documented support for a given PHP/Symfony/illuminate triple. Treat every dependency bump as a candidate for a scheduler-wide outage. Pin aggressively and re-run a smoke test of schedule:run in CI after upgrades.

Cron granularity is one minute and the runner is stateless. schedule:run must be invoked every minute by cron. If it is invoked every five, ->everyMinute() silently becomes every five. There is no sub-minute scheduling, and no daemon mode — do not try to loop it.

Overlapping schedule:run invocations. The cron entry itself is not overlap-protected. If a run takes longer than a minute, invocations stack. Add flock to the cron line, in addition to per-task locking:

* * * * * flock -n /tmp/schedule.lock php bin/console schedule:run

Lock backend must be shared. setMaxLockTime and friends are only as good as the underlying store. On a multi-host fleet with a local cache lock, every host runs the job and you get N concurrent billing runs. Confirm Redis/DB-backed locking or you have a false sense of safety.

Timezone. Laravel's scheduler resolves "daily at 02:00" against an application timezone. Symfony apps often don't set one. If you have jobs whose correctness depends on wall-clock time (month-end reports, DST-sensitive cutovers), set and test the timezone explicitly rather than inheriting PHP's default.

Exit codes and output are the whole observability story. schedule:run can return 0 even when an individual scheduled command failed, depending on how the port surfaces failures. Do not rely on the exit code for alerting — log per-command outcomes, or wrap the run command.

Laravel 5.8 docs are a trap for a subset of the API. The README links the 5.8 docs as "all the features," but this is a port. Methods you know from Laravel (runInBackground, between, environments, evenInMaintenanceMode) may be absent, partial, or behave differently against Symfony's console. Grep ScheduleInterface and the concrete implementation before designing around a method.

Debugging checklist when a job "doesn't run":

  1. Is the provider in the compiled container? (debug:container / look for the tag or service registration)
  2. Is the command itself registered and callable? (php bin/console app:your-command directly)
  3. Does the expression actually consider it due now? (php bin/console schedule:run -v and read the output)
  4. Is a lock held from a previous crashed run? Check/clear the lock store.
  5. Is cron actually firing? (grep CRON /var/log/syslog, or write a timestamp line from the cron entry)
  6. Timezone mismatch between cron host and PHP app.

Extension points are thin, by design. There is no plugin system. The leverage you have:

  • Replace or decorate the schedule:run command service via a compiler pass.
  • Decorate ScheduleInterface if you want to intercept fluent calls for logging or assertions.
  • Add console event listeners for run/failure telemetry. Everything else means forking.

Archived = you are the maintainer. No CVE patches, no PHP 8.x/Symfony 7 fixes, no bug triage. Before adopting for anything business-critical, make the explicit call: fork and own it, or use Symfony's first-party symfony/scheduler. The scheduling model is worth copying either way; the dependency is not worth inheriting unexamined.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package