Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Stagehand Fsm Laravel Package

piece/stagehand-fsm

Stagehand FSM is a Laravel/PHP finite state machine package for defining states, transitions, guards, and actions. Model workflows and object lifecycles with clean, testable state-driven logic.

Deep Wiki
Context7

Getting Started

The repository link is missing and the last tagged release is from January 2018, so treat the package source itself as the documentation. Start by reading composer.json (for PHP/Laravel constraints), then src/ and tests/ — the tests are usually the most accurate usage guide for a package this age.

  1. Install and inspect what you actually got:

    composer require piece/stagehand-fsm
    # then read the installed source rather than guessing at the API
    ls vendor/piece/stagehand-fsm/src
    sed -n '1,200p' vendor/piece/stagehand-fsm/README.md
    
  2. If the package ships a service provider, Laravel 5.5+ auto-discovery may register it. If not, add it manually to config/app.php. Check for a publishable config:

    php artisan vendor:publish --tag=stagehand
    
  3. Pick one narrow workflow for the first use case: an Order moving through pending → paid → shipped → cancelled. Model it before touching invoices, refunds, or inventory. The value of an FSM is that illegal transitions become impossible to represent, so start with the smallest lifecycle that has real rules.

  4. Verify the core verbs before designing around them. A typical FSM package exposes something like: define states/transitions, attach a trait to an Eloquent model, check whether a transition is allowed, and apply it. Confirm the real method names in src/ — the examples below are illustrative, not copied from a README.

// config/stagehand.php — illustrative shape, verify keys against the package
return [
    'machines' => [
        'order' => [
            'states' => ['pending', 'paid', 'shipped', 'cancelled'],
            'transitions' => [
                'pay'    => ['from' => 'pending', 'to' => 'paid'],
                'ship'   => ['from' => 'paid',    'to' => 'shipped'],
                'cancel' => ['from' => ['pending', 'paid'], 'to' => 'cancelled'],
            ],
        ],
    ],
];
class Order extends Model
{
    use \Piece\StagehandFsm\Traits\HasStateMachine; // confirm trait name

    protected $stateMachine = 'order';
    protected $stateColumn  = 'status';
}

$order->transition('pay');       // or $order->stateMachine()->apply('pay')
$order->canTransition('ship');   // confirm availability

Implementation Patterns

Keep the transition map declarative and central. Put states and transitions in one config file or one dedicated class per domain, not scattered across controllers. When a new engineer asks "what can an order do?", there should be exactly one file to open.

Use the FSM as a routing layer, not a business-logic container. Guards should answer "is this transition legal right now?" (payment captured, stock reserved). The work that happens because of the transition — sending mail, writing ledger rows, queuing jobs — belongs in listeners or a small service that wraps the transition.

DB::transaction(function () use ($order) {
    $order->transition('ship');
    ShipmentCreated::dispatch($order);
});

Authorize before transitioning. Map transitions to policies or gates so a controller never calls transition() on an arbitrary user-supplied string. Enumerate the allowed transitions for the current state and render only those buttons; reject everything else server-side.

$this->authorize('transition', [$order, $request->input('transition')]);

Persist an audit trail. The current state column tells you where a record is, not how it got there. Add a state_transitions table (model, from, to, transition name, actor, timestamp) written in the same transaction as the state change. This is the single most useful thing you can add on day one; it pays for itself the first time support asks "why is this order cancelled?"

Test the transition matrix, not just happy paths. Write one test per legal transition and one per illegal pair. Better: a data-provider test that iterates every state/transition combination so adding a state without a test fails loudly.

/** @test */
public function a_shipped_order_cannot_be_cancelled()
{
    $order = Order::factory()->create(['status' => 'shipped']);

    $this->expectException(\Exception::class);
    $order->transition('cancel');
}

Support more than one machine per model when lifecycles genuinely differ. Payment status and fulfillment status are separate concerns with separate rules; forcing them into one state column produces a combinatorial mess. Use a named machine per concern if the package supports it, otherwise use separate columns and separate machine definitions.

Wrap transitions in jobs for slow side effects. If a transition triggers external calls, transition synchronously, commit, then dispatch. This keeps the FSM fast and avoids holding a database transaction open during an HTTP request to a shipping provider.

Gotchas and Tips

  • Age is the main risk. A 2018 release predates PHP 8 enums, attributes, and modern Laravel conventions. Check composer.json constraints; on PHP 8.1+/Laravel 10+ you may need --ignore-platform-reqs, a fork, or a maintained alternative. Budget for reading and possibly patching the source.
  • There is no repository link. Before adopting, confirm the package is still installable and that Packagist metadata points somewhere you can file issues. If not, vendor it or fork it into your own namespace so you control fixes.
  • Concurrency is not handled for you. FSM libraries generally check the in-memory state and then save; two concurrent requests can both pass the guard. Use lockForUpdate() inside a transaction, or an optimistic where status = ? update and check the affected-row count.
  • Magic strings drift. If states are plain strings, a typo in a controller silently creates an unknown state. Define constants or a backed enum wrapper around the package's state values and reference only those.
  • Event ordering matters. Know whether transition events fire before or after persistence. Side effects fired before commit can be lost on rollback; side effects fired after commit can fail while the state is already changed. Choose deliberately and make listeners idempotent.
  • Guard semantics vary. Some packages treat a guard returning false as "not allowed"; others require an exception. Read the guard implementation once and standardize on one style across the codebase.
  • Unregistered transitions fail at runtime, not compile time. Add a test that every transition name referenced in code exists in the machine definition, and that every state is reachable.
  • Don't expose transition names to clients verbatim. If the request body contains transition=ship, validate it against the machine's allowed transitions for the current state rather than trusting it.
  • Extension points to look for in src/: guard/condition callbacks, pre/post-transition hooks, custom state storage, and any event dispatcher integration. These are where you add auditing, logging, and notifications without forking.
  • Debugging tip: log from, to, transition name, and the guard result at debug level. When a transition is mysteriously rejected, the guard — not the state map — is almost always the cause.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings