alex-kassel / history-engine
Storage-agnostic history and state navigation engine for PHP 8.2+ and Laravel applications.
Requires
- php: ^8.2 || ^8.3 || ^8.4
- illuminate/cache: ^11.0 || ^12.0 || ^13.0
- illuminate/contracts: ^11.0 || ^12.0 || ^13.0
- illuminate/redis: ^11.0 || ^12.0 || ^13.0
- illuminate/session: ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- phpunit/phpunit: ^11.0 || ^12.0
README
Storage-agnostic history navigation, state tracking, and undo/redo pointer engine for PHP 8.2+ and Laravel applications
Installation • Quick Start • UI Predicates & Counters • String Commands • Drivers • Release Gate • Changelog
History Engine provides a clean, storage-agnostic state history navigation engine for Laravel applications. It manages linear item histories, navigation pointers, branching truncation, and undo/redo operations across multiple persistent backends (Session, Cache, Redis).
Key Features
- Multi-Backend Storage: Seamlessly switch between
session,cache, andredisstorage drivers, or register custom stores viaHistoryEngine::extend(). - Isolated Scopes: Create isolated history stacks per user session, workflow step, or UI component via
HistoryEngine::engine('scope_name'). - Bidirectional Step Counters: Query available undo/redo steps (
backCount(),forwardCount()) for browser-like button badges. - UI Predicates & Peek: Instant status checks (
canStepBack(),canStepForward(),isAtStart(),isAtEnd()) and non-destructive peek methods (peekBack(),peekForward()). - Frontend DTO Snapshot: Export complete state via
snapshot()(HistorySnapshotimplementingJsonSerializableandArrayable) for Vue, React, Livewire, and Inertia. - Automatic Branch Truncation: Recording a new item while positioned in the middle of history automatically truncates future forward steps (browser-like navigation semantics).
- Consecutive Deduplication: Automatically avoids recording redundant duplicate entries when the current pointer already matches the payload.
- String Command DSL: Quick navigation with shorthand command syntax (
<,>,<<,>>,<2,>3,<>index,@). - First-Class IDE DX: 100% strictly typed API and Facade autocomplete annotations passing PHPStan at level Max.
Requirements
- PHP: 8.2+ (tested on 8.2, 8.3, 8.4)
- Laravel Framework: 11.x | 12.x | 13.x
Installation
Install the package via Composer:
composer require alex-kassel/history-engine
The Service Provider and HistoryEngine facade will register automatically via Laravel package discovery.
Optionally publish the configuration file:
php artisan vendor:publish --tag="history-engine-config"
Configuration
The published config/history-engine.php allows setting the default store, TTL, and driver settings:
return [ // Default driver: session | cache | redis 'default' => env('HISTORY_ENGINE_DRIVER', 'session'), // Key prefix used across storage drivers 'prefix' => env('HISTORY_ENGINE_PREFIX', 'history_engine:'), 'drivers' => [ 'session' => [ 'class' => \AlexKassel\HistoryEngine\Stores\SessionHistoryStore::class, ], 'cache' => [ 'class' => \AlexKassel\HistoryEngine\Stores\CacheHistoryStore::class, 'store' => env('HISTORY_ENGINE_CACHE_STORE', null), 'ttl' => env('HISTORY_ENGINE_TTL', null), // seconds (null = forever) ], 'redis' => [ 'class' => \AlexKassel\HistoryEngine\Stores\RedisHistoryStore::class, 'connection' => env('HISTORY_ENGINE_REDIS_CONNECTION', null), 'ttl' => env('HISTORY_ENGINE_TTL', null), // seconds (null = forever) ], ], ];
Quick Start
1. Basic Recording & Navigation
use AlexKassel\HistoryEngine\Facades\HistoryEngine; // Direct facade usage with default scope, or specify custom scope HistoryEngine::record('filter:audi', 'search-filters'); HistoryEngine::record('filter:audi-q4', 'search-filters'); HistoryEngine::record('filter:audi-q4-2024', 'search-filters'); // Inspect state HistoryEngine::getCurrent('search-filters'); // 'filter:audi-q4-2024' HistoryEngine::getPointer('search-filters'); // 2 HistoryEngine::getAll('search-filters'); // ['filter:audi', 'filter:audi-q4', 'filter:audi-q4-2024'] // Navigate backwards & forwards HistoryEngine::stepBack(1, 'search-filters'); // 'filter:audi-q4' HistoryEngine::stepBack(1, 'search-filters'); // 'filter:audi' HistoryEngine::stepForward(1, 'search-filters'); // 'filter:audi-q4' // Jump directly to boundaries or indices HistoryEngine::goToStart('search-filters'); // 'filter:audi' HistoryEngine::goToEnd('search-filters'); // 'filter:audi-q4-2024' HistoryEngine::goToIndex(1, 'search-filters'); // 'filter:audi-q4'
2. Method Chaining
$engine = HistoryEngine::engine('wizard') ->clear() ->record('step-1') ->record('step-2') ->record('step-3');
UI Predicates & Step Counters
Perfect for rendering interactive Back/Forward navigation in Blade, Livewire, Inertia, Vue, or React:
$engine = HistoryEngine::engine('catalog'); $engine->canStepBack(); // bool (true if pointer > 0) $engine->canStepForward(); // bool (true if forward steps exist) $engine->backCount(); // int (e.g. 3 steps available backward) $engine->forwardCount(); // int (e.g. 2 steps available forward) $engine->isAtStart(); // bool (true if at first item) $engine->isAtEnd(); // bool (true if at last item) $engine->isEmpty(); // bool $engine->count(); // int (total history items, implements \Countable) // Peek surrounding items without moving pointer $previous = $engine->peekBack(); // 'step-2' (pointer remains unchanged!) $next = $engine->peekForward(); // 'step-4'
3. Frontend JSON Snapshot
// In a Laravel Controller / Inertia response: return response()->json(HistoryEngine::engine('filters')->snapshot()); // Output JSON: // { // "scope": "filters", // "current": "audi-q4", // "pointer": 2, // "total": 5, // "items": ["all", "audi", "audi-q4", "audi-q4-2024", "audi-q4-ev"], // "can_step_back": true, // "can_step_forward": true, // "back_count": 2, // "forward_count": 2, // "is_at_start": false, // "is_at_end": false, // "is_empty": false // }
String Commands
The package includes a concise command DSL for driving navigation from UI requests, URL query parameters, or keyboard shortcuts:
// Back / Forward single step HistoryEngine::applyCommand('<', 'wizard'); // Step back HistoryEngine::applyCommand('>', 'wizard'); // Step forward // Multi-step jumps HistoryEngine::applyCommand('<2', 'wizard'); // Step back 2 items HistoryEngine::applyCommand('>3', 'wizard'); // Step forward 3 items // Jump to start / end HistoryEngine::applyCommand('<<', 'wizard'); // Go to start (first item) HistoryEngine::applyCommand('>>', 'wizard'); // Go to end (latest item) // Jump to specific 0-based index HistoryEngine::applyCommand('<>2', 'wizard'); // Go to index 2 // Clear history stack HistoryEngine::applyCommand('@', 'wizard');
Drivers
Using Specific Drivers Directly
// Use Redis driver explicitly for long-lived background jobs $redisEngine = HistoryEngine::driver('redis'); // Register custom storage drivers HistoryEngine::extend('database', function ($app, array $config) { return new MyDatabaseHistoryStore(); });
Testing
Run unit and integration test suites:
# Run PHPUnit tests php artisan test -c packages/alex-kassel/history-engine/phpunit.xml # Run PHPStan static analysis at level max vendor/bin/phpstan analyse packages/alex-kassel/history-engine/src --level=max
Changelog
Please see CHANGELOG.md for more information on what has changed recently.
Security Vulnerabilities
Please review Security Policies on how to report vulnerabilities.
License
The MIT License (MIT). Please see License File for more information.