Architecture
Package structure, data model, caching, multi-tenancy and Spatie integration
An overview of the main building blocks inside the Status Management package and how they fit together.
Data Model
The package introduces several database tables alongside a trait that wires them into your Eloquent models.
statuses
Defines every possible state for a given model type.
| Column | Type | Required | Description |
|---|---|---|---|
id | bigint | auto | Auto-incrementing primary key |
name | string(255) | yes | Human-readable label displayed in the UI (e.g. "Pending Review") |
code | string(255) | yes | Unique machine-readable slug used in code (e.g. PENDING). Unique per entity_type |
description | string(1000) | no | Optional longer explanation of what this status means |
color | string(8) | no | Hex color for badges, icons, and charts (e.g. #00BA54) |
icon | string(255) | no | Heroicon identifier (e.g. heroicon-o-clock) |
entity_type | string(128) | yes | Fully-qualified class name of the model this status belongs to (e.g. App\Models\Order) |
is_final | boolean | no | When true, the status is terminal — no further transitions are allowed. Default: false |
is_default | boolean | no | When true, this status is auto-assigned to new records on creation. Default: false |
initial | boolean | no | When true, this status can be set as the very first status on a record that has no status yet. Default: false |
require_comment | boolean | no | When true, a comment is mandatory when transitioning to this status. Default: false |
sort_order | smallint | no | Controls display order in dropdowns and admin UI. Lower values appear first. Default: 0 |
tenant_id | bigint | no | Foreign key to the tenant. Only present when tenant_aware is enabled in config |
created_at / updated_at | timestamp | auto | Standard Laravel timestamps |
status_values
Polymorphic pivot table — links a specific entity instance to its current Status. Each record has exactly one current status (the combination of entity_type + entity_id is unique).
| Column | Type | Required | Description |
|---|---|---|---|
id | bigint | auto | Auto-incrementing primary key |
status_id | bigint | yes | Foreign key → statuses.id. Cascades on delete |
entity_id | bigint | yes | Primary key of the model instance (e.g. the order ID) |
entity_type | string(128) | yes | Fully-qualified class name of the model |
comment | text | no | Optional text note attached to this status assignment (e.g. rejection reason) |
tenant_id | bigint | no | Foreign key to the tenant. Only present when tenant_aware is enabled in config |
created_at / updated_at | timestamp | auto | Standard Laravel timestamps |
status_transitions
Defines every allowed from → to path for a given entity_type. Roles and permissions are managed via separate pivot tables (see below).
| Column | Type | Required | Description |
|---|---|---|---|
id | bigint | auto | Auto-incrementing primary key |
entity_type | string(128) | yes | Fully-qualified class name of the model |
from_status_id | bigint | yes | Foreign key → statuses.id — the source status |
to_status_id | bigint | yes | Foreign key → statuses.id — the target status |
created_at / updated_at | timestamp | auto | Standard Laravel timestamps |
The combination of (entity_type, from_status_id, to_status_id) is unique.
status_transitions_roles / status_transitions_permissions
Pivot tables linking transitions to Spatie roles and permissions. When a transition has associated roles, only users with one of those roles can perform it.
| Column | Type | Required | Description |
|---|---|---|---|
transition_id | bigint | yes | Foreign key → status_transitions.id |
role_id / permission_id | bigint | yes | Foreign key → Spatie roles.id or permissions.id |
status_entity_configs
Per-model configuration. Controls whether strict transition mode is enabled for a given entity type.
| Column | Type | Required | Description |
|---|---|---|---|
entity_type | string(128) | yes | Primary key — fully-qualified class name of the model |
strict_transitions | boolean | no | When true, only transitions defined in the matrix are allowed. Default: true |
created_at / updated_at | timestamp | auto | Standard Laravel timestamps |
HasStatus Trait
Add HasStatus to any Eloquent model to enable status management:
use Beboost\StatusManagement\Models\HasStatus;
use Beboost\StatusManagement\Models\Contracts\Statusable;
class Order extends Model implements Statusable
{
use HasStatus;
}The trait provides:
| Member | Description |
|---|---|
$model->status | Current Status model instance |
$model->statusCode | Current status code string |
$model->hasStatus('CODE') | Boolean check by code |
$model->status = $status | Assign via Status, int, or code string |
scopeStatus('CODE') | Eloquent query scope by code |
scopeStatusId($id) | Eloquent query scope by ID |
Transition Pipeline
Every status change is funneled through TransitionManager::transit(), which runs a deterministic validation pipeline before persisting anything.
TransitionManager::transit($record, $toStatusId)
│
▼
TransitionCheckerManager::canTransitTo(TransitionContext)
│
├── 1. Initial Rule new record → is target marked as initial?
├── 2. Matrix Rule existing record → is from→to path defined in status_transitions?
├── 3. Permission Rule does the authenticated user hold the required Spatie role/permission?
└── 4. Custom Rules any additional business-rule checkers registered by your app
│
▼
TransitionResult
├── isSuccess() → status saved, change logged via Spatie Activity Log
└── getFailedRule() → validation exception with the rule that blocked the transitionCustom Rules
Register application-specific checkers in a service provider:
use Beboost\StatusManagement\Transitions\TransitionCheckerManager;
use App\StatusRules\OrderFulfillmentRule;
TransitionCheckerManager::register(OrderFulfillmentRule::class);Each checker must implement TransitionCheckerContract and return a TransitionResult.
Cache & Optimization
Two-Level Caching
The package ships with two independent caching layers that work together to eliminate redundant database queries.
Level 1 — Laravel Cache (persistent)
Statuses and transition matrices are stored in your application's configured cache store (file, database, Redis, Memcached, etc.). The cache is keyed per entity_type and is automatically invalidated whenever a status or transition is created, updated, or deleted.
// config/status-management.php
'cache' => [
'store' => env('STATUS_MANAGEMENT_CACHE_STORE', 'default'), // any Laravel cache store
'ttl' => env('STATUS_MANAGEMENT_CACHE_TTL', 3600), // seconds
],To use a dedicated Redis instance for status data, point STATUS_MANAGEMENT_CACHE_STORE to a separate cache store defined in config/cache.php:
// config/cache.php
'stores' => [
'status' => [
'driver' => 'redis',
'connection' => 'status',
],
],STATUS_MANAGEMENT_CACHE_STORE=statusLevel 2 — In-memory (request-scoped)
Within a single HTTP request, resolved statuses and transition maps are held in a static registry on the manager class. This means repeated calls to $model->status or permission checks within one request hit zero external I/O — no Redis round-trips, no database queries.
Request
└─► StatusRepository::forEntity('App\Models\Order')
├─ (hit) in-memory registry → instant
├─ (hit) Laravel Cache (Redis, etc.) → ~0.1 ms
└─ (miss) database query + warm both → ~2–5 msCache is warmed automatically on first access and invalidated via model observers on any write.
Asset Optimization
Run Filament's built-in optimize command before deploying to production. It compiles and caches Filament's blade components, icons, and config:
php artisan filament:optimizeAdd it to your deployment pipeline (Forge, Envoyer, GitHub Actions):
php artisan filament:optimize
php artisan optimize
php artisan config:cacheTo clear the Filament cache during development:
php artisan filament:optimize-clearFrankenPHP
For maximum throughput we recommend running the application under FrankenPHP — a modern PHP application server built on top of Caddy. Unlike traditional FPM setups, FrankenPHP runs your Laravel application in worker mode: the bootstrap cycle executes once and the application stays resident in memory between requests.
This makes the in-memory caching layer (Level 2 above) persistent across requests — statuses and transition matrices are loaded once per worker process and shared for the lifetime of that worker.
# Dockerfile (FrankenPHP worker mode)
FROM dunglas/frankenphp
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
COPY . /app
RUN install-php-extensions intl opcache pdo_mysql redisKey benefits when running Status Management under FrankenPHP:
| Benefit | Details |
|---|---|
| Persistent in-memory cache | Status/transition data survives across requests per worker |
| Zero cold-start overhead | No repeated bootstrap for each request |
| Built-in HTTPS | Caddy handles TLS automatically |
| HTTP/2 & HTTP/3 | Multiplexed asset delivery out of the box |
| Lower infrastructure cost | Fewer workers needed for the same RPS |
Worker mode requires your application to be stateless between requests. Review FrankenPHP worker mode docs before enabling in production.
Multi-Tenancy
When config('status-management.tenant_aware') is true, a TenantScope is automatically applied to the Status and StatusValue models, isolating all queries to the current tenant.
// config/status-management.php
'tenant_aware' => true,
'tenant_foreign_key' => 'team_id', // column on statuses / status_valuesThe scope resolves the current tenant from Filament::getTenant() (when inside a Filament panel) or from the value returned by the configured tenant resolver. All status queries, cache keys, and transition lookups are automatically namespaced per tenant — no manual filtering required.
Multi-tenancy must be enabled before running migrations. Enabling it on an existing single-tenant installation requires a data migration to backfill the tenant foreign key.
Spatie Compatibility
The package integrates with several Spatie packages:
- spatie/laravel-activitylog (
^4.0) — required for status change tracking. All transitions are logged with from/to status, the user who made the change, optional comments, and a timestamp. Learn more → - spatie/laravel-permission (
^6.0) — optional, for role-based status transition control: base permissions per entity type, per-transition role restrictions, and UI permission management. Learn more → - spatie/laravel-model-states (
^2.0) — migration support viaphp artisan status-management:import-spatie {model} {column=state}, which reads the model'sgetStateConfig()and creates matching statuses for each state class.
Next Steps
How is this guide?