karriere/state
A Laravel state machine for Eloquent models. Define states and transitions, guard changes, fire events, and keep a history of every transition—ideal for orders, payments, approvals, and other workflow-driven records.
Check the version gate before anything else. v3.3.0 raised the floor and dropped two supported stacks: PHP 8.1 is gone (8.2 minimum), illuminate/support and illuminate/session v10 are gone (Laravel 11+), and psr/cache v1/v2 are gone (v3 only). Confirm your matrix before you composer require:
php -v # need 8.2+ (8.5 now supported)
composer show laravel/framework | grep versions
composer why psr/cache # anything pinning ^1 or ^2 blocks 3.3
If you are on Laravel 10 or PHP 8.1, v3.2.x is your terminal branch — see the pinning note in Gotchas.
Install and confirm the wiring. The package is a thin state store over Laravel's cache and session stores, so the install is the standard Composer flow:
composer require karriere/state
Laravel's package discovery should register the service provider automatically. Before writing any code, read these three things in this order:
README.md — confirm the facade alias, config key names, and driver options. With 17 stars and no dependents, the README is the contract; treat source as the source of truth.src/StateServiceProvider.php — check whether a config file is published and what the default driver resolves to.get/set/forget/has or similar) and whether values are typed or serialized.If a config file is publishable, publish and inspect it:
php artisan vendor:publish --provider="Karriere\State\StateServiceProvider"
First use case: a per-request/per-user flag that outlives a single controller call. Typical candidates are wizard progress in a multi-step form, a "currently selected tenant/branch" for the session, or a maintenance/feature flag read on every request. Start with the session driver for per-user state, and the cache driver only when you need the value to be visible across users or across queued jobs.
use Karriere\State\Facades\State; // confirm alias in README
State::set('checkout.step', 2);
$step = State::get('checkout.step', 1);
Get one read and one write working end-to-end (assert it survives a redirect) before wiring it into middleware or Blade. The v3.3.0 changelog is a good sign of maintenance — PHP 8.5 support landed ahead of most packages — but the release cadence is the thing to watch, not abandonment.
Session driver — per-user, per-session state. Use this for UI flow state: wizard steps, dismissed banners, "last viewed" filters, draft form data. The value dies with the session, which is usually what you want and is the safest default:
// Controller
State::set('onboarding.step', 3);
// Blade
@if (State::get('onboarding.dismissed_tour', false))
...
@endif
Cache driver — shared or cross-request state. Use this for state that must be readable outside the user's request lifecycle: a global "banner enabled" flag, a deployment marker, a rate-limit-style counter, or state a queued job needs to read. Set an explicit TTL rather than relying on a default:
State::set('import.running', true, now()->addMinutes(30));
Hydrate once in middleware. Rather than scattering State::get() calls through controllers and views, resolve the values you need in a middleware and share them, or inject a small wrapper service. This keeps the third-party API at the boundary of your app and makes it trivial to swap out:
// app/Http/Middleware/ShareAppState.php
public function handle($request, Closure $next)
{
view()->share('appState', [
'maintenance' => State::get('maintenance', false),
'tenant' => State::get('tenant'),
]);
return $next($request);
}
Key namespacing discipline. The package almost certainly does not enforce key structure, so define your own convention (domain.entity.attribute) and centralise it in constants or an enum. This is the single highest-leverage thing you can do for day-to-day sanity — it makes cache-key debugging and forget calls predictable.
Integration with jobs and events. If a job needs to read or mutate state, the cache driver is the only one that makes sense — sessions do not exist in a queue worker. Pass the key explicitly into the job rather than relying on ambient state, and write the state change at the end of the job so a failure does not leave a "running" flag stuck.
Testing. Use Laravel's own fakes rather than the package's internals: Cache::spy() / Session::start() plus assertions on the underlying store, or Cache::shouldReceive('put'). Assert on behaviour (does the flag flip, does the redirect preserve the step) rather than on the package's API surface — that keeps your tests valid if you replace the package with Cache::/session() calls later. Add PHP 8.2/8.3/8.4/8.5 to your CI matrix now that 8.1 is out of scope for v3.3.
v3.3.0 is a minor-version breaking change. Dropping PHP 8.1 and Laravel 10 in a 3.3.0 bump is a semver stretch, and it means the floor can move again on any 3.x patch. Pin exactly ("karriere/state": "3.3.0") or with a tight ~3.3.0, and read the changelog before every bump — a composer update that silently raises your PHP minimum is a bad CI surprise.
Laravel 10 / PHP 8.1 users are on a dead-end branch. Composer will resolve you to 3.2.x and it will keep working, but you get no further bug or security fixes there. Pin ^3.2 explicitly so the resolution is visible in composer.json, and put a Laravel 11/PHP 8.2 upgrade on the roadmap — the package has effectively announced where you need to be.
psr/cache v1/v2 removal can block the upgrade for unrelated reasons. If a transitive dependency still pins psr/cache: ^2.0, you cannot install v3.3 at all, and the conflict error will name psr/cache rather than this package. Run composer why psr/cache before assuming the blocker is the package itself; it also confirms the cache driver is built over a PSR-6 pool, so any custom PSR-6 store you plug in must be v3.
The session driver is silently unavailable in the wrong contexts. API routes, queue workers, console commands, and anything without StartSession middleware will either throw or (worse) write to a throwaway session. Guard with request()->hasSession() or route all background/job state through the cache driver.
Cache keys are not namespaced for you. Check whether the package prefixes keys with a configured string. If it does not, your keys collide with anything else sharing the same cache store — especially on a shared Redis database. Add your own prefix and confirm the resolved key with Cache::getStore() inspection during debugging.
config:cache will freeze your driver choice. If you publish a config, remember that a stale cached config is the most common "it worked locally" failure for small packages. After changing the config: php artisan config:clear (and cache:clear if the state itself lives in the cache store).
Serialization bites with object state. Store scalars and arrays only. Caching a model or DTO couples your state to the class definition and will produce confusing deserialization errors after a deploy. If you must store an object, store its ID.
No locking. This is a read/write wrapper, not a concurrency primitive. If two requests can mutate the same key, use Cache::lock() directly around your read-modify-write rather than assuming the package handles it.
Debug with the underlying store, not the facade. When State::get() returns the wrong thing, bypass the package: check the raw key in the cache/session (Cache::get($prefixedKey), session()->all()), confirm the resolved driver from the config, and only then suspect the package. Nine times out of ten the issue is the prefixed key or the session not being started.
Extension points are likely thin. Look for a driver/manager binding in the service provider — if there is one, you can register a custom store (e.g. a database-backed driver) by rebinding the interface. If there is not, wrap the facade behind your own AppState service so you retain a seam for replacement.
Contain the blast radius anyway. The package is maintained (v3.3.0 shows active PHP tracking) but tiny — one maintainer, no dependents. Keep your usage confined to a wrapper service and your keys centralised; replacing it with Cache:: and session() calls should remain a one-file change. Version-gate your composer.json and your CI PHP matrix together, since that pair is what actually determines whether the next 3.x lands cleanly.
How can I help you explore Laravel packages today?