draw/workflow
draw/workflow is a Laravel/PHP package for modeling and running workflows. Define steps and transitions, track state changes, and execute processes consistently across your application. Useful for approvals, onboarding flows, and other multi-step business processes.
Install the Package
composer require draw/workflow
Requires Symfony Workflow ^6.4.0 and PHP 8.1+.
Define a Workflow Extension
Create a class implementing ExtensionInterface:
use Draw\Workflow\Extension\ExtensionInterface;
use Symfony\Component\Workflow\WorkflowInterface;
class CustomGuardExtension implements ExtensionInterface
{
public function getName(): string
{
return 'custom_guard';
}
public function canApply(WorkflowInterface $workflow, $entity, string $transition, string $from, string $to): bool
{
// Custom guard logic (e.g., role-based, attribute checks)
return true;
}
}
Register the Extension
Extend your Symfony Workflow configuration (e.g., config/packages/workflow.yaml):
workflows:
my_workflow:
type: 'state_machine'
supports: [App\Entity\MyEntity]
initial_marking: 'pending'
places: ['pending', 'approved', 'rejected']
transitions:
- from: pending
to: approved
guard: 'custom_guard' # Reference your extension
Apply the Workflow Use the extended workflow in a controller or service:
use Symfony\Component\Workflow\WorkflowInterface;
class OrderController
{
public function __construct(private WorkflowInterface $workflow) {}
public function approveOrder(MyEntity $order)
{
$this->workflow->apply($order, 'approve');
// Custom guard logic is now enforced
}
}
First Use Case: Dynamic Guards Implement a guard that checks user permissions:
class RoleGuardExtension implements ExtensionInterface
{
public function canApply(WorkflowInterface $workflow, $entity, string $transition, string $from, string $to): bool
{
$user = auth()->user();
return $user->hasRole('admin');
}
}
Extension-Based Workflows
ExtensionInterface to add reusable logic (guards, transitions, events).class EmailNotificationExtension implements ExtensionInterface
{
public function onTransition(WorkflowInterface $workflow, $entity, string $transition, string $from, string $to)
{
Mail::to($entity->user)->send(new WorkflowNotification($transition));
}
}
Event-Driven Extensions
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class DynamicTransitionExtension implements ExtensionInterface
{
public function __construct(private EventDispatcherInterface $dispatcher) {}
public function getTransitions(WorkflowInterface $workflow, $entity): array
{
$this->dispatcher->dispatch(new WorkflowTransitionsEvent($entity));
return [...]; // Modified transitions
}
}
Conditional Transitions
transitions:
- from: pending
to: approved
guard: 'custom_guard'
# Only enabled if $entity->isValid()
Integration with Laravel
EventDispatcher to Laravel’s Events system.use Symfony\Component\EventDispatcher\EventDispatcher;
use Illuminate\Support\Facades\Event;
$dispatcher = new EventDispatcher();
$dispatcher->addListener('workflow.transition', function ($event) {
Event::dispatch(new LaravelWorkflowEvent($event));
});
Leverage Symfony’s DI
services.yaml:
services:
App\Workflow\CustomExtension:
tags: ['draw.workflow.extension']
Compose Complex Workflows
extensions:
- 'guard_extension'
- 'notification_extension'
- 'audit_extension'
Test Workflows
WorkflowTestCase or Laravel’s WorkflowTestTrait:
use Symfony\Component\Workflow\Tests\WorkflowTestCase;
class MyWorkflowTest extends WorkflowTestCase
{
public function testCustomGuard()
{
$this->assertTrue($this->workflow->can($entity, 'approve'));
}
}
Debugging Workflows
$workflow->registerTransition('debug', function ($transition) {
logger()->debug('Transition', ['transition' => $transition->getName()]);
});
Extension Registration Order
ExtensionInterface::getPriority() to control execution order.Circular Dependencies
Symfony Version Mismatches
symfony/workflow or symfony/event-dispatcher.composer.json:
"require": {
"symfony/workflow": "^6.4.0",
"symfony/event-dispatcher": "^6.4.0"
}
Laravel-Specific Quirks
EventDispatcher may not play well with Laravel’s service container.symfony/event-dispatcher-bundle or manually bind services.State Persistence
Log Workflow Events
$workflow->registerTransition('log', function ($transition) {
\Log::debug('Workflow transition', [
'transition' => $transition->getName(),
'from' => $transition->getFrom(),
'to' => $transition->getTo(),
]);
});
Inspect Workflow State
$state = $workflow->getEnabledTransitions($entity);
\Log::info('Enabled transitions', $state);
Use Symfony’s Debug Toolbar
symfony/web-profiler-bundle to visualize workflow states.Custom Guards
canApply() to add logic before transitions:
public function canApply(WorkflowInterface $workflow, $entity, string $transition, string $from, string $to): bool
{
return $entity->isValid() && auth()->check();
}
Transition Handlers
onTransition() for post-transition logic:
public function onTransition(WorkflowInterface $workflow, $entity, string $transition, string $from, string $to)
{
$entity->markAsProcessed();
$entity->save();
}
Event Listeners
use Symfony\Component\Workflow\Event\TransitionEvent;
$dispatcher->addListener(TransitionEvent::NAME, function (TransitionEvent $event) {
// Custom logic
});
Avoid Heavy Logic in Guards
Cache Workflow Definitions
WorkflowInterface instance:
$workflow = $container->get('workflow.my_workflow');
Batch Processing
applyAll() or queue transitions:
$workflow->applyAll($entities, 'transition_name');
Service Provider Integration
use Draw\Workflow\Extension\ExtensionInterface;
class WorkflowServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->tag(
[CustomExtension::class],
['draw.workflow.extension']
);
}
}
Artisan Commands
use Symfony\Component\Workflow\WorkflowInterface;
class WorkflowCommand extends Command
{
protected $workflow;
public function __construct(WorkflowInterface $workflow)
{
$this
How can I help you explore Laravel packages today?