alex-kassel/history-engine

Storage-agnostic history and state navigation engine for PHP 8.2+ and Laravel applications.

Maintainers

Package info

github.com/alex-kassel/history-engine

pkg:composer/alex-kassel/history-engine

Transparency log

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.3.0 2026-08-26 00:15 UTC

This package is auto-updated.

Last update: 2026-08-26 00:41:37 UTC


README

Storage-agnostic history navigation, state tracking, and undo/redo pointer engine for PHP 8.2+ and Laravel applications

InstallationQuick StartUI Predicates & CountersString CommandsDriversRelease GateChangelog

Audit Verified Latest Version Laravel Support PHP Support PHPStan Level Max

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, and redis storage drivers, or register custom stores via HistoryEngine::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() (HistorySnapshot implementing JsonSerializable and Arrayable) 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.