Installation
composer require zerodahero/laravel-workflow
For Laravel 10/11/12/13, use ^6.x (PHP 8.1+). For older versions, check the version matrix.
Publish Config
php artisan vendor:publish --provider="ZeroDaHero\LaravelWorkflow\WorkflowServiceProvider"
This generates config/workflow.php.
Define a Workflow
Add a workflow to config/workflow.php:
'blog_post' => [
'supports' => [App\Models\BlogPost::class],
'places' => ['draft', 'review', 'published'],
'transitions' => [
'to_review' => ['from' => 'draft', 'to' => 'review'],
'publish' => ['from' => 'review', 'to' => 'published'],
],
]
Attach to Model
Use the WorkflowTrait in your model:
use ZeroDaHero\LaravelWorkflow\Traits\WorkflowTrait;
class BlogPost extends Model
{
use WorkflowTrait;
}
First Transition
$post = BlogPost::find(1);
$post->workflow_apply('to_review'); // Apply transition
$post->save(); // Persist state
State Machine (Single State)
Use type: 'state_machine' for models with one active state (e.g., order status).
'order_status' => [
'type' => 'state_machine',
'supports' => [App\Models\Order::class],
'places' => ['pending', 'shipped', 'delivered'],
'transitions' => [...],
]
Workflow (Multiple States) Default type. Use for complex workflows (e.g., approval chains).
'approval_workflow' => [
'type' => 'workflow',
'supports' => [App\Models\Document::class],
'places' => ['submitted', 'reviewed', 'approved', 'rejected'],
'transitions' => [...],
]
Dynamic Workflows Load workflows dynamically via a service or API:
$workflowConfig = $this->fetchWorkflowConfigFromApi();
Workflow::register($workflowConfig);
Validation Check transitions before applying:
if ($post->workflow_can('publish')) {
$post->workflow_apply('publish');
}
Events Listen to transitions for side effects (e.g., notifications):
// In EventServiceProvider
$this->listen(
'workflow.blog_post.transition.publish',
\App\Listeners\SendPublishNotification::class
);
Metadata Attach metadata to places/transitions for business logic:
'places' => [
'draft' => ['metadata' => ['max_words' => 1000]],
],
Custom Guards Block transitions via events:
public function onGuard(GuardEvent $event) {
if ($event->getSubject()->is_confidential) {
$event->getOriginalEvent()->setBlocked(true);
}
}
Testing Mock workflows in tests:
$workflow = Mockery::mock(WorkflowInterface::class);
$workflow->shouldReceive('can')->andReturn(true);
$this->app->instance(WorkflowInterface::class, $workflow);
Forgetting to Save
Transitions update the model’s marking property but won’t auto-save. Always call $model->save() after applying transitions.
Multiple Workflows If a model supports multiple workflows, specify the name:
$workflow = Workflow::get($post, 'blog_post'); // Explicit workflow
State Machine vs. Workflow
Event Listener Duplication
Avoid listening to raw event classes (e.g., GuardEvent). Use Symfony’s dot syntax (e.g., workflow.blog_post.guard.publish) to prevent duplicates.
Metadata Access Metadata is stored but not automatically cast. Access it via:
$metadata = $workflow->getMetadataStore()->getPlaceMetadata('draft');
Check Current State
$places = $workflow->getMarking($post)->getPlaces();
dd($places); // ['draft', 'review']
Enabled Transitions
$enabled = $workflow->getEnabledTransitions($post);
dd($enabled->getNames()); // ['publish']
Event Debugging Use Tinker to test events:
php artisan tinker
>>> event(new \ZeroDaHero\LaravelWorkflow\Events\GuardEvent(...));
Custom Marking Stores
Override the default EloquentMethodMarkingStore for non-Eloquent models:
'marking_store' => [
'property' => 'status',
'class' => \App\Services\CustomMarkingStore::class,
]
Dynamic Workflow Registration Register workflows at runtime:
Workflow::register([
'dynamic_workflow' => [...],
]);
Custom Transition Logic Extend the workflow class:
class CustomWorkflow extends Workflow
{
public function preApply(Transition $transition, $subject) {
// Custom logic before transition
}
}
Bind it in AppServiceProvider:
$this->app->bind(WorkflowInterface::class, CustomWorkflow::class);
Laravel Policies Integrate with Laravel’s authorization:
public function authorizePublish(User $user, BlogPost $post) {
return $post->workflow_can('publish');
}
Caching Workflows
Register workflows once (e.g., in boot()) to avoid repeated parsing:
public function boot() {
Workflow::register(config('workflow'));
}
Avoid Over-Fetching
Use with() to eager-load related models if workflows depend on them:
$post = BlogPost::with('author')->find(1);
How can I help you explore Laravel packages today?