Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Workflow Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the package via Composer:
    composer require symfony/workflow
    
  2. Define a workflow in YAML (e.g., 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 }
    
  3. Load the workflow in a service provider (e.g., 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']);
        });
    }
    
  4. Apply to an Eloquent model (e.g., 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'
    

First Use Case: Order Status Workflow

  • Problem: Managing order states (draft, submitted, approved, etc.) with scattered if-else logic.
  • Solution: Define transitions in YAML and apply them to orders:
    // Transition an order to 'approved'
    $workflow->apply($store, 'approve');
    
    // Check current state
    $currentState = $workflow->getEnabledTransitions($store)->getName();
    

Implementation Patterns

1. Workflow Definition

  • YAML Configuration (Recommended for readability):
    # 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 }
    
  • PHP Configuration (For dynamic workflows):
    $definition = new Definition([
        'places' => ['pending', 'processing', 'completed', 'failed'],
        'transitions' => [
            'start' => ['from' => 'pending', 'to' => 'processing'],
            'complete' => ['from' => 'processing', 'to' => 'completed'],
            'fail' => ['from' => 'processing', 'to' => 'failed'],
        ],
    ]);
    

2. Integration with Eloquent Models

  • Store Workflow State in Database: Use MethodMarkingStore to sync workflow states with a model attribute (e.g., status):
    $store = new MethodMarkingStore($content, 'status');
    $workflow->apply($store, 'publish');
    
  • Custom Stores (For complex scenarios): Extend 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();
        }
    }
    

3. Guards and Conditions

  • Inline Guards (Simple conditions):
    transitions:
      approve: { from: submitted, to: approved, guard: 'order.is_approved_by_manager()' }
    
  • Custom Guard Classes: Implement 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' }
    

4. Events and Listeners

  • Listen to Workflow Events: Use Symfony’s event system to trigger side effects (e.g., notifications):
    use Symfony\Component\Workflow\WorkflowEvent;
    
    $workflow->on(
        WorkflowEvent::TRANSITIONED,
        fn(WorkflowEvent $event) => Log::info("Transitioned to {$event->getTransition()->getTo()->getName()}")
    );
    
  • Laravel Events Integration: Dispatch custom Laravel events:
    $workflow->on(
        WorkflowEvent::TRANSITIONED,
        fn(WorkflowEvent $event) => event(new OrderStatusChanged(
            $event->getSubject(),
            $event->getTransition()->getTo()->getName()
        ))
    );
    

5. Dynamic Workflows

  • Runtime Workflow Loading: Load workflows dynamically based on context (e.g., tenant ID):
    $loader = new YamlFileLoader();
    $workflow = new Workflow($loader->load("config/workflows/{$tenantId}.yaml")['workflow_name']);
    
  • Composite Workflows: Combine multiple workflows (e.g., order + payment workflows):
    order_workflow:
      support:
        places: [pending, processing, completed]
        transitions: [...]
    payment_workflow:
      support:
        places: [unpaid, paid, refunded]
        transitions: [...]
    

6. Validation and Testing

  • Validate Workflow Definitions: Use WorkflowValidator to catch misconfigurations early:
    $validator = new WorkflowValidator();
    $errors = $validator->validate($definition);
    
  • Test Workflows: Mock workflows in tests:
    $workflow = $this->createMock(Workflow::class);
    $workflow->method('can')->willReturn(true);
    $workflow->method('apply')->willReturn(true);
    

Gotchas and Tips

Pitfalls

  1. State Contamination:

    • Issue: Reusing the same MarkingStore instance across workflows can cause state contamination.
    • Fix: Use dedicated store instances per workflow or reset stores:
      $store->setMarking([]); // Reset
      
  2. Guard Caching:

    • Issue: Guards are cached by default, which can lead to stale evaluations.
    • Fix: Disable caching or implement GuardInterface with fresh logic:
      $definition->addGuardFactory('custom_guard', fn() => new FreshGuard());
      
  3. Transition Names vs. Method Names:

    • Issue: Confusing transition names (YAML) with method names (guards).
    • Fix: Use consistent naming conventions (e.g., transition_name in YAML → guardTransitionName() in PHP).
  4. Event Dispatching:

    • Issue: Events may not fire if the subject is already in the target state.
    • Fix: Use WorkflowEvent::ENTERED and WorkflowEvent::EXITED for state changes:
      $workflow->on(WorkflowEvent::ENTERED, fn($event) => Log::info("Entered {$event->getMarking()}"));
      
  5. Performance Overhead:

    • Issue: Workflow initialization adds ~5–10ms overhead.
    • Fix: Cache workflow instances:
      $this->app->singleton('workflow', fn() => new Workflow($definition));
      

Debugging Tips

  1. 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.

  2. Check Enabled Transitions: Inspect available transitions at runtime:

    $enabledTransitions = $workflow->getEnabledTransitions($store);
    dd($enabledTransitions->getName());
    
  3. 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
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport
twbs/bootstrap4