Weave 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

The Symfony Workflow component provides tools for managing workflows and finite state machines in PHP applications, letting you define states, transitions, and guards to model business processes cleanly and reliably.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps to First Use Case

  1. 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). Current stable line is 8.1.x (latest patch v8.1.8).

  2. 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: ~
    
  3. 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\Definition;
    use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore;
    use Symfony\Component\Workflow\Registry;
    use Symfony\Component\Workflow\Workflow;
    
    class WorkflowServiceProvider extends ServiceProvider
    {
        public function register()
        {
            $this->app->singleton(Registry::class, function ($app) {
                $definition = $app['config']['workflows.order_workflow'];
                $store = new MethodMarkingStore(true, 'status'); // single state field
    
                $registry = new Registry();
                $registry->addWorkflow(
                    'order_workflow',
                    new Workflow($definition, $store)
                );
    
                return $registry;
            });
        }
    }
    
  4. Apply the Workflow to a Model Use the MarkingStore to track state in your Eloquent model:

    // app/Models/Order.php
    class Order extends Model
    {
        public function getStatus(): string
        {
            return $this->status;
        }
    
        public function setStatus(string $status): void
        {
            $this->status = $status;
        }
    }
    
  5. Trigger a Transition Use the registry to fetch the workflow for the subject:

    use Symfony\Component\Workflow\Registry;
    
    public function submitOrder(Order $order)
    {
        $workflow = app(Registry::class)->get($order, 'order_workflow');
        $workflow->apply($order, 'submit');
        $order->save();
    
        event(new OrderSubmitted($order));
    }
    
  6. Check Current State

    $marking = $workflow->getMarking($order);
    $isPending = $workflow->can($order, 'approve');
    $nextSteps = $workflow->getEnabledTransitions($order);
    

First Use Case: Order Processing

  • Problem: Orders move through draft → pending → processing → shipped → delivered with conditional transitions (e.g., "approve only if payment is verified").
  • Solution: Define the workflow in YAML, apply it to the Order model, and trigger transitions via API/controllers.
  • Outcome: Replace 50+ lines of if-else logic with a declarative workflow, reducing bugs and improving maintainability.

Implementation Patterns

Workflow Integration Workflows

  1. Model Integration

    • Use MethodMarkingStore for Eloquent models (recommended for Laravel):
      $store = new MethodMarkingStore(true, 'status'); // uses getStatus()/setStatus()
      $workflow = new Workflow($definition, $store);
      
    • For non-Eloquent entities, use DoctrineMarkingStore or PropertyMarkingStore.
  2. Event-Driven Transitions

    • Bridge Symfony workflow events into Laravel events rather than inventing a custom listener API:
      use Symfony\Component\EventDispatcher\EventDispatcher;
      use Symfony\Component\Workflow\Event\TransitionEvent;
      use Symfony\Component\Workflow\Event\GuardEvent;
      
      $dispatcher = new EventDispatcher();
      $dispatcher->addListener('workflow.order_workflow.transition', function (TransitionEvent $event) {
          $order = $event->getSubject();
          $transition = $event->getTransition()->getName();
          event(new WorkflowTransitioned($order, $transition));
      });
      
      $workflow = new Workflow($definition, $store, $dispatcher, 'order_workflow');
      
    • Guard events (workflow.order_workflow.guard.<transition> / workflow.<name>.guard) are your primary extension point for blocking transitions.
  3. Dynamic Workflows

    • Load workflows from the database or config:
      $registry->addWorkflow('dynamic_workflow', new Workflow($dynamicDefinition, $store));
      
    • Useful for multi-tenancy or tenant-specific workflows.
  4. Guard Conditions

    • Add transition guards (e.g., "approve only if payment is verified"):
      pending:
          transitions:
              approve:
                  to: processing
                  guard: [is_granted, payment_verified]  # or a Closure/service listener
      
    • Implement guards as listeners on the guard event:
      $dispatcher->addListener('workflow.order_workflow.guard.approve', function (GuardEvent $event) {
          if (! $event->getSubject()->payment->isVerified()) {
              $event->setBlocked(true, 'Payment not verified');
          }
      });
      
  5. Traceable Workflows

    • Enable audit trails with TraceableWorkflow:
      $traceableWorkflow = new TraceableWorkflow($workflow);
      $traceableWorkflow->apply($order, 'submit');
      $trace = $traceableWorkflow->getTrace();
      
    • Persist the trace to a table for compliance/auditing; call reset() between long-running iterations (available in current 8.x, including 8.1.x).
  6. Visualization

    • Generate Mermaid diagrams for documentation:
      php bin/console workflow:dump --format=mermaid order_workflow
      
    • Or use the dumpers in code:
      use Symfony\Component\Workflow\Dumper\GraphvizDumper;
      use Symfony\Component\Workflow\Dumper\MermaidDumper;
      
      echo (new GraphvizDumper())->dump($workflow->getDefinition());
      echo (new MermaidDumper(MermaidDumper::TRANSITION_TYPE, 'state'))->dump($workflow->getDefinition());
      

