Beboost

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.

ColumnTypeRequiredDescription
idbigintautoAuto-incrementing primary key
namestring(255)yesHuman-readable label displayed in the UI (e.g. "Pending Review")
codestring(255)yesUnique machine-readable slug used in code (e.g. PENDING). Unique per entity_type
descriptionstring(1000)noOptional longer explanation of what this status means
colorstring(8)noHex color for badges, icons, and charts (e.g. #00BA54)
iconstring(255)noHeroicon identifier (e.g. heroicon-o-clock)
entity_typestring(128)yesFully-qualified class name of the model this status belongs to (e.g. App\Models\Order)
is_finalbooleannoWhen true, the status is terminal — no further transitions are allowed. Default: false
is_defaultbooleannoWhen true, this status is auto-assigned to new records on creation. Default: false
initialbooleannoWhen true, this status can be set as the very first status on a record that has no status yet. Default: false
require_commentbooleannoWhen true, a comment is mandatory when transitioning to this status. Default: false
sort_ordersmallintnoControls display order in dropdowns and admin UI. Lower values appear first. Default: 0
tenant_idbigintnoForeign key to the tenant. Only present when tenant_aware is enabled in config
created_at / updated_attimestampautoStandard 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).

ColumnTypeRequiredDescription
idbigintautoAuto-incrementing primary key
status_idbigintyesForeign key → statuses.id. Cascades on delete
entity_idbigintyesPrimary key of the model instance (e.g. the order ID)
entity_typestring(128)yesFully-qualified class name of the model
commenttextnoOptional text note attached to this status assignment (e.g. rejection reason)
tenant_idbigintnoForeign key to the tenant. Only present when tenant_aware is enabled in config
created_at / updated_attimestampautoStandard 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).

ColumnTypeRequiredDescription
idbigintautoAuto-incrementing primary key
entity_typestring(128)yesFully-qualified class name of the model
from_status_idbigintyesForeign key → statuses.id — the source status
to_status_idbigintyesForeign key → statuses.id — the target status
created_at / updated_attimestampautoStandard 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.

ColumnTypeRequiredDescription
transition_idbigintyesForeign key → status_transitions.id
role_id / permission_idbigintyesForeign 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.

ColumnTypeRequiredDescription
entity_typestring(128)yesPrimary key — fully-qualified class name of the model
strict_transitionsbooleannoWhen true, only transitions defined in the matrix are allowed. Default: true
created_at / updated_attimestampautoStandard 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:

MemberDescription
$model->statusCurrent Status model instance
$model->statusCodeCurrent status code string
$model->hasStatus('CODE')Boolean check by code
$model->status = $statusAssign 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 transition

Custom 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=status

Level 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 ms

Cache 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:optimize

Add it to your deployment pipeline (Forge, Envoyer, GitHub Actions):

php artisan filament:optimize
php artisan optimize
php artisan config:cache

To clear the Filament cache during development:

php artisan filament:optimize-clear

FrankenPHP

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 redis

Key benefits when running Status Management under FrankenPHP:

BenefitDetails
Persistent in-memory cacheStatus/transition data survives across requests per worker
Zero cold-start overheadNo repeated bootstrap for each request
Built-in HTTPSCaddy handles TLS automatically
HTTP/2 & HTTP/3Multiplexed asset delivery out of the box
Lower infrastructure costFewer 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_values

The 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 via php artisan status-management:import-spatie {model} {column=state}, which reads the model's getStateConfig() and creates matching statuses for each state class.

Next Steps

How is this guide?

On this page