skywarth/chaotic-schedule
Generate random, realistic cron schedules and intervals in PHP and Laravel. Chaotic Schedule makes it easy to fake unpredictable schedule frequencies for tests, seeders, demos, and load simulations, with constraints for hours, days, and expressions.
The package adds chaotic (randomised-but-deterministic) scheduling macros to Laravel's Illuminate\Console\Scheduling\Schedule, so a job can fire "somewhere in this week" instead of at a fixed minute.
^2.0 — the API is identical, so you can write the same schedule code and migrate later.composer require skywarth/chaotic-scheduleRandomDateScheduleBasis enum (WEEK / MONTH / YEAR) and the randomDays() / randomTimeSchedule() macros in the README. Those two macros plus the enum are essentially the whole surface area.First use case: a nightly report or sync that must run N times per week but must not hammer the same minute every night:
Schedule::command('reports:nightly')
->randomDays(RandomDateScheduleBasis::WEEK, 3)
->between('01:00', '05:00');
Run php artisan schedule:list immediately afterwards — you should see concrete, plausible timestamps rather than the macro name. That's your sanity check that the seeding works.
Enum cases, not integers. The v3 call style is the documented one:
->randomDays(RandomDateScheduleBasis::WEEK, 3)
->randomDays(RandomDateScheduleBasis::MONTH, 10)
->randomDays(RandomDateScheduleBasis::YEAR, 24)
Keep the determinism tuple stable. Generated runs are a pure function of (uniqueIdentifier, basisDate, periodType). That means every web node, queue worker and CI box computes the same schedule — no leader election needed to avoid duplicate runs. Treat the identifier as part of your application's contract: renaming it silently reshuffles every future run for that job.
Translate config values at the boundary, not in routes/console.php. If periods come from .env or a DB settings table, convert once:
$basis = RandomDateScheduleBasis::tryFrom(strtoupper(config('reports.period')))
?? RandomDateScheduleBasis::WEEK;
tryFrom() replaces the old validate() / isValid() helpers, and cases() replaces getAll() — useful for populating an admin dropdown without hardcoding values.
Layer the usual schedule constraints on top. The macros pick runs; between(), weekdays(), when(), withoutOverlapping() and onOneServer() still behave normally. Prefer between() to express your maintenance window and let the macro choose the moment inside it, rather than post-filtering.
Combine random days with random times for a two-axis spread (which day and which minute), which is the usual way to avoid a thundering herd when you have dozens of tenants on one scheduler.
Test against a frozen basis date. Because seeding is deterministic, a test that pins the clock and asserts the rendered schedule:list output is stable and cheap. This is the single highest-value test to write — it catches accidental identifier renames and period changes in review.
Upgrade path: bump composer.json, run your test suite, then grep for the removed helpers:
grep -rn "getString\|getDayCount\|isValid\|getAll\|InvalidScheduleBasisProvided" app/ config/ routes/
TypeError is not an Exception. This is the migration trap that will bite you in production. Invalid enum values now fail at the type-system level, so a broad catch (\Exception $e) around schedule construction (a common pattern in a settings controller that builds schedules from user input) will no longer catch it and you'll get a 500. Widen to catch (\Throwable $e) or add catch (\TypeError $e) ahead of it. Better still, validate with tryFrom() before the value reaches the macro.InvalidScheduleBasisProvided import. It's gone in v3. Static analysis catches this, but only if you actually run it — a stale use statement plus a catch block is silent dead code.basisDate to a fixed date, the schedule never reshuffles and you've built a fancy constant. Derive it from the current period (now()->startOfWeek(), startOfMonth()), and be aware that this means every consumer sees a new distribution at each period boundary — expected, but surprising if you assumed per-day variation.->timezone() on the event. Mixing the two produces runs that look random in local time but are deterministic in UTC — confusing when debugging.randomTimeSchedule float→int fix removes an implicit-conversion deprecation. Seed determinism is explicitly preserved across v2→v3, so existing schedules should not drift; still, diff schedule:list output before and after upgrading on a staging box if you have compliance requirements about run times.schedule:list before anything else. If the macro produced a time outside your between() window, or the event was filtered out entirely, it will be visible there.schedule:run alone in tests. It only fires events that are due this minute; with chaotic schedules that's almost never. Use schedule:list / schedule:test to assert the computed runs instead.How can I help you explore Laravel packages today?