Install the Package
composer require zerodahero/laravel-workflow
Publish the config file (if needed):
php artisan vendor:publish --provider="Zerodahero\Workflow\WorkflowServiceProvider"
Define a Workflow
Create a workflow definition in config/workflow.php (or publish the default config):
'workflows' => [
'order' => [
'supports' => [Order::class],
'places' => ['draft', 'submitted', 'paid', 'shipped', 'cancelled'],
'transitions' => [
'submit' => ['from' => 'draft', 'to' => 'submitted'],
'pay' => ['from' => 'submitted', 'to' => 'paid'],
// ...
],
'initial_places' => ['draft'],
'property' => 'status', // Model property to track workflow state
],
],
Apply to a Model
Use the HasWorkflow trait in your model (e.g., Order):
use Zerodahero\Workflow\Traits\HasWorkflow;
class Order extends Model
{
use HasWorkflow;
protected $workflow = 'order'; // Matches config key
}
First Use Case: Transition an Order
$order = Order::find(1);
$order->apply('submit'); // Transitions from 'draft' to 'submitted'
$order->save(); // Persist the state change
Model-Based Workflows
HasWorkflow trait for models with stateful processes (e.g., orders, tickets, approvals).Ticket through new → assigned → resolved → closed.getWorkflowProperty() to customize the property name dynamically.Service Layer Abstraction
WorkflowManager into services to decouple workflow logic from controllers:
public function __construct(private WorkflowManager $workflows) {}
public function processOrder(Order $order) {
$this->workflows->apply($order, 'pay');
}
Guard Clauses for Transitions
if ($order->can('pay')) {
$order->apply('pay');
} else {
throw new \RuntimeException('Order cannot be paid in current state.');
}
Event-Driven Extensions
Workflow::onTransitioning(function (TransitionEvent $event) {
if ($event->getTransition()->getName() === 'pay') {
// Send payment confirmation email
}
});
Custom Transition Logic
canApply() in your model to enforce business rules:
public function canApply($transition, $to)
{
if ($transition === 'cancel' && $this->status === 'shipped') {
return false;
}
return parent::canApply($transition, $to);
}
Bulk Workflow Updates
WorkflowManager::applyTo() for batch processing:
$orders = Order::where('status', 'submitted')->get();
foreach ($orders as $order) {
$this->workflows->apply($order, 'pay');
}
Property Mismatch
property in config matches the model’s attribute name (e.g., status vs. workflow_status).dd($model->getWorkflowProperty()) to verify.Initial State Confusion
initial_places is an array (e.g., ['draft']), not a string.initial_place.Transition Guard Bypass
canApply() can silently fail if not called explicitly. Use:
if (!$this->canApply($transition, $to)) {
throw new \RuntimeException("Transition '$transition' blocked.");
}
Database Sync Issues
$model->save() after apply() to persist the state.class OrderObserver {
public function saved(Order $order) {
if ($order->isDirty($order->getWorkflowProperty())) {
$order->workflow->persist();
}
}
}
Circular Dependencies
A → B → A). Use validate() in transitions:
'transitions' => [
'approve' => [
'from' => 'draft',
'to' => 'approved',
'validate' => function ($order) {
return $order->meetsApprovalCriteria();
},
],
],
Inspect Workflow State
dd($model->workflow->getPlaces());
dd($model->workflow->getCurrentPlaces());
Enable Workflow Logging
Add to config/workflow.php:
'debug' => env('WORKFLOW_DEBUG', false),
Test Transitions Isolated Use PHPUnit to verify transitions:
public function testOrderCannotPayWhenNotSubmitted()
{
$order = new Order(['status' => 'draft']);
$this->assertFalse($order->can('pay'));
}
Custom Transition Validators
Extend Workflow to add reusable validators:
class OrderWorkflow extends Workflow
{
protected function validatePayTransition(Order $order)
{
return $order->amount > 0;
}
}
Workflow Events Dispatch custom events for transitions:
Workflow::onTransitioned(function (TransitionEvent $event) {
event(new OrderPaid($event->getSubject()));
});
Dynamic Workflows Load workflows from the database:
$workflowConfig = WorkflowConfig::find($model->workflow_config_id);
$this->workflows->load($workflowConfig->toArray());
API Integration Expose workflow transitions via API:
Route::post('/orders/{order}/transition', function (Order $order, Request $request) {
$order->apply($request->transition);
return response()->json(['status' => 'transited']);
});
Fallback for Missing Transitions Handle undefined transitions gracefully:
try {
$order->apply('unknown');
} catch (TransitionException $e) {
report($e);
return back()->withError('Invalid transition.');
}
How can I help you explore Laravel packages today?