symfony/workflow
Symfony Workflow Component helps you model and run workflows or finite state machines in PHP. Define places, transitions, and guards to control state changes, track progress, and integrate with events for process automation.
composer require symfony/workflow
config/workflows/order.yaml):
order_workflow:
support:
places: [draft, submitted, approved, rejected, published]
transitions:
submit: { from: draft, to: submitted }
approve: { from: submitted, to: approved, guard: 'order.is_ready_for_review()' }
reject: { from: submitted, to: rejected }
publish: { from: approved, to: published }
AppServiceProvider):
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\YamlFileLoader;
public function register()
{
$this->app->singleton('order.workflow', function () {
$loader = new YamlFileLoader();
return new Workflow($loader->load(__DIR__.'/../config/workflows/order.yaml')['order_workflow']);
});
}
Order):
use Symfony\Component\Workflow\MethodMarkingStore;
$workflow = app('order.workflow');
$store = new MethodMarkingStore($order, 'status'); // 'status' is the model attribute
$workflow->apply($store, 'submit'); // Transition from 'draft' to 'submitted'
draft, submitted, approved, etc.) with scattered if-else logic.// Transition an order to 'approved'
$workflow->apply($store, 'approve');
// Check current state
$currentState = $workflow->getEnabledTransitions($store)->getName();
# config/workflows/content.yaml
content_workflow:
support:
places: [draft, under_review, published, archived]
transitions:
submit_for_review: { from: draft, to: under_review }
publish: { from: under_review, to: published, guard: 'content.is_ready()' }
archive: { from: published, to: archived }
$definition = new Definition([
'places' => ['pending', 'processing', 'completed', 'failed'],
'transitions' => [
'start' => ['from' => 'pending', 'to' => 'processing'],
'complete' => ['from' => 'processing', 'to' => 'completed'],
'fail' => ['from' => 'processing', 'to' => 'failed'],
],
]);
MethodMarkingStore to sync workflow states with a model attribute (e.g., status):
$store = new MethodMarkingStore($content, 'status');
$workflow->apply($store, 'publish');
MarkingStoreInterface to store states in a custom location (e.g., JSON column):
class JsonMarkingStore implements MarkingStoreInterface
{
public function __construct(private Model $model, private string $attribute)
{}
public function getMarking(): array
{
return json_decode($this->model->{$this->attribute}, true) ?: [];
}
public function setMarking(array $marking): void
{
$this->model->{$this->attribute} = json_encode($marking);
$this->model->save();
}
}
transitions:
approve: { from: submitted, to: approved, guard: 'order.is_approved_by_manager()' }
GuardInterface for complex logic:
class BudgetGuard implements GuardInterface
{
public function __construct(private Order $order) {}
public function guard(Transition $transition): bool
{
return $this->order->budget >= 1000;
}
}
Register in YAML:
transitions:
approve: { from: submitted, to: approved, guard: '@budget_guard' }
use Symfony\Component\Workflow\WorkflowEvent;
$workflow->on(
WorkflowEvent::TRANSITIONED,
fn(WorkflowEvent $event) => Log::info("Transitioned to {$event->getTransition()->getTo()->getName()}")
);
$workflow->on(
WorkflowEvent::TRANSITIONED,
fn(WorkflowEvent $event) => event(new OrderStatusChanged(
$event->getSubject(),
$event->getTransition()->getTo()->getName()
))
);
$loader = new YamlFileLoader();
$workflow = new Workflow($loader->load("config/workflows/{$tenantId}.yaml")['workflow_name']);
order_workflow:
support:
places: [pending, processing, completed]
transitions: [...]
payment_workflow:
support:
places: [unpaid, paid, refunded]
transitions: [...]
WorkflowValidator to catch misconfigurations early:
$validator = new WorkflowValidator();
$errors = $validator->validate($definition);
$workflow = $this->createMock(Workflow::class);
$workflow->method('can')->willReturn(true);
$workflow->method('apply')->willReturn(true);
State Contamination:
MarkingStore instance across workflows can cause state contamination.$store->setMarking([]); // Reset
Guard Caching:
GuardInterface with fresh logic:
$definition->addGuardFactory('custom_guard', fn() => new FreshGuard());
Transition Names vs. Method Names:
transition_name in YAML → guardTransitionName() in PHP).Event Dispatching:
WorkflowEvent::ENTERED and WorkflowEvent::EXITED for state changes:
$workflow->on(WorkflowEvent::ENTERED, fn($event) => Log::info("Entered {$event->getMarking()}"));
Performance Overhead:
$this->app->singleton('workflow', fn() => new Workflow($definition));
Dump Workflow: Use the CLI tool to visualize workflows:
php artisan workflow:dump --format=mermaid config/workflows/order.yaml
Outputs a Mermaid diagram for easy debugging.
Check Enabled Transitions: Inspect available transitions at runtime:
$enabledTransitions = $workflow->getEnabledTransitions($store);
dd($enabledTransitions->getName());
Log Workflow Events: Enable Symfony’s profiler or log events:
$workflow->on(WorkflowEvent::TRANSITIONED, fn($event) => Log::debug('Transitioned', [
'from' => $event->getTransition()->getFrom()->getName(),
'to' => $event->getTransition
How can I help you explore Laravel packages today?