symfony/workflow
Symfony Workflow component helps model and run workflows or finite state machines. Define places and transitions, guard rules, events, and marking stores to track state changes and integrate processes cleanly into your application.
Install the Package
composer require symfony/workflow
For Laravel integration, use symfony/workflow alongside symfony/dependency-injection and symfony/http-kernel (if needed for DI).
Define a Workflow
Create a YAML/array definition for your workflow (e.g., order_workflow.yaml):
# config/workflows/order_workflow.yaml
app.order_workflow:
supports:
- App\Models\Order
initial_marking: draft
places:
draft:
transitions:
submit: pending
pending:
transitions:
approve: processing
reject: rejected
processing:
transitions:
ship: shipped
shipped:
transitions:
deliver: delivered
rejected:
transitions: ~
Register the Workflow in Laravel Use Laravel’s service provider to load the workflow:
// app/Providers/WorkflowServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\Component\Workflow\Registry;
class WorkflowServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(Registry::class, function ($app) {
$registry = new Registry();
$registry->addWorkflow(
'order_workflow',
$app['config']['workflows.order_workflow']
);
return $registry;
});
}
}
Apply the Workflow to a Model
Use the MarkingStore to track state in your Eloquent model:
// app/Models/Order.php
use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore;
class Order extends Model
{
protected static function booted()
{
static::created(function ($order) {
$order->markAsDraft(); // Initial state
});
}
public function markAsDraft()
{
$this->update(['status' => 'draft']);
}
public function getStatus(): string
{
return $this->status;
}
}
Trigger a Transition Use the workflow registry to apply transitions:
use Symfony\Component\Workflow\WorkflowInterface;
public function submitOrder(Order $order)
{
$workflow = app(WorkflowInterface::class)->get('order_workflow');
$workflow->apply($order, 'submit');
// Emit Laravel event (optional)
event(new OrderSubmitted($order));
}
Check Current State
$currentState = $workflow->getMarking($order);
$isPending = $workflow->can($order, 'approve');
draft → pending → processing → shipped → delivered with conditional transitions (e.g., "approve only if payment is verified").Order model, and trigger transitions via API/controllers.if-else logic with a declarative workflow, reducing bugs and improving maintainability.Model Integration
MethodMarkingStore for Eloquent models (recommended for Laravel):
$store = new MethodMarkingStore(
$order,
'getStatus',
'markAs{status}'
);
$workflow = new Workflow($definition, $store);
DoctrineMarkingStore or PropertyMarkingStore.Event-Driven Transitions
$workflow->on('order.submitted', function (OrderSubmittedEvent $event) {
// Send notification, log audit trail, etc.
});
EventDispatcher or Laravel’s Event system.Dynamic Workflows
$registry->addWorkflow('dynamic_workflow', $dynamicDefinition);
Guard Conditions
pending:
transitions:
approve:
to: processing
guards: [payment_verified]
$workflow->addGuard('payment_verified', function (Order $order) {
return $order->payment->isVerified();
});
Traceable Workflows
TraceableWorkflow:
$traceableWorkflow = new TraceableWorkflow($workflow);
$traceableWorkflow->apply($order, 'submit');
$history = $traceableWorkflow->getHistory();
Visualization
php bin/console workflow:dump --format=mermaid order_workflow
GraphvizDumper in code:
$dumper = new GraphvizDumper();
echo $dumper->dump($workflow);
Service Container Integration
$this->app->bind(WorkflowInterface::class, function ($app) {
return $app['workflow.registry']->get('order_workflow');
});
Artisan Commands
WorkflowDumpCommand for Laravel:
use Symfony\Component\Workflow\Command\WorkflowDumpCommand;
$command = new WorkflowDumpCommand();
$command->run(new Application(), ['command' => 'workflow:dump', 'workflow' => 'order_workflow']);
Testing Workflows
$mockWorkflow = $this->createMock(WorkflowInterface::class);
$mockWorkflow->method('can')->willReturn(true);
$mockWorkflow->method('apply')->willReturnSelf();
API Resource Integration
return new JsonResponse([
'status' => $order->status,
'allowed_transitions' => $workflow->getEnabledTransitions($order),
]);
Queueable Transitions
dispatch(new ApplyWorkflowTransition($order, 'submit'));
public function handle()
{
$workflow = app(WorkflowInterface::class)->get('order_workflow');
$workflow->apply($this->order, $this->transition);
}
State Contamination
MethodMarkingStore with explicit getter/setter methods or upgrade to Symfony 7.4+ (fixed in #62211).Empty String Place Names
"") may fail.null or non-empty names (fixed in #62719).Event Dispatching
Workflow::apply() with true for the second argument to force event dispatching (fixed in #60194).TraceableWorkflow Reset
TraceableWorkflow lacked a reset() method in older versions.HTML Escaping in Graphviz
GraphvizDumper may escape HTML incorrectly.BackedEnum Support
MethodMarkingStore may crash with BackedEnum properties.PropertyMarkingStore for enums.Dump Workflow State
php artisan workflow:dump order_workflow
How can I help you explore Laravel packages today?