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

Cronos Bundle Laravel Package

mybuilder/cronos-bundle

Symfony bundle that integrates the Cronos cron expression library, making it easy to parse, validate, and evaluate cron schedules for scheduled jobs and time-based tasks in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the package name carefully before you composer require. mybuilder/cronos-bundle is a Symfony bundle. There is no bundles.php in a Laravel app, no RegisterBundles step, and no autowiring alias for Cron\CronExpression — the bundle's entire value is container wiring, and that wiring does not exist in Laravel. What you can adopt is the underlying library, mybuilder/cronos (Cron\CronExpression), wired up yourself in a service provider. Budget this as "borrow the parser, write ten lines of glue," not "install a Laravel package."

Minimal setup:

composer require mybuilder/cronos
// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->singleton(CronEvaluator::class, fn () => new CronEvaluator());
}

Look at the mybuilder/cronos README (not the bundle's), and specifically the four methods you will use daily:

  • CronExpression::factory(string $expr) — parse
  • CronExpression::isValidExpression(string $expr) — validate
  • ->getNextRunDate($now, $nth, $allowCurrentDate, $tz) — preview/schedule
  • ->isDue($now, $tz) / ->getPreviousRunDate(...) — fire decisions

First use case: a "scheduled report" form where the user types 0 6 * * 1-5. You need three things in that request — reject garbage with a 422, show "next run: Mon 06:00" under the field, and drive the actual schedule. That whole loop is the package in a nutshell:

$expr = CronExpression::factory($request->string('cron'));   // throws on invalid
return response()->json([
    'next_run' => $expr->getNextRunDate('now', 0, false, config('app.timezone'))
                       ->format(DATE_ATOM),
]);

Implementation Patterns

Wrap it in one application-level class. The most useful pattern, and the one that makes the eventual swap cheap, is a thin CronEvaluator service that never leaks Cron\CronExpression into controllers, models, or jobs:

final class CronEvaluator
{
    public function isValid(string $expr): bool
    {
        return $expr !== '' && CronExpression::isValidExpression($expr);
    }

    public function nextRun(string $expr, ?DateTimeInterface $from = null, ?string $tz = null): CarbonImmutable
    {
        $next = CronExpression::factory($expr)
            ->getNextRunDate($from ?? 'now', 0, false, $tz ?? config('app.timezone'));

        return CarbonImmutable::instance($next)->setTimezone($tz ?? config('app.timezone'));
    }

    public function isDue(string $expr, ?string $tz = null): bool
    {
        return CronExpression::factory($expr)->isDue('now', $tz ?? config('app.timezone'));
    }
}

A validation rule, reused everywhere. Put the "is this parseable" check in a Rule object so form requests, Nova/Filament fields, and API resources all agree:

final class CronExpressionRule implements ValidationRule
{
    public function __construct(private CronEvaluator $cron) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (!is_string($value) || !$this->cron->isValid($value)) {
            $fail("The {$attribute} field must be a valid cron expression.");
        }
    }
}

Dynamic schedules from the database. The pattern that usually justifies the dependency: users define schedules in a table, and schedule() loops over them.

// routes/console.php  or  Console\Kernel::schedule()
foreach (ScheduledJob::where('active', true)->cursor() as $job) {
    $schedule->call(fn () => $job->run())
        ->cron($job->cron_expression)          // Laravel's own parser
        ->timezone($job->timezone)
        ->name("job:{$job->id}")
        ->withoutOverlapping();
}

Note the tension here: ->cron() already accepts the raw expression and Laravel parses it for you. Reach for your CronEvaluator only when you need something the scheduler doesn't give you — a preview, a catch-up window, or a manual due check.

Catch-up after downtime. The genuinely valuable use case: a job was down from Friday to Monday and you need to know which of its runs you missed.

$missed = [];
$cursor = $lastRunAt;
while (($next = $this->cron->nextRun($expr, $cursor))->lte(now())) {
    $missed[] = $next;
    $cursor = $next;
    if (count($missed) > 100) break;   // always bound the loop
}

Preview/UX endpoint. A debounced GET /schedules/preview?cron=... returning five upcoming runs as ISO strings is a small feature that removes most bad-cron support tickets, and it's ~15 lines with this parser.

Self-scheduling worker / isDue polling. If you poll more than once per minute (e.g. a while(true) worker, or several queue workers), gate it with a cache lock and a last-run marker — see gotchas below.

Gotchas and Tips

  • Class collision with Laravel's own parser. laravel/framework already depends on dragonmantank/cron-expression, which ships the same Cron\CronExpression FQCN as Cronos. Installing both puts two definitions of one class on the autoloader; whichever wins is not something you control. For most Laravel apps the honest conclusion is: you already have this capability — use the shipped parser, and only evaluate Cronos if you need a behaviour it has and the incumbent doesn't. If you do adopt it, add a Composer-level guard so the duplicate can't silently re-enter.
  • Don't install the bundle. mybuilder/cronos-bundle will pull Symfony DI into a Laravel app and give you nothing. Depend on mybuilder/cronos directly.
  • Staleness. Last release was January 2023. Check the composer.json PHP constraint against your runtime before committing; if it doesn't declare your PHP minor, expect deprecation notices or a composer conflict flag, and pin the version.
  • Strict 5-field parsing. Cronos is a cron parser, not a Quartz parser. ?, L, W, #, year fields, and 6-field seconds are not guaranteed — write a test asserting the exact expression shapes you accept. Users pasting from Jenkins/Quartz/Kubernetes will hit this on day one, so validate on the input side with a friendly message rather than letting an InvalidArgumentException bubble out of a controller as a 500.
  • @daily, @hourly, @weekly aliases are the exception that usually works — verify per version, and prefer expanding them to canonical 5-field form in your storage layer so a future parser swap stays a one-liner.
  • Exceptions are not validation. factory() and the date methods throw on malformed input; isValidExpression() does not. Always validate first, then evaluate — never wrap the evaluation in a bare catch (\Throwable) and return null, or you'll silently lose schedules.
  • Timezone is explicit, and it matters. Pass config('app.timezone') (or the record's own timezone) rather than relying on date_default_timezone_set. getNextRunDate() honours the tz argument; skipping it produces previews that disagree with the scheduler, and during a DST spring-forward a 30 2 * * * job may legitimately skip a day — decide that behaviour deliberately, not by accident.
  • isDue() is true for the whole minute. Polling twice in a minute fires twice, and overlapping workers double-fire. Use Cache::lock("cron:{$id}", 60) plus a last_ran_at comparison, or withoutOverlapping() if you're inside the scheduler.
  • Infinite loops. Any getNextRunDate loop (catch-up, "next five runs") must have a hard iteration cap. A pathological expression plus a far-past cursor will otherwise spin.
  • Cache parsed expressions in hot paths. If a tick evaluates hundreds of rows, memoize factory() results per expression string in a request-scoped array; parsing is cheap individually and not free in bulk.
  • Test with frozen time. Carbon::setTestNow(...) makes isDue/getNextRunDate deterministic; wrap the parser call in something injectable (the CronEvaluator above) so tests don't depend on the real clock.
  • Extension point = your own binding. In Symfony the extension point is the bundle's service definition and autowiring alias; in Laravel it is the interface you registered in AppServiceProvider. Keep Cron\ behind that boundary — 87 stars and a mid-2023 release cadence is not a foundation to couple controllers to, and the swap to dragonmantank/cron-expression (near-identical API) should be a single-file change.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings