zaber-dev / laravel-workflow
Application-level workflow orchestration for Laravel with state transitions, audit history, rollback support, cache and database drivers, Eloquent integration, and a fluent API.
Requires
- php: ^8.2
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- nesbot/carbon: ^2.63|^3.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.0|^11.0|^12.0
README
Laravel Workflow
Supports: Laravel 11, 12 & 13+ • PHP 8.2+ • Redis • Memcached • Database
Application-level workflow orchestration for Laravel. Model business processes, manage state transitions, and track workflow history using cache or database storage.
Manage workflows with a clean, expressive API using cache or persistent database storage, attach them directly to Eloquent models, protect routes with declarative middleware, and extend the package with custom storage drivers.
Workflow Definition Defines the workflow graph (for example,
OrderWorkflow).Workflow Instance Tracks a specific entity moving through that graph (for example,
Order #1542).
Laravel Workflow manages workflow instances with validated state transitions, audit history, rollback support, and interchangeable storage drivers.
Workflow Lifecycle
Created
│
▼
In Progress
│
┌─┴────────────┐
▼ ▼
Approved Rejected
│
▼
Completed
Quick Example
Workflow::for($order, new OrderWorkflow()) ->block('payment_processed', function () { // Capture payment... // Send confirmation email... });
Documentation
Features
- Workflow lifecycle ⭐: Model complete business processes with validated state transitions, ensuring entities only move through permitted workflow steps.
- Atomic transitions ⭐: Prevent concurrent status mutations and race conditions across multi-threaded webhooks using row-level locking.
- Audit history ⭐: Every state transition logs exact chronological records including old step, new step, timestamp, transition metadata, and actor.
- Rollback support ⭐: Safely revert any workflow instance to a previously visited historical step using
$workflow->rollbackTo('previous_step'). - Expressive API: Chain expressive calls like
Workflow::for($order, new OrderWorkflow())->using('database')->transition('shipped'). - Eloquent integration: Attach the
HasWorkflowstrait to any model for scoped workflow management. - Middleware: Protect endpoints automatically using
workflow:step_name,WorkflowClasswith automatic HTTP409enforcement. - Storage drivers: Switch seamlessly between high-performance cache stores (Redis, Memcached, Array) and persistent database storage.
- DTOs: Work safely with strict
WorkflowStateInfoandWorkflowHistoryItemData Transfer Objects. - Extensibility: Register custom storage drivers on the fly with closure-based creators.
- Prunable storage: Built-in
Prunabletrait integration ensures completed or archived workflow instances never clutter your database.
Why not a status column?
A simple status column works well for straightforward applications.
Laravel Workflow is designed for applications where the process itself is part of the business logic.
- Order fulfillment
- Employee onboarding
- Approval chains
- Publishing workflows
- Claims processing
- KYC verification
| Feature | Status Column | Laravel Workflow |
|---|---|---|
| Primary Purpose | Simple column enum/status | Multi-step process orchestration & audit trails |
| Fluent Builder API | ❌ Manual | ✅ Expressive & Clean |
| First-Class Eloquent Integration | ❌ | ✅ Native (HasWorkflows) |
| Storage Architecture | ❌ DB Column Only | ✅ Cache & Database |
| Transition Validation | ❌ | ✅ |
| Chronological Audit Logs | ❌ Manual Table | ✅ Built-in (history()) |
| Historical Transition Data | ❌ | ✅ |
| Rollback to Previous Steps | ❌ | ✅ Built-in (rollbackTo()) |
| Auto-Rollback Route Middleware | ❌ | ✅ Built-in (RequireWorkflowStep) |
| Immutable DTOs | ❌ | ✅ Strict (CarbonImmutable) |
| Automatic Database Pruning | N/A | ✅ Built-in (Prunable) |
Installation
Ready to get started? Install the package with Composer:
composer require zaber-dev/laravel-workflow
Publish the configuration and database migrations:
php artisan vendor:publish --provider="ZaberDev\Workflow\WorkflowServiceProvider"
Run migrations if you intend to use the database driver:
php artisan migrate
Configuration
The configuration file lets you define the default storage driver, driver parameters, and event dispatching behaviors:
return [ /* |-------------------------------------------------------------------------- | Default Workflow Driver |-------------------------------------------------------------------------- | | Supported drivers: "cache", "database" | */ 'default' => env('WORKFLOW_DRIVER', 'cache'), 'drivers' => [ 'cache' => [ 'driver' => 'cache', 'store' => env('WORKFLOW_CACHE_STORE', null), 'prefix' => 'workflows:', ], 'database' => [ 'driver' => 'database', 'table' => 'workflow_instances', ], ], 'events' => [ 'dispatch' => true, ], ];
Usage Guide
1. Defining Your Workflow
Implement WorkflowDefinitionContract to define your steps, valid transitions, and starting point:
namespace App\Workflows; use ZaberDev\Workflow\Contracts\WorkflowDefinitionContract; class OrderProcessingWorkflow implements WorkflowDefinitionContract { public function name(): string { return 'order_processing'; } public function initialStep(): string { return 'created'; } public function steps(): array { return ['created', 'inventory_reserved', 'payment_processed', 'shipped', 'cancelled']; } public function transitions(): array { return [ 'created' => ['inventory_reserved', 'cancelled'], 'inventory_reserved' => ['payment_processed', 'cancelled'], 'payment_processed' => ['shipped', 'cancelled'], 'shipped' => [], 'cancelled' => [], ]; } }
2. The Fluent Workflow API
The Workflow facade provides an expressive builder interface for inspecting, transitioning, and rolling back workflow instances.
Checking Status & Transitioning
use ZaberDev\Workflow\Facades\Workflow; $instance = Workflow::for($order, new OrderProcessingWorkflow()); if ($instance->can('inventory_reserved')) { $instance->transition('inventory_reserved', [ 'reserved_at' => now()->toIso8601String(), 'warehouse' => 'US_WEST_1' ]); } // Check current status echo $instance->current(); // "inventory_reserved"
Atomic Block Transition (block)
For operations vulnerable to partial failure across external APIs, use the block() helper. block() transitions to your target step, executes your callback, and automatically rolls back (rollbackTo) if the closure throws an exception:
Workflow::for($order, new OrderProcessingWorkflow())->block('payment_processed', function () use ($order) { $this->stripeService->charge($order); }, payload: ['gateway' => 'stripe']);
Rolling Back & History Inspection
// Inspect chronological step history foreach ($instance->history() as $item) { echo "Step: " . $item->step . " at " . $item->transitionedAt->toDateTimeString(); } // Rollback to a previous step $instance->rollbackTo('created', 'Payment failed, returning order to created.');
3. Eloquent Model Integration (HasWorkflows)
Add the HasWorkflows trait to any Eloquent model (such as Order, Document, or User) to manage workflow instances directly off the entity:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use ZaberDev\Workflow\HasWorkflows; class Order extends Model { use HasWorkflows; }
You can now interact directly with your model instance:
$order = Order::find(1); // Transition this order to "payment_processed" $order->workflow(new OrderProcessingWorkflow())->transition('payment_processed'); // Check current step if ($order->workflow(new OrderProcessingWorkflow())->current() === 'shipped') { // Order already shipped }
Polymorphic Database Querying
When using the database driver, HasWorkflows also exposes a workflowInstances() polymorphic relationship, allowing direct querying and bulk management:
// Get all database workflow instances for this order $instances = $order->workflowInstances()->get();
4. Route Middleware
Protect routes declaratively without writing boilerplate checks in your controllers using the RequireWorkflowStep middleware:
use Illuminate\Support\Facades\Route; // Enforce that $order is currently in the "payment_processed" step before allowing shipment dispatch Route::post('/orders/{order}/ship', [ShipmentController::class, 'ship']) ->middleware('workflow:payment_processed,App\Workflows\OrderProcessingWorkflow');
How the Middleware Works
- Verifies the entity is currently in the required workflow step.
- Allows the request to continue when validation succeeds.
- Returns HTTP 409 Conflict when the entity is in an invalid state.
5. Working with Storage Drivers (using & driver)
By default, the package uses the storage driver defined in config/workflows.php. You can switch storage drivers on the fly per request or action:
// Store high-speed ephemeral user wizard workflows in cache Workflow::for($user, new WizardWorkflow())->using('cache')->transition('step_2'); // Store critical audit-trailed order workflows inside SQL database Workflow::for($order, new OrderProcessingWorkflow())->using('database')->transition('shipped');
Registering Custom Storage Drivers
You can extend the WorkflowManager with your own storage drivers (e.g., DynamoDB, MongoDB) in your AppServiceProvider:
use ZaberDev\Workflow\Contracts\WorkflowDriverContract; use ZaberDev\Workflow\Facades\Workflow; public function boot(): void { Workflow::extend('dynamodb', function ($app) { return new MyDynamoDbWorkflowDriver($app['config']['workflows.drivers.dynamodb']); }); }
6. Database Pruning (Prunable)
When using the database driver, completed or cancelled instances can be marked for pruning via Laravel's Prunable trait on the ZaberDev\Workflow\Models\WorkflowInstanceModel model.
To clean up old records automatically, schedule Laravel's model:prune command in your console.php or Kernel.php:
use Illuminate\Support\Facades\Schedule; use ZaberDev\Workflow\Models\WorkflowInstanceModel; Schedule::command('model:prune', ['--model' => WorkflowInstanceModel::class])->daily();
7. Events
Whenever a workflow transitions or rolls back, the package dispatches strongly typed events if enabled (workflows.events.dispatch = true):
ZaberDev\Workflow\Events\WorkflowTransitioned: Dispatched whentransition()succeeds ($key,$oldStep,$newStep,$payload,$info).ZaberDev\Workflow\Events\WorkflowRolledBack: Dispatched whenrollbackTo()succeeds ($key,$targetStep,$reason,$info).
You can listen to these in your EventServiceProvider for notification emails, analytics tracking, or webhook triggers.
Related Packages
This package is part of the ZaberDev Laravel Ecosystem (Laravel Productivity Toolkit) — a cohesive suite of high-level application primitives engineered for concurrency, state management, and resource allocation.
Explore the complete directory of packages, detailed use cases, and documentation in our Ecosystem Index Hub.
Testing & Quality
Run the comprehensive PHPUnit test suite locally:
composer test
Contributing
Thank you for considering contributing! Please ensure any pull requests include thorough PHPUnit tests covering unit, feature, and driver integration scenarios.
License
The MIT License (MIT). Please see LICENSE.md for more information.
Built with ❤️ as part of the ZaberDev Laravel Ecosystem.