Laravel-Specific Patterns

  1. Service Container Integration

    • Bind a named workflow instance via a resolver rather than binding WorkflowInterface (the interface has no get() method):
      $this->app->bind('workflow.order', function ($app) {
          return $app[Registry::class]->get(new \App\Models\Order(), 'order_workflow');
      });
      
    • Or inject Registry and resolve per-subject, which is the safe route for anything tenant/config-dependent.
  2. Artisan Commands

    • Wrap Symfony’s WorkflowDumpCommand in a Laravel command, or call the dumper directly in a custom php artisan workflow:dump command. Avoid instantiating Symfony’s Console Application inside Laravel; it fights the Laravel kernel.
  3. Testing Workflows

    • Prefer testing against a real Workflow instance built from the actual definition (fast, no DB), and only mock when the workflow is injected:
      $workflow = new Workflow($definition, new MethodMarkingStore(true, 'status'));
      $this->assertTrue($workflow->can($order, 'submit'));
      
  4. API Resource Integration

    • Expose workflow transitions in API responses:
      return new JsonResponse([
          'status' => $order->status,
          'allowed_transitions' => array_map(
              fn ($t) => $t->getName(),
              $workflow->getEnabledTransitions($order)
          ),
      ]);
      
  5. Queueable Transitions

    • Dispatch workflow transitions to queues for async processing:
      dispatch(new ApplyWorkflowTransition($order, 'submit'));
      
    • Handle in a job:
      public function handle(Registry $registry)
      {
          $workflow = $registry->get($this->order, 'order_workflow');
          $workflow->apply($this->order, $this->transition);
          $this->order->save();
      }
      
    • Guard against double-apply: job retries can leave the subject already in the target place, so check can() first or add ShouldBeUnique.

Gotchas and Tips

Pitfalls

  1. State Contamination

    • Issue: Class-based getter/setter caches can cause incorrect state reads/writes.
    • Fix: Use MethodMarkingStore with explicit getter/setter methods, or upgrade to Symfony 7.4+/8.x (fixed in #62211).
  2. Empty String Place Names

    • Issue: Workflows with empty string place names (e.g., "") may fail.
    • Fix: Avoid empty strings; use non-empty names (fixed in #62719).
  3. Event Dispatching

    • Issue: Events may not fire if the subject is already in the target marking.
    • Fix: Pass true as the second argument to Workflow::apply($subject, $transition, true) to force context (fixed in #60194).
  4. Mermaid Label Escaping (#) — new in v8.1.8

    • Issue: Mermaid labels containing a # (e.g. places/transitions named order#42, step#2) produced invalid Mermaid syntax and broke diagram rendering. Fixed in #66202 ("Escape the hash in Mermaid labels").
    • Fix: Upgrade to v8.1.8+. If you’re pinned to ≤ 8.1.7, avoid # in place/transition names, or post-process the dumper output and replace # with #35; before feeding it to Mermaid.
    • Why it matters for Laravel: Mermaid output is the usual way teams embed workflow docs in READMEs/CI artefacts. A broken diagram is silent — the dump command succeeds, the rendering fails later.
  5. TraceableWorkflow Reset

    • Issue: TraceableWorkflow lacked a reset() method in older versions, so traces accumulated across loops/requests.
    • Fix: Upgrade to Symfony 7.4.6+/8.x, or manually clear the trace array.
  6. HTML Escaping in Graphviz

    • Issue: GraphvizDumper may escape HTML incorrectly.
    • Fix: Upgrade to Symfony 7.4.9+ (carried forward into 8.x); otherwise sanitize labels before rendering.
  7. BackedEnum Support

    • Issue: MethodMarkingStore may crash with BackedEnum properties.
    • Fix: Upgrade to Symfony 8.0+ (you're safely on 8.1.x at v8.1.8), or use PropertyMarkingStore for enum-backed state.
  8. Version Drift in a Framework-Agnostic Package

    • Issue: The workflow component ships independently of Laravel; composer require symfony/workflow can pull a different minor than symfony/console/http-kernel already in your lockfile, and the Mermaid/Graphviz dumpers have had several patch-level fixes.
    • Fix: Pin the whole Symfony set to the same minor (^8.1) and bump together. Check composer show symfony/workflow before filing a "diagram is broken" bug — v8.1.8 vs. v8.1.0 matters here.

Debugging Tips

  1. Dump Workflow State

    php artisan workflow:dump order_workflow
    php artisan workflow:dump order_workflow --format=mermaid
    
    • Outputs a visual representation (DOT or Mermaid). If a Mermaid diagram fails to render on v8.1.8+, suspect label content (#, quotes, brackets) rather than the workflow definition.
  2. Verify the Registry, Not the Interface

    $workflow = app(\Symfony\Component\Workflow\Registry::class)->get($order, 'order_workflow');
    dump($workflow->getMarking($order)->getPlaces());
    dump($workflow->getEnabledTransitions($order));
    

    WorkflowInterface has no get() method — the Registry does. A BadMethodCallException here almost always means a stale copy-paste binding.

  3. Log Every Transition via One Listener

    $dispatcher->addListener('workflow.order_workflow.transition', fn ($e) =>
        logger()->info('workflow', ['transition' => $e->getTransition()->getName(), 'id' => $e->getSubject()->id])
    );
    

    This single hook surfaces "why didn't my guard fire" faster than instrumenting each controller.

  4. Assert Blocked Guards Explicitly

    $event = new GuardEvent($order, $marking, $transition);
    // assert $event->isBlocked() and inspect getBlockedReasons()
    

    Silent guard failures (a closure returning null instead of false) are the most common "transition does nothing" cause.

  5. Check composer show symfony/workflow First Several of the pitfalls above are patch-level fixes. Before debugging dumper output or marking-store behaviour, confirm you're on the latest 8.1.x — v8.1.8 currently.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator