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 Laravel Package

kphoen/doctrine-state-machine

Bridges Symfony's StateMachine component with Doctrine ORM, letting entity state transitions be validated and applied automatically during persistence. Configure a state machine for an entity and Doctrine listeners enforce allowed transitions, keeping workflow state consistent and domain models clea...

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install (expect friction). composer require kphoen/doctrine-state-machine. The package was last released in April 2015 and pins era-appropriate constraints (doctrine/common, symfony/workflow, sensio/framework-extra-bundle). On a modern stack Composer will refuse; do the install on a PHP 5.6/7.x + Symfony 2.8/3.x app, or vendor the source and hand-wire the services.

  2. Register the bundle/subscriber. Add the behavior bundle to AppKernel.php (pre-Symfony 4) — in a Symfony 4+ app you must register it manually in config/bundles.php or as a service, since the package predates Flex. Confirm the exact class by opening vendor/kphoen/doctrine-state-machine/.

  3. Declare the machine. The package is config-driven, keyed by entity FQCN:

    # app/config/config.yml
    doctrine_state_machine:
        machines:
            AppBundle\Entity\Order:
                states: [cart, pending, paid, shipped]
                transitions:
                    pay:    { from: [cart, pending], to: paid }
                    ship:   { from: [paid],          to: shipped }
    
  4. Attach the trait(s) to the entity. The behavior provides the state property plus getState(), can($transition) and a transition/apply method:

    use Knp\DoctrineBehaviors\Model\StateMachine\StateMachineProperties;
    use Knp\DoctrineBehaviors\Model\StateMachine\StateMachineMethods;
    
    class Order
    {
        use StateMachineProperties, StateMachineMethods;
    }
    

    Map the state column (@ORM\Column(type="string")) if the trait doesn't already do it, then run doctrine:schema:update / a migration.

  5. First use case — order lifecycle. Create in cart, guard the UI with $order->can('pay'), call $order->apply('pay') (or transition('pay') — check the trait) in a controller/service, then flush(). The Doctrine event subscriber validates the transition on persist/update and throws when it isn't allowed.

Where to look first: vendor/kphoen/doctrine-state-machine/README.md, then the trait in Model/StateMachine/ (that's your real API surface — the README is thin), then the YAML loader.

Implementation Patterns

  • Wrap transitions in a service, not a controller. Keep entity methods for state access only; put $order->apply('pay') inside an OrderWorkflow service that also dispatches domain events and persists. This is what makes the behavior testable and swappable for native Symfony Workflow later.

    class OrderWorkflow
    {
        public function pay(Order $order): void
        {
            if (! $order->can('pay')) {
                throw new \DomainException('Order cannot be paid in its current state.');
            }
            $order->apply('pay');
            $this->dispatcher->dispatch(new OrderPaid($order));
        }
    }
    
  • Use the definition as the single source of truth for UI. Derive available action buttons from the configured transitions rather than hard-coding state checks — can() per transition is far less brittle than if ($order->getState() === 'paid').

  • Lean on symfony/workflow underneath. Because the behavior is built on the Workflow component, you can grab the definition ($workflow->getDefinition()) to power a state-diagram, admin viewer, or documentation page — a nice day-2 win.

  • Guards via events, not config. For role-based or business-rule guards, hook the workflow.guard event listener rather than stuffing conditions into YAML, so the rule lives in testable PHP.

  • Audit transitions with a Doctrine listener capturing getState() before/after on preUpdate/postUpdate. The package doesn't ship an audit trail; a small subscriber fills the gap.

  • Test without a database. Instantiate the entity, set a starting state, assert can() and assert the exception on illegal transitions. This gives you fast state-machine coverage independent of schema and proxies.

  • Migration staging. If you plan to leave the package, keep all apply()/can() calls behind the workflow service so swapping to framework.workflows (Symfony 3.2+) is a one-file change.

Gotchas and Tips

  • Abandoned and unmaintained. Last release 2015, zero dependents, explicitly marked [DEPRECATED]. Treat every line as technical debt and plan the native symfony/workflow migration (or a simple enum + service) from day one.

  • Version conflicts are the default. The dependency graph drags in old doctrine/common annotations and pre-Flex Symfony conventions. PHP 8 typed-property/attribute changes and Symfony 6 will break it; expect to pin versions or fork.

  • can() vs. apply() mismatch. can() returns a boolean while apply() throws — if you only guard with can() in one code path and let another call apply() directly, you get a runtime exception at flush time. Route all transitions through one service method.

  • Stale config cache. Changing machine YAML without clearing the cache (bin/console cache:clear) leaves the old definition loaded — the classic "why can't I make this transition anymore?" bug.

  • Don't mutate state manually on top of the behavior. setState('paid') bypasses the transition validation the subscriber enforces. Grep for direct state setter calls; only the behavior should move state.

  • Trait property mapping. If you redeclare $state with your own @ORM\Column or override the getter, the subscriber may no longer recognise the field. Keep the trait's mapping intact or explicitly configure it.

  • Flush semantics. State is mutated in memory before flush(). If the flush fails (constraint, deadlock), the in-memory object is now inconsistent with the DB — reload or retry, don't reuse the instance.

  • Proxies. can()/getState() read plain scalar fields, so they work on Doctrine proxies; just avoid comparing two detached entities' states across different EntityManager instances.

  • Extension points to prefer: the workflow.guard listener (business rules), a postUpdate subscriber (auditing), and a custom service around the machine (policy). Everything else in the package is closed-off 2015 code you shouldn't patch.

  • Verify names locally. README examples are incomplete — apply/transition, the exact doctrine_state_machine root key, and the trait namespace should be confirmed against vendor/kphoen/doctrine-state-machine/ before writing production code.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo