zachiler/laravel-cadence

Run your Laravel application in accelerated time with a tick-based simulation loop.

Maintainers

Package info

github.com/zachiler/laravel-cadence

pkg:composer/zachiler/laravel-cadence

Statistics

Installs: 13

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1-alpha 2026-03-16 00:56 UTC

This package is auto-updated.

Last update: 2026-03-16 01:05:07 UTC


README

Run your Laravel application in accelerated time.

Tests PHP Version Laravel Version License

What It Does

Install the package, implement a tick handler, and run cadence:run. Your Eloquent models, queued jobs, and scheduled commands all operate against simulated time — hours pass in seconds. Observe your application behaving as if days or weeks have elapsed, all from a single Artisan command.

Quick Start

composer require zachiler/laravel-cadence
php artisan cadence:install
php artisan migrate
// app/Cadence/Handlers/OrderHandler.php
class OrderHandler extends \Cadence\Support\BaseHandler
{
    protected function handle(\Cadence\State\TickContext $context): void
    {
        $context->businessHours()->every('1 hour', 'create-orders', function () {
            Order::factory()->count($this->config('order_rate', 3))->create();
        });
    }
}
php artisan cadence:run --scenario=growth --duration="14 days"

Register your scenario in config/cadence.php:

'scenarios' => [
    'growth' => App\Cadence\Scenarios\GrowthScenario::class,
],

See Getting Started for the full walkthrough.

Configuration

// config/cadence.php
return [
    // Environments where Cadence is allowed to run.
    'allowed_environments' => ['local', 'staging'],

    // Default simulation settings. Can be overridden per-run via CLI options.
    'defaults' => [
        // Simulated seconds per real second. 900 = 15 simulated minutes per real second.
        'speed' => 900,

        // Real-time interval between ticks in milliseconds.
        'tick_interval' => 1000,
    ],

    // Named presets for common simulation profiles.
    // Usage: php artisan cadence:run --preset=fast
    'presets' => [
        'fast' => ['speed' => 7200, 'tick_interval' => 500],
        'slow' => ['speed' => 300, 'tick_interval' => 2000],
        'realtime' => ['speed' => 1, 'tick_interval' => 1000],
    ],

    // Queue connection during simulation. Must be 'database'.
    'queue_connection' => 'database',

    // Where database snapshots are stored.
    'snapshot_path' => storage_path('cadence/snapshots'),

    // Custom binary paths for snapshot commands (null = auto-detect).
    'binary_paths' => [
        'mysqldump' => null,
        'mysql' => null,
        'pg_dump' => null,
        'psql' => null,
    ],

    // Prefix for all cache keys used for cross-process state.
    'cache_prefix' => 'cadence',

    // Cache store for simulation state and signals. null = app default.
    'state_store' => null,

    // Named scenarios (implement Cadence\Contracts\Scenario).
    // Supports class strings or [class, default_params] arrays.
    'scenarios' => [
        // 'growth' => App\Cadence\Scenarios\GrowthScenario::class,
        // 'growth-aggressive' => ['class' => App\Cadence\Scenarios\GrowthScenario::class, 'params' => ['signup_rate' => 0.9]],
    ],

    // Tick handler classes (used when no --scenario is specified).
    'handlers' => [],

    // Default options passed to handler config.
    'handler_options' => [],

    // Maximum real-time seconds a single tick is allowed to take. 0 = no limit.
    'max_tick_duration' => 30,

    // Event log storage driver: 'database' or 'file'.
    'event_log_driver' => 'database',

    // Path for JSONL event log when using the 'file' driver.
    'event_log_path' => storage_path('cadence/events.jsonl'),
];

How Time Works

Cadence uses Carbon's setTestNow() to shift all time-dependent code. During a simulation:

  • now(), Carbon::now(), Date::now() — all return simulated time
  • Eloquent timestamps (created_at, updated_at) — use simulated time automatically
  • Queued job delays — evaluated against simulated time
  • Scheduled commands — fire based on simulated cron evaluation

You do not need to call any special clock function in your application code. Standard Laravel time functions work as expected — the simulation makes them return simulated time instead of real time.

The Clock class (Cadence\Clock\Clock) is used internally by the runner. You can use Clock::now() or just now() — they return the same value during simulation.

Feature Highlights

Tick Scheduling DSL

Avoid time-checking boilerplate. The DSL handles interval tracking and day/time filtering:

$context->businessHours()->every('1 hour', 'process-orders', function () { /* ... */ });
$context->weekends()->every('4 hours', 'weekend-report', function () { /* ... */ });
$context->on('monday')->between('09:00', '12:00')->every('30 minutes', 'standup', function () { /* ... */ });

Full reference

Handler Dependencies

Declare execution order through dependencies — the runner topologically sorts handlers before the first tick:

