winzou/state-machine-bundle
Lightweight PHP/Symfony bundle for defining state machines on your domain objects. Configure graphs with states, transitions, and optional guard/before/after callbacks via YAML/XML/PHP, then apply and test transitions without hard-coded state logic.
Installation:
composer require winzou/state-machine-bundle
Register the bundle in config/bundles.php:
return [
// ...
winzou\Bundle\StateMachineBundle\winzouStateMachineBundle::class => ['all' => true],
];
Define a Graph:
Configure a state machine graph in config/packages/winzou_state_machine.yaml:
winzou_state_machine:
my_entity:
class: App\Entity\MyEntity
property_path: state
graph: default
states: [draft, published, archived]
transitions:
publish:
from: [draft]
to: published
archive:
from: [published]
to: archived
First Use Case:
Inject the Factory service and apply a transition:
use SM\Factory\Factory;
public function publishAction(MyEntity $entity, Factory $factory)
{
$stateMachine = $factory->get($entity, 'default');
$stateMachine->apply('publish');
// $entity->state is now 'published'
}
State Validation: Check if a transition is allowed before applying it:
if ($stateMachine->can('publish')) {
$stateMachine->apply('publish');
}
Dynamic State Transitions: Use callbacks to enforce business logic:
callbacks:
guard:
validate_publish:
on: publish
do: ['@my_service', 'validate']
args: ['object']
Multi-Graph Entities:
Define multiple graphs for a single entity (e.g., workflow and audit):
winzou_state_machine:
my_entity:
# ... default graph
my_entity_audit:
class: App\Entity\MyEntity
graph: audit
states: [active, suspended]
transitions:
suspend:
from: [active]
to: suspended
Event-Driven Transitions:
Trigger transitions from events (e.g., onPostPersist):
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getObject();
$stateMachine = $factory->get($entity, 'default');
$stateMachine->apply('create');
}
$builder->add('title', TextType::class, [
'disabled' => !$stateMachine->can('publish'),
]);
403 Forbidden if a transition is invalid:
if (!$stateMachine->can('transition')) {
throw new \Symfony\Component\HttpKernel\Exception\HttpException(403);
}
Callback Argument Overrides:
Avoid splitting args across multiple config files—only the last definition is used (fixed in v0.2.1).
State Property Path:
Ensure property_path in config matches the entity property (default: state). Use getState() to debug:
$stateMachine->getState(); // Verify current state
Circular Dependencies:
Avoid callbacks that create infinite loops (e.g., a before callback applying another transition).
Symfony Version Quirks:
priority in callbacks for ordering:
callbacks:
before:
log_transition:
on: publish
do: ['@logger', 'info']
args: ['object', 'Transitioning to published']
priority: 100
php bin/console debug:state-machine
# config/services.yaml
SM\Factory\Factory:
calls:
- [setDebug, [true]]
Custom Guards:
Extend the Guard class to add logic:
use SM\Guard\GuardInterface;
class MyGuard implements GuardInterface {
public function check($object, $transition, $stateMachine) {
// Custom validation
}
}
Register in config:
callbacks:
guard:
my_guard:
on: '*'
do: ['@my_guard_service', 'check']
Dynamic Graphs: Load graphs dynamically via services:
services:
my_dynamic_graph:
class: App\StateMachine\DynamicGraphBuilder
tags: [winzou_state_machine.graph_builder]
Event Listeners: Listen to state changes:
use SM\Event\TransitionEvent;
public function onTransition(TransitionEvent $event) {
if ($event->getTransition() === 'publish') {
// Handle publish event
}
}
Register as a service with the kernel.event_listener tag.
How can I help you explore Laravel packages today?