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

Doctrine State Machine Bundle Laravel Package

kphoen/doctrine-state-machine-bundle

Symfony bundle that integrates the Doctrine finite state machine library, letting you define state machines and transitions in configuration and expose them as container services for clean, testable workflow logic in your PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. Before writing any code, internalize what that means:

  • It integrates with Symfony's DI container, bundle extension/config system, and Doctrine ORM — none of which Laravel provides out of the box.
  • It is marked DEPRECATED, last released in 2016, with 0 dependents. Treat it as reference material for the state-machine pattern, not a dependency you should ship to production.
  • Laravel's default ORM is Eloquent, not Doctrine. You will either bring Doctrine into Laravel (laravel-doctrine/orm) or, more realistically, port the idea to Eloquent.

First steps if you are evaluating it:

  1. Read the README and the bundle's DependencyInjection/Configuration.php to understand the config shape (states, transitions, callbacks).
  2. Inspect the underlying state-machine library and the service wiring. The bundle is mostly glue; the reusable logic lives below it.
  3. Check composer.json constraints. The package predates modern Symfony/PHP versions and will likely conflict with current dependencies.
  4. Spin it up in a throwaway Symfony app (or a Docker image pinned to the era) to see the intended developer experience before trying to force it into Laravel.

First meaningful use case: an Order entity with pending → paid → shipped → cancelled transitions. You want guards (canTransitionTo), side effects on transition (email, audit log), and persistence of the new state.

Minimal Laravel-shaped setup:

composer require laravel-doctrine/orm   # if you truly need Doctrine
# do NOT expect `composer require kphoen/doctrine-state-machine-bundle` to "just work"
// app/Providers/StateMachineServiceProvider.php
public function register(): void
{
    // Manually construct whatever factory/registry the bundle exposes.
    // You will not get Symfony's bundle bootstrapping or config merging.
}

If you only need the behavior (and you almost certainly do), skip the bundle and implement the pattern against Eloquent — see below.

Implementation Patterns

1. Translate the Symfony concepts into Laravel concepts

Symfony bundle concept Laravel equivalent
Bundle + Extension Service provider + config/state_machines.php
DI service definitions Container bindings in register()
YAML/XML config PHP config array
Doctrine entity listener/subscriber Eloquent observer or explicit service call
Symfony event dispatcher Laravel events/listeners
Container compiler pass Manual wiring or a small factory

2. Manual wiring in a service provider

The bundle assumes a Symfony container. In Laravel you must build the registry yourself:

public function register(): void
{
    $this->app->singleton(StateMachineRegistry::class, function ($app) {
        return new StateMachineRegistry(config('state_machines'));
    });
}

Keep the bundle's factory/config objects isolated behind your own interface so you can swap them for a hand-rolled implementation later.

3. Config as a PHP array

Do not try to reuse the bundle's YAML loader. Store definitions in config/state_machines.php:

return [
    'order' => [
        'property' => 'status',
        'states'   => ['pending', 'paid', 'shipped', 'cancelled'],
        'transitions' => [
            'pay'     => ['from' => ['pending'], 'to' => 'paid'],
            'ship'    => ['from' => ['paid'],    'to' => 'shipped'],
            'cancel'  => ['from' => ['pending', 'paid'], 'to' => 'cancelled'],
        ],
    ],
];

4. Entity/Model integration pattern

If using Doctrine entities in Laravel, keep the state property private and expose intent-revealing methods. Never let controllers set the status field directly.

class Order
{
    private string $status = 'pending';

    public function pay(): void
    {
        $this->applyTransition('pay');
    }

    private function applyTransition(string $name): void
    {
        // delegate to the configured machine; throw on invalid transition
    }
}

For Eloquent, this becomes a trait:

trait HasStateMachine
{
    public function canTransitionTo(string $transition): bool { /* ... */ }

    public function transitionTo(string $transition): void
    {
        DB::transaction(function () use ($transition) {
            if (! $this->canTransitionTo($transition)) {
                throw new InvalidTransition($transition);
            }
            $this->{config('state_machines.'.$this->stateMachineKey().'.property')}
                = $this->nextState($transition);
            $this->save();
            event(new StateTransitioned($this, $transition));
        });
    }
}

5. Guard + transaction pattern

State machines are only useful if transitions and side effects are atomic. Wrap the transition, the persistence, and any domain event dispatch in a single transaction. Use optimistic locking (updated_at check or a version column) to prevent two concurrent requests from both transitioning pending → paid.

6. Callbacks vs. events

The bundle supports callbacks around transitions. In Laravel, prefer events/listeners for side effects (email, audit) and reserve synchronous guards for invariants (e.g., "cannot ship an unpaid order"). This keeps the state machine pure and testable.

7. Testing pattern

  • Unit-test transition tables without a database: assert every from/to pair and every invalid pair.
  • Integration-test persistence separately with in-memory SQLite (Doctrine) or the default test database (Eloquent).
  • Assert that a failed guard leaves the persisted state unchanged — this catches transaction-boundary bugs.

8. Recommended migration path

Given the deprecation, the practical "day-to-day leverage" is:

  • Use symfony/workflow standalone (it does not require the full framework) if you want a maintained FSM library.
  • Or implement the ~100-line Eloquent trait above. For most Laravel apps this is lower risk than introducing Doctrine plus an abandoned Symfony bundle.

Gotchas and Tips

  • Abandonment risk is the headline. Deprecated, last release 2016, zero dependents. Expect PHP/Symfony version conflicts, unpatched bugs, and no security response. Do not put it on a critical path.
  • No Laravel auto-discovery. There is no extra.laravel provider entry. You must register everything manually; php artisan vendor:publish will not work.
  • Symfony DI assumptions. The bundle relies on compiler passes, service tags, and container parameters. Laravel's container has no equivalent lifecycle; wiring by hand often exposes hidden dependencies one at a time. Budget time for this.
  • Config caching mismatch. Symfony's config tree validation/merging won't run in Laravel. Validate your config/state_machines.php yourself at boot, or malformed definitions will fail at runtime instead of deploy time.
  • Two ORMs, two transaction boundaries. If you mix Doctrine entities with Eloquent models, DB::transaction() does not cover Doctrine's unit of work. A rollback in one will not roll back the other. Keep state transitions entirely on one side.
  • Concurrency. Without optimistic locking, two simultaneous requests can both read pending and both write paid. Add a version column or a conditional UPDATE ... WHERE status = ?.
  • Callback ordering and exceptions. If a post-transition callback throws after the state was written, you can persist an inconsistent state unless the whole thing is transactional. Verify the bundle's callback order in the source before relying on it.
  • Field mapping/hydration. If the machine reads state via a getter, ensure the Doctrine metadata mapping matches the property name exactly. Silent mismatch shows up as "always in initial state."
  • Event/listener registration. Symfony's dispatcher wiring does not exist in Laravel. If you port callbacks to Laravel events, register them explicitly in EventServiceProvider and test they actually fire.
  • Debugging tips: dump the transition graph at boot (php artisan tinker), log every attempted transition with from/to/actor, and assert canTransitionTo() in tests before calling transitionTo().
  • Extension points worth keeping: custom guard callbacks, a decorator around the machine factory, and listeners on transition events. These are the seams that let you later replace the bundle without touching domain code.
  • Bottom line for a Laravel team: read the source to learn the pattern, then implement it natively with Eloquent or adopt symfony/workflow standalone. Shipping the bundle itself into a Laravel app is high effort, high risk, and low long-term value.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource