elcodi/state-transition-machine-bundle
Symfony bundle from the Elcodi e-commerce suite that provides state transition machine support for managing workflow states. This package is a read-only mirror maintained via the main elcodi/elcodi repository under the MIT license.
Understand what you are installing before you install it. This repository is a read-only subtree split of elcodi/elcodi. It contains only the Symfony bundle wiring — the container extension, service definitions, and kernel registration. The actual state machine domain logic (StateMachine, State, Transition, guards) lives in the sibling elcodi/state-transition-machine component. Issues, PRs, and the real documentation live in the main elcodi/elcodi repository, not here. Anything you learn from this repo alone is incomplete by design.
Install. Pin an explicit version — do not use dev-master in anything you intend to keep.
composer require elcodi/state-transition-machine-bundle:^1.0
composer show elcodi/state-transition-machine-bundle
Immediately inspect composer.json for the PHP and Symfony constraints it drags in. This bundle targets Symfony 2/3-era conventions; if you are on a modern stack, install will either fail or force downgrades elsewhere in your tree. Find that out in a throwaway branch, not in main.
Register the bundle (Symfony kernel — app/AppKernel.php on the era this targets):
new Elcodi\Bundle\StateTransitionMachineBundle\ElcodiStateTransitionMachineBundle(),
Read these in this order — the README is four paragraphs of community links and no usage docs, so treat the code as the spec:
DependencyInjection/ — the extension and configuration tree. This is the authoritative list of YAML keys and service IDs. Read Configuration.php before you write a single line of config.Resources/config/ — service definitions. This shows you how machines, states, and transitions are actually constructed, and which IDs you can inject.elcodi/state-transition-machine component's source and its test suite — that is where the transition semantics (guards, callbacks, exception behavior) are defined and demonstrated.elcodi.io/docs — the only prose documentation that exists.First use case — order lifecycle. The canonical one: cart → pending → paid → shipped → delivered, with → cancelled branches. Configure one machine, one state, and one transition, then exercise it in a test before it touches checkout. Confirm the exact transition API from the component source rather than guessing from bundle config.
If you are on Laravel, stop at step 1. There is no composer require path here that ends well. The realistic "getting started" for a Laravel team is a one-hour spike: composer require in a scratch branch, open vendor/elcodi/, read the component's transition logic, and decide whether to reimplement it. The day-to-day leverage you get from this package in a Laravel codebase is the pattern, not the dependency.
Configuration-first machine definition. States and transitions belong in YAML under the bundle's config key, not in controllers or model methods. One file per workflow (order_states.yml, payment_states.yml, shipment_states.yml), imported from the main config. Keep machine and state identifiers stable — they are frequently interpolated into service IDs, so renaming a state in config can silently change what you can inject.
Inject the machine; never construct it. The bundle registers machines as container services. Reference the specific machine ID in the service that needs it and let the container hand you the configured instance. The payoff: transition rules live in exactly one place, and other bundles (payment, shipping) can contribute to or observe the same machine instead of re-deriving your order lifecycle.
Guard the transition, not the caller. Encode preconditions in the transition definition (guards) or in a listener that vetoes the transition, not in the calling controller. If your code says if ($order->isPaid()) { ship(); }, the state machine has been reduced to decoration. The value of the pattern is that an illegal transition fails loudly regardless of which code path attempts it.
Use listeners for side effects and audit. Attach listeners to transition events to write audit rows, dispatch notifications, or reserve stock. This is the main extension point the bundle gives you. Keep listeners idempotent — a retried transition must not double-charge or double-ship.
Persist state outside the machine. The machine decides legality; your entity owns the current state. Apply the new state and the side effects in one transaction so you cannot end up with a paid order that was never marked paid.
Keep transition rules readable by product. The strongest argument for a config- or table-driven definition is that a non-engineer can review "can a refunded order be re-shipped?" as data rather than as control flow. If the definition lives in a format only one engineer understands, you have lost the main benefit.
Laravel pattern — do this instead of adopting the bundle. Write ~150 lines: a HasStateMachine trait on the model, a transitions() definition (array or config file), a transitionTo(string $state) method that validates legality, wraps in a DB transaction, records an audit row, and fires a domain event. Keep the definition declarative and tested:
class Order extends Model {
use HasStateMachine;
public static function transitions(): array {
return [
'pending' => ['paid', 'cancelled'],
'paid' => ['shipped', 'refunded'],
'shipped' => ['delivered', 'refunded'],
'delivered'=> ['refunded'],
'refunded' => [], 'cancelled' => [],
];
}
}
$order->transitionTo('paid'); // throws IllegalTransition; logs; fires OrderPaid
This gives you the same guarantees the bundle was aiming at, with no Symfony container, no legacy dependency floor, and code your team can debug on day one.
vendor/ and do not fork casually — the split is regenerated from the parent, so local changes are fragile and the canonical issue tracker is elsewhere. A fork is an ownership commitment, and owning a state machine is a bigger promise than it looks.Configuration.php line by line the first time.composer why-not before you spend time on a real integration; the answer is usually "this package predates three major Symfony releases."elcodi/elcodi checkout or in your own adapter layer. Editing the split risks losing the change on the next subtree update, and creates a fork nobody is tracking.bin/console debug:container <machine-id> (or container:debug on older Symfony) to confirm the machine exists and which definition won. Most "transition not allowed" confusion is a config key that never loaded because the file was not imported.How can I help you explore Laravel packages today?