jasperfernandez / laraflow
Workflow engine for Laravel
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/contracts: ^11.0||^12.0||^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^11.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.35
This package is auto-updated.
Last update: 2026-04-29 00:31:56 UTC
README
Laraflow is a workflow engine for Laravel applications. It lets you define workflow templates with ordered steps, assign roles to each step, configure allowed actions, and run workflow instances against any Eloquent model.
It is a good fit for application flows such as membership approvals, onboarding, request routing, document review, or any process that needs step history, assignments, status transitions, and audit trails.
Installation
Install the package with Composer:
composer require jasperfernandez/laraflow
Publish and run the package migrations:
php artisan vendor:publish --tag="laraflow-migrations"
php artisan migrate
Publish the config file:
php artisan vendor:publish --tag="laraflow-config"
Before You Start
Laraflow manages workflow templates, workflow instances, workflow steps, assignments, and transitions. Your application is responsible for the domain records that Laraflow references:
- roles
- statuses
- actions
- the subject model you want to attach a workflow to
By default, the package checks whether the acting user can execute a step by calling hasRole(string $role) on the authenticated user model. If your user model does not expose that method, the default authorization will deny the action.
Configuration
After publishing the config, point the package at your own role, status, and action models:
<?php use App\Models\Action; use App\Models\Role; use App\Models\Status; use JasperFernandez\Laraflow\Models\WorkflowInstance; use JasperFernandez\Laraflow\Models\WorkflowInstanceStep; use JasperFernandez\Laraflow\Models\WorkflowInstanceStepAssignment; use JasperFernandez\Laraflow\Models\WorkflowInstanceTransition; return [ 'table_names' => [ 'workflow_templates' => 'workflow_templates', 'workflow_template_steps' => 'workflow_template_steps', 'workflow_template_step_assignments' => 'workflow_template_step_assignments', 'workflow_template_step_actions' => 'workflow_template_step_actions', 'workflow_instances' => 'workflow_instances', 'workflow_instance_steps' => 'workflow_instance_steps', 'workflow_instance_step_assignments' => 'workflow_instance_step_assignments', 'workflow_instance_transitions' => 'workflow_instance_transitions', ], 'column_names' => [ 'model_morph_key' => 'subject_id', 'model_morph_type' => 'subject_type', ], 'models' => [ 'role' => Role::class, 'status' => Status::class, 'action' => Action::class, 'workflow_instance' => WorkflowInstance::class, 'workflow_instance_step' => WorkflowInstanceStep::class, 'workflow_instance_step_assignment' => WorkflowInstanceStepAssignment::class, 'workflow_instance_transition' => WorkflowInstanceTransition::class, ], ];
Your application models should provide these fields:
Role:id,nameStatus:id,code,nameAction:id,code,name
Defining A Workflow
A workflow is defined with four main records:
- A
WorkflowTemplate - One or more
WorkflowTemplateSteprecords - One or more
WorkflowTemplateStepAssignmentrecords that assign roles to each step - One or more
WorkflowTemplateStepActionrecords that define which actions are allowed and where they lead
Example:
use App\Models\Action; use App\Models\Role; use App\Models\Status; use JasperFernandez\Laraflow\Models\WorkflowTemplate; use JasperFernandez\Laraflow\Models\WorkflowTemplateStep; use JasperFernandez\Laraflow\Models\WorkflowTemplateStepAction; use JasperFernandez\Laraflow\Models\WorkflowTemplateStepAssignment; $memberRole = Role::firstOrCreate(['name' => 'member']); $reviewerRole = Role::firstOrCreate(['name' => 'reviewer']); $completedStep = Status::firstOrCreate( ['code' => 'completed'], ['name' => 'Completed'], ); $pendingReview = Status::firstOrCreate( ['code' => 'pending_eligibility_verification'], ['name' => 'Pending Eligibility Verification'], ); $approvedStep = Status::firstOrCreate( ['code' => 'approved_step'], ['name' => 'Approved Step'], ); $approvedApplication = Status::firstOrCreate( ['code' => 'approved'], ['name' => 'Approved'], ); $submitAction = Action::firstOrCreate( ['code' => 'submit_application'], ['name' => 'Submit Application'], ); $approveAction = Action::firstOrCreate( ['code' => 'approve_application'], ['name' => 'Approve Application'], ); $template = WorkflowTemplate::create([ 'template_code' => 'MEMBERSHIP-APPLICATION', 'template_name' => 'Membership Application Workflow', 'description' => 'Workflow for membership application approvals', 'is_active' => true, ]); $registrationStep = WorkflowTemplateStep::create([ 'workflow_template_id' => $template->id, 'step_code' => 'APPLICANT_REGISTRATION', 'step_name' => 'Applicant Registration', 'step_description' => 'Initial registration step', 'sequence_no' => 1, 'is_active' => true, ]); $reviewStep = WorkflowTemplateStep::create([ 'workflow_template_id' => $template->id, 'step_code' => 'ELIGIBILITY_REVIEW', 'step_name' => 'Eligibility Review', 'step_description' => 'Review submitted application', 'sequence_no' => 2, 'is_active' => true, ]); WorkflowTemplateStepAssignment::create([ 'workflow_template_step_id' => $registrationStep->id, 'role_id' => $memberRole->id, ]); WorkflowTemplateStepAssignment::create([ 'workflow_template_step_id' => $reviewStep->id, 'role_id' => $reviewerRole->id, ]); WorkflowTemplateStepAction::create([ 'workflow_template_step_id' => $registrationStep->id, 'action_id' => $submitAction->id, 'next_workflow_template_step_id' => $reviewStep->id, 'completes_step' => true, 'resulting_step_status_id' => $completedStep->id, 'resulting_subject_status_id' => $pendingReview->id, 'closes_workflow' => false, ]); WorkflowTemplateStepAction::create([ 'workflow_template_step_id' => $reviewStep->id, 'action_id' => $approveAction->id, 'next_workflow_template_step_id' => null, 'completes_step' => true, 'resulting_step_status_id' => $approvedStep->id, 'resulting_subject_status_id' => $approvedApplication->id, 'closes_workflow' => true, ]);
Starting A Workflow
Use WorkflowEngine::start() to create a workflow instance for any Eloquent model:
use App\Models\Application; use JasperFernandez\Laraflow\Services\WorkflowEngine; $application = Application::findOrFail(1); $instance = app(WorkflowEngine::class)->start( templateCode: 'MEMBERSHIP-APPLICATION', subject: $application, context: [ 'started_by' => auth()->id(), 'channel' => 'portal', ], );
When a workflow starts, Laraflow:
- resolves the active template by
template_code - picks the lowest step
sequence_no - creates a workflow instance
- opens the first runtime step
- creates runtime assignments for the valid roles on that step
Applying An Action
Use WorkflowEngine::apply() to execute an action on the current step:
use App\Models\User; use JasperFernandez\Laraflow\Data\TransitionPayload; use JasperFernandez\Laraflow\Services\WorkflowEngine; $actor = User::findOrFail(1); $result = app(WorkflowEngine::class)->apply( instance: $instance->fresh(), actionCode: 'submit_application', actor: $actor, payload: new TransitionPayload( remarks: 'Submitted for eligibility review', metadata: ['channel' => 'portal'], ), );
The returned TransitionResult includes:
instance: the updated workflow instancefromStep: the step that was acted ontoStep: the next opened step, ornullif the workflow was closedtransition: the recorded transition rowclosed: whether the workflow is now closed
Authorization
The default authorization strategy is role-based. The package checks the assigned role names on the current step and calls hasRole() on the actor.
Example:
class User extends Authenticatable { public function hasRole(string $role): bool { return $this->roles()->where('name', $role)->exists(); } }
If you want a different authorization strategy, bind your own implementation of JasperFernandez\Laraflow\Contracts\WorkflowAuthorization in your application container.
End-To-End Example
use App\Models\Application; use App\Models\User; use JasperFernandez\Laraflow\Data\TransitionPayload; use JasperFernandez\Laraflow\Services\WorkflowEngine; $engine = app(WorkflowEngine::class); $application = Application::create([ 'name' => 'Jane Doe', ]); $instance = $engine->start( templateCode: 'MEMBERSHIP-APPLICATION', subject: $application, context: ['started_by' => auth()->id()], ); $member = User::findOrFail(1); $firstResult = $engine->apply( instance: $instance->fresh(), actionCode: 'submit_application', actor: $member, payload: new TransitionPayload( remarks: 'Application submitted', ), ); $reviewer = User::findOrFail(2); $finalResult = $engine->apply( instance: $firstResult->instance->fresh(), actionCode: 'approve_application', actor: $reviewer, payload: new TransitionPayload( remarks: 'Application approved', ), ); if ($finalResult->closed) { // The workflow has finished. }
Subject Integration
To easily manage workflows on your models, use the HasWorkflows trait:
use Illuminate\Database\Eloquent\Model; use JasperFernandez\Laraflow\Traits\HasWorkflows; class Application extends Model { use HasWorkflows; } // Usage $application = Application::find(1); $currentWorkflow = $application->currentWorkflow; $allWorkflows = $application->workflowInstances;
Events
Laraflow dispatches the following events during the workflow lifecycle:
| Event | Dispatched When |
|---|---|
WorkflowStarted |
A new workflow instance is initialized. |
WorkflowTransitioned |
An action is successfully applied to a step. |
WorkflowClosed |
A workflow instance is marked as closed. |
You can listen to these events in your EventServiceProvider:
use JasperFernandez\Laraflow\Events\WorkflowTransitioned; public function boot() { Event::listen(WorkflowTransitioned::class, function ($event) { // $event->result is a TransitionResult DTO }); }
Notes
- Use
WorkflowEngineas the main entry point. - The package can attach workflows to any Eloquent model through Laravel morph relationships.
- Workflow definitions are loaded from the database through the default repository implementation.
- Inactive workflow templates are ignored.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Credits
License
The MIT License (MIT). Please see LICENSE.md for more information.