class BillingHandler extends BaseHandler implements HasHandlerDependencies
{
    public function dependsOn(): array
    {
        return [TeamLifecycleHandler::class, ProjectActivityHandler::class];
    }
}

Full reference

Invariants & Breakpoints

Define conditions checked after every tick. Invariants assert correctness; breakpoints pause for inspection:

$runner->invariant(fn () => User::count() > 0, 'has-users', InvariantBehavior::Pause);
$runner->breakWhen(fn () => Order::where('status', 'failed')->count() > 10, 'too-many-failures');

Full reference

Time-Series Metrics

Record named metrics from handlers for post-run analysis:

$this->metric('teams.active', Team::count());     // gauge
$this->increment('invoices.created', $count);      // counter
$series = MetricQuery::series('teams.active');      // query after run

Full reference

Speed Schedules

Define multi-phase speed profiles for a single run:

$runner->speedSchedule([
    ['until' => '7 days', 'speed' => 3600],
    ['until' => 'end',    'speed' => 60],
]);

Full reference

Tick Middleware

Wrap the entire tick cycle with before/after logic:

class LogTickDuration implements TickMiddleware
{
    public function handle(TickContext $context, \Closure $next): void
    {
        $start = microtime(true);
        $next($context);
        logger("Tick {$context->tick} took " . round(microtime(true) - $start, 3) . 's');
    }
}

Full reference

Simulation Report

An automatic end-of-run summary with handler performance, event counts, and telemetry:

╔══════════════════════════════════════════╗
║     Cadence Simulation Summary           ║
╚══════════════════════════════════════════╝

  Scenario: growth
  Ticks: 168
  Speed: 3600x
  Reason: completed
  Real elapsed: 187.4s
  Events logged: 1,247

  Handler Performance
+------------------------+-------+---------+----------+----------+
| Handler                | Ticks | Elapsed | Avg (ms) | P95 (ms) |
+------------------------+-------+---------+----------+----------+
| TeamLifecycleHandler   | 168   | 187.4s  | 22.31    | 189.42   |
| ProjectActivityHandler | 168   | 185.2s  | 1.24     | 5.87     |
| BillingHandler         | 168   | 185.3s  | 2.14     | 3.21     |
| EscalationHandler      | 168   | 185.3s  | 0.58     | 1.12     |
+------------------------+-------+---------+----------+----------+

Full reference

And More...

Documentation

Topic Description
Getting Started Installation, first handler, first run
Handlers TickHandler, BaseHandler, config, logging, TickContext API
Scenarios Scenario interface, parameters, registration
Tick Scheduling The DSL: every(), businessHours(), weekdays(), between(), on()
Running Simulations CLI options, presets, controlling, monitoring
Handler Dependencies Dependency declarations, topological sort, exceptions
Invariants & Breakpoints Assertions, pause/throw/log behaviors, breakpoints
Middleware Tick middleware, before/after pattern, short-circuiting
Telemetry & Metrics Handler performance, time-series metrics, MetricQuery
Speed Control Speed schedules, adaptive speed, quiet period skipping
Snapshots Taking, restoring, metadata hooks, auto-checkpoints
Simulation Lifecycle Warm-up, tagging, dry-run, reproducibility, report
Handler Communication TickBag, config hot-reload
Event Logging Writing, querying, exporting, streaming, CadenceEventType
Eloquent Scopes Clock-aware createdSince, updatedSince, between scopes
Database Drivers MySQL/PG vs SQLite, cache driver selection
Testing TickContext::factory(), handler testing patterns

Requirements

  • PHP 8.3+
  • Laravel 12+
  • MySQL or PostgreSQL recommended (see Database Drivers)
  • Database queue driver (during simulation only)
  • Cross-process cache driver (file, database, Redis, or Memcached)

Database Drivers

MySQL and PostgreSQL are recommended. SQLite works for quick local tests but causes lock contention under real simulation loads. See Database Drivers for configuration details and workarounds.

Safety

  • Environment gatingcadence:run refuses to execute in environments not listed in allowed_environments
  • Concurrent run prevention — checks for an already-running simulation before starting
  • Queue driver enforcement — validates QUEUE_CONNECTION=database at runtime
  • Cache driver validation — rejects array and null cache drivers that can't communicate across processes
  • Tick timeoutmax_tick_duration prevents a single tick from blocking the simulation indefinitely
  • Clock cleanupsetTestNow is always reset in a finally block, even on crash
  • Snapshot confirmationcadence:restore requires interactive confirmation before replacing the database
  • Dependency validation — circular and missing handler dependencies are caught at boot, not mid-simulation
  • Dry-run isolation--dry-run wraps the entire simulation in a transaction, rolling back all data changes

About

Laravel Cadence was co-authored by Zac Hiler and Claude. Learn more about the journey of building Laravel Cadence at zachiler.dev/projects/laravel-cadence.

License

MIT. See LICENSE.