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

Scheduler Bundle Laravel Package

goksagun/scheduler-bundle

A Symfony bundle for defining and running scheduled tasks in PHP. Register recurring jobs with simple configuration, then let the scheduler handle execution so you can keep cron logic inside your application instead of scattered server crontabs.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: goksagun/scheduler-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no config/scheduler.php, no facade. If you are working in a Laravel codebase, your day-to-day tool is already Illuminate\Console\Scheduling\Schedule (routes/console.php or app/Console/Kernel.php), and pulling this in buys you nothing. The realistic scenarios where you touch it are:

  1. You are contracting on / inheriting a Symfony app and need to understand its scheduled jobs.
  2. You are building a package that must schedule work in both frameworks and want the framework-agnostic goksagun/scheduler core underneath this bundle.

Minimal steps (Symfony context):

composer require goksagun/scheduler-bundle

Register it in config/bundles.php:

return [
    // ...
    Goksagun\SchedulerBundle\SchedulerBundle::class => ['all' => true],
];

Where to look first — in this order, because the docs are thin at 7 stars / 0 dependents:

  1. src/Attribute/ — the scheduling attribute(s). This is the whole public API surface.
  2. src/Command/ — the console command that evaluates and dispatches due tasks. Its name and options tell you how the bundle actually ticks.
  3. src/ service definitions (DependencyInjection/) — how tasks are collected from the container.

First use case: annotate an existing console command with the schedule attribute instead of writing a crontab line.

#[Scheduled(cron: '*/5 * * * *')]
class SyncInventoryCommand extends Command
{
    protected static $defaultName = 'app:sync-inventory';
    // ...
}

Then wire the bundle's dispatch command into system cron at one-minute granularity — the bundle decides which annotated tasks are due on each tick:

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

(The exact command name is in src/Command/ — confirm it before writing the crontab, don't trust a blog post.)

Implementation Patterns

Keep commands thin, put logic in services. The attribute binds scheduling to a console command. Do not put business logic in the command — it makes the task untestable and un-reusable from a queue or HTTP trigger. Pattern:

#[Scheduled(cron: '0 * * * *')]
class HourlyReportCommand extends Command
{
    public function __construct(private ReportGenerator $generator) { parent::__construct(); }

    protected function execute(InputInterface $in, OutputInterface $out): int
    {
        $this->generator->run();
        return Command::SUCCESS;
    }
}

Centralize schedule definitions when they multiply. Attribute-per-command is fine for a handful of jobs. Past ~10, you lose the ability to see the whole timetable, and "why did this run at 3am?" becomes a grep exercise. At that point add a bin/console listing/inspection step to CI that dumps all annotated tasks and their cron expressions, and diff it in review.

Idempotency over mutexes. This bundle is a thin dispatcher; assume there is no withoutOverlapping() / onOneServer() equivalent out of the box. Design every task so a duplicate run is harmless:

  • Upserts, not inserts.
  • UPDATE ... WHERE status = 'pending' claim-and-process rather than read-then-write.
  • A row-level lock (SELECT ... FOR UPDATE SKIP LOCKED) if you must have single execution.

If you genuinely need mutual exclusion, add it yourself in the command:

$lock = $this->lockFactory->createLock('app:sync-inventory', 300);
if (!$lock->acquire()) { return Command::SUCCESS; } // previous run still going
try { $this->generator->run(); } finally { $lock->release(); }

Testing pattern. Because the task is a plain command, test the service directly and test the binding separately:

public function testInventorySyncIsScheduledEveryFiveMinutes(): void
{
    $attr = (new \ReflectionClass(SyncInventoryCommand::class))
        ->getAttributes(Scheduled::class)[0]->newInstance();

    $this->assertSame('*/5 * * * *', $attr->cron);
}

That one assertion catches the most common production bug in this class of package: someone edits the cron string and silently changes the cadence.

Integration tips for a Laravel dev reading a Symfony codebase: map the concepts, don't fight them.

Laravel This bundle
$schedule->command('x')->everyFiveMinutes() #[Scheduled(cron: '*/5 * * * *')] on the command
->withoutOverlapping() do it yourself (lock)
->onOneServer() do it yourself (shared cache/DB lock)
->timezone('UTC') likely unsupported — normalize inside the task
schedule:run in cron the bundle's run command in cron
schedule:list check whether the bundle ships one; if not, write it

Logging is not optional. The dispatcher's decision ("no tasks due", "task X exited 0") is invisible unless you capture it. Point the cron redirect at a log file and also $out->writeln() a line per task with a correlation ID at the start of execute().

Gotchas and Tips

Don't try to shoehorn it into Laravel. No service provider, no auto-discovery, no Schedule facade. You would be hand-wiring a Symfony Kernel-adjacent bundle into Laravel's container for a feature Laravel already ships. If a teammate suggests it, the answer is "use routes/console.php".

Adoption risk is the headline gotcha. 7 stars, 0 dependents means: no StackOverflow corpus, no third-party blog posts, and no crowd-sourced bug reports. Consequences:

  • Pin the exact version ("goksagun/scheduler-bundle": "1.x.y"), not ^1.0. Unattended minor bumps in a low-adoption package are how you get a 2am pager alert.
  • Read the source of the run command end-to-end before going live. It is small — 20 minutes now saves a week later.
  • Own the tests. Do not assume the package's test suite reflects your PHP version or your cron strings.

Attribute discovery is container-cached. In Symfony, adding an attribute to a command is a code change the compiled container may not see. After adding or editing a #[Scheduled], always:

bin/console cache:clear

A "my new task never runs" bug is cache 90% of the time, cron 9%, attribute typo 1%.

Cron granularity gotcha. The dispatcher runs every minute; a task whose runtime exceeds its interval will pile up. */1 * * * * with a 90-second job means two overlapping runs before the first finishes. Either lengthen the interval, add a lock, or make the job resumable.

Timezone. Server clock, PHP date.timezone, and the business's "midnight" are three different things. If the bundle does not expose a per-task timezone, convert inside the task and store timestamps in UTC. Never rely on the box being UTC because it isn't on your laptop.

Debugging order of operations:

  1. bin/console debug:container --tag=... and confirm the command is registered at all.
  2. Run the dispatch command manually with -vvv; if it does nothing, the attribute is not being collected.
  3. Run the annotated command directly. If that fails, it's your code, not the scheduler.
  4. Only then look at cron: grep CRON /var/log/syslog and confirm the entry actually fires.

Extension points to know before you fork: the command itself (override/subclass if the cron runner's options are insufficient), the task-collection mechanism in the DI extension (to register tasks programmatically rather than by attribute — useful for multi-tenant apps where jobs are per-tenant and can't be expressed as static attributes), and the Symfony event dispatcher if the bundle emits pre/post-run events. Check src/Event/ for the latter; if it exists, hook logging and metrics there rather than sprinkling $out->writeln() everywhere.

The pragmatic verdict for a Laravel shop: treat this as documentation for reading Symfony scheduler code, not as a dependency. If you need framework-agnostic scheduling in a shared package, depend on the goksagun/scheduler core only, wrap it in your own Laravel service provider, and keep the Symfony bundle out of your composer.json entirely.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager