Laravel Workflow

repository·develop·Indexed 42 days ago

https://github.com/zerodahero/laravel-workflow

A wrapper around the Symfony Workflow component that provides seamless integration with Laravel's ecosystem, specifically Eloquent models and configuration patterns. It allows for the definition of state machines and multi-state workflows via config/workflow.php, provides a WorkflowTrait for models, and includes a Workflow facade for managing transitions. Features include GuardEvent for blocking transitions, dot-syntax event listening, and a workflow:dump CLI command for generating visual diagrams via Graphviz.

Tokens
7.8K
Snippets
30
Records
35
Agent score
49%

What's inside Laravel Workflow

  1. Configure Workflow vs State Machine transitions

    develop

    In a multi-state workflow, you can define transitions based on whether a subject must be in exactly multiple places or either of multiple places.

    • Exactly multiple places: To require a subject to be in multiple places simultaneously (e.g., both content_approved AND legal_approved), nest the array of places inside another array in the from key: ['from' => [['place1', 'place2']]].
    • Either of multiple places: To allow a transition from any one of several places (e.g., content_approved OR legal_approved), provide a flat array in the from key: ['from' => ['place1', 'place2']].
    <?php
    
    // Example 1: Exactly two places transition to one (Nested array)
    'transitions' => [
        'publish' => [
            'from' => [ 
                ['content_review', 'legal_review']
            ],
            'to' => 'published'
        ],
    ],
    
    // Example 2: Either of two places transition to one (Flat array)
    'transitions' => [
        'publish' => [
            'from' => [
                'content_review',
                'legal_review'
            ],
            'to' => 'published'
        ],
    ]
  2. Define initial places in a workflow

    develop

    In a workflow definition, you can specify which place(s) the workflow starts in using the initial_places key. This is necessary if the starting place is not the first item in the places list. The value can be a single string or an array of strings for multiple initial places.

    return [
        'type' => 'workflow',
        'places' => [
            'review',
            'rejected',
            'published',
            'draft',
        ],
        'initial_places' => 'draft',
        'transitions' => [
            'to_review' => [
                'from' => 'draft',
                'to' => 'review',
            ],
        ],
    ];
  3. Handle multiple states in 'workflow' type

    develop

    When using a workflow of type workflow (as opposed to state_machine), the model can exist in multiple places simultaneously. To support this, your model must cast the marking property to an array (or json) using Eloquent's casting mechanism.

    class BlogPost extends Model
    {
        use WorkflowTrait;
    
        protected $casts = [
            'marking' => 'array',
        ];
    }
  4. Manual Service Provider and Facade registration

    develop

    If your application does not use Laravel package auto-discovery, you must manually register the service provider and the facade in config/app.php.

    // config/app.php
    
    'providers' => [
        ...
        ZeroDaHero\LaravelWorkflow\WorkflowServiceProvider::class,
    ],
    
    'facades' => [
        ...
        'Workflow' => ZeroDaHero\LaravelWorkflow\Facades\WorkflowFacade::class,
    ],
  5. Configure Laravel workflow via config/workflow.php

    develop

    After installing, publish the configuration file using Artisan:

    php artisan vendor:publish --provider="ZeroDaHero
    LaravelWorkflow//WorkflowServiceProvider"

    Workflows are defined in config/workflow.php. Each top-level key in the array represents a unique workflow name.

    Key Configuration Options:

    • type: Either 'workflow' (multiple states allowed) or 'state_machine' (single state). Defaults to 'workflow'.
    • marking_store: Defines how the state is stored on the model.
      • property: The attribute name on the model (defaults to 'marking').
      • class: The marking store class (defaults to EloquentMethodMarkingStore for Eloquent models).
    • supports: An array of class names (e.g., ['App\BlogPost']) that this workflow applies to.
    • places: An array of available states/places.
    • initial_places: The starting state(s). Defaults to the first element in places if omitted.
    • transitions: An array defining how to move between places. Each transition requires a from and to key.
    • events_to_dispatch: (Only for workflow type) An array of Symfony\Component\Workflow\WorkflowEvents constants to trigger. Setting to null dispatches all; [] dispatches none.
    • metadata: Optional metadata that can be attached at the workflow, place, or transition level.
    return [
        'straight' => [
            'type' => 'workflow',
            'marking_store' => [
                'property' => 'marking',
                'class' => MethodMarkingStore::class,
            ],
            'supports' => ['App\BlogPost'],
            'places' => ['draft', 'review', 'rejected', 'published'],
            'initial_places' => ['draft'],
            'transitions' => [
                'to_review' => [
                    'from' => 'draft',
                    'to' => 'review',
                ],
                'publish' => [
                    'from' => 'review',
                    'to' => 'published'
                ],
            ],
        ]
    ];
  6. Listen to workflow events using dot syntax

    develop

    The package provides several events fired during transitions. It is highly recommended to use Symfony's dot syntax style for event emission. This provides precision and prevents receiving the same event class multiple times for the 'same' event, which can happen if you only listen by class name in Laravel.

    <?php
    
    namespace App\Listeners;
    
    use ZeroDaHero\LaravelWorkflow\Events\GuardEvent;
    
    class BlogPostWorkflowSubscriber
    {
        public function subscribe($events)
        {
            // Format: workflow.[workflow name].[event type].[optional transition/place name]
            
            // workflow.guard
            $events->listen('workflow.straight.guard', 'App\Listeners\BlogPostWorkflowSubscriber@onGuard');
    
            // workflow.leave
            $events->listen('workflow.straight.leave', 'App\Listeners\BlogPostWorkflowSubscriber@onLeave');
    
            // workflow.transition
            $events->listen('workflow.straight.transition', 'App\Listeners\BlogPostWorkflowSubscriber@onTransition');
    
            // workflow.enter
            $events->listen('workflow.straight.enter', 'App\Listeners\BlogPostWorkflowSubscriber@onEnter');
    
            // workflow.entered
            $events->listen('workflow.straight.entered', 'App\Listeners\BlogPostWorkflowSubscriber@onEntered');
    
            // workflow.completed
            $events->listen('workflow.straight.completed', 'App\Listeners\BlogPostWorkflowSubscriber@onCompleted');
    
            // workflow.announce
            $events->listen('workflow.straight.announce', 'App\Listeners\BlogPostWorkflowSubscriber@onAnnounce');
        }
    }
  7. Integrate WorkflowTrait into models

    develop

    To enable workflow capabilities directly on your Eloquent models, use the WorkflowTrait within your class definition.

    namespace App;
    
    use Illuminate\Database\Eloquent\Model;
    use ZeroDaHero\LaravelWorkflow\Traits\WorkflowTrait;
    
    class BlogPost extends Model
    {
      use WorkflowTrait;
    }
  8. Implement a custom mutator for EloquentMarkingStore

    develop

    If you want to perform custom logic (like logging, triggering side effects, or complex casting) whenever a workflow marking is updated, you can define a mutator on your Eloquent model.

    The EloquentMarkingStore will automatically detect and use a method named set{PropertyName}Attribute.

    For a property named marking, the method signature should be: public function setMarkingAttribute($marking, array $context = [])

    class Order extends //Illuminate\Database\Eloquent\Model
    {
        // The property where marking is stored
        protected $marking = [];
    
        /**
         * Custom mutator called by EloquentMarkingStore
         */
        public function setMarkingAttribute($marking, array $context = [])
        {
            // $marking is the array of places
            // $context contains additional workflow context
            $this->attributes['marking'] = json_encode($marking);
            
            // Perform custom logic here
            logger("Workflow moved to: " . implode(', ', $marking));
        }
    }
  9. Enable registry tracking for dynamic workflows

    develop

    If you are loading workflow definitions dynamically (e.g., from a database), you should enable registry tracking. This allows the registry to monitor loaded workflows and handle duplicates.

    Configure this in config/workflow_registry.php using the following keys:

    • track_loaded: Set to true to enable tracking.
    • ignore_duplicates: When track_loaded is true, set this to true to silently ignore duplicate registrations, or false to throw a DuplicateWorkflowException when a duplicate is encountered.
    return [
        'track_loaded' => true,
        'ignore_duplicates' => false,
    ];
  10. Handle duplicate workflow registrations

    develop

    When loading workflows dynamically, you may encounter duplicates. You can handle this in two ways:

    1. Configuration: Set 'ignore_duplicates' => true in config/workflow_registry.php to have the registry ignore the new definition.
    2. Try/Catch: Wrap the addFromArray call in a try-catch block to catch DuplicateWorkflowException.
    try {
        $registry->addFromArray($workflowName, $workflowDefinition);
    } catch (DuplicateWorkflowException $e) {
        // Workflow is already loaded
    }