Distributed Workflow Engine
Difficulty: Advanced | Time: 3+ hours | Integration: State Machines, Commands, Events, Broadcasting, CQRS
Implement a sophisticated workflow system with state transitions, conditional branching, and distributed processing using specification-driven business rules with real-time updates and event sourcing.
The Challenge: Complex Workflow Management
Your enterprise application needs to handle complex, multi-step business processes (document approvals, order fulfillment, employee onboarding) with conditional logic, parallel processing, and real-time status updates across distributed teams.
🔍 The Problem: Monolithic Workflow Logic
Here's the nightmarish workflow code that has become unmaintainable:
<?php
namespace App\Services;
use App\Models\WorkflowInstance;
use App\Models\User;
use App\Models\Document;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class DocumentApprovalService
{
public function processDocumentSubmission(Document $document): void
{
// Hardcoded workflow logic mixed with business rules
$workflow = WorkflowInstance::create([
'type' => 'document_approval',
'subject_id' => $document->id,
'subject_type' => Document::class,
'status' => 'started',
'current_step' => 'initial_review',
'started_by' => auth()->id(),
]);
// Step 1: Initial Review
if ($document->department === 'hr') {
// HR documents need compliance check first
if ($this->needsComplianceReview($document)) {
$workflow->update(['current_step' => 'compliance_review']);
$this->assignToComplianceTeam($document);
} else {
$workflow->update(['current_step' => 'manager_review']);
$this->assignToManager($document);
}
} elseif ($document->department === 'finance') {
// Finance documents have different rules
if ($document->amount > 10000) {
$workflow->update(['current_step' => 'cfo_review']);
$this->assignToCFO($document);
} elseif ($document->amount > 1000) {
$workflow->update(['current_step' => 'finance_manager_review']);
$this->assignToFinanceManager($document);
} else {
$workflow->update(['current_step' => 'supervisor_review']);
$this->assignToSupervisor($document);
}
} elseif ($document->department === 'legal') {
// Legal documents always need senior review
$workflow->update(['current_step' => 'senior_legal_review']);
$this->assignToSeniorLegal($document);
} else {
// Default workflow for other departments
$workflow->update(['current_step' => 'manager_review']);
$this->assignToManager($document);
}
$this->sendNotifications($workflow, $document);
}
public function processApproval(WorkflowInstance $workflow, User $approver, string $action, ?string $comments = null): void
{
// More hardcoded state transitions
$document = $workflow->subject;
DB::transaction(function () use ($workflow, $approver, $action, $comments, $document) {
// Record the action
$workflow->actions()->create([
'user_id' => $approver->id,
'action' => $action,
'comments' => $comments,
'performed_at' => now(),
]);
if ($action === 'approve') {
// Determine next step based on current step and document type
switch ($workflow->current_step) {
case 'compliance_review':
$workflow->update(['current_step' => 'manager_review']);
$this->assignToManager($document);
break;
case 'manager_review':
if ($document->department === 'hr' && $document->type === 'policy_change') {
$workflow->update(['current_step' => 'senior_management_review']);
$this->assignToSeniorManagement($document);
} elseif ($document->department === 'finance' && $document->amount > 5000) {
$workflow->update(['current_step' => 'cfo_review']);
$this->assignToCFO($document);
} else {
$workflow->update(['current_step' => 'final_approval', 'status' => 'approved']);
$this->finalizeApproval($document);
}
break;
case 'cfo_review':
if ($document->amount > 50000) {
$workflow->update(['current_step' => 'board_review']);
$this->assignToBoard($document);
} else {
$workflow->update(['current_step' => 'final_approval', 'status' => 'approved']);
$this->finalizeApproval($document);
}
break;
case 'senior_legal_review':
if ($document->risk_level === 'high') {
$workflow->update(['current_step' => 'external_counsel_review']);
$this->assignToExternalCounsel($document);
} else {
$workflow->update(['current_step' => 'final_approval', 'status' => 'approved']);
$this->finalizeApproval($document);
}
break;
case 'board_review':
case 'external_counsel_review':
case 'senior_management_review':
$workflow->update(['current_step' => 'final_approval', 'status' => 'approved']);
$this->finalizeApproval($document);
break;
default:
throw new \Exception("Unknown workflow step: {$workflow->current_step}");
}
} elseif ($action === 'reject') {
$workflow->update(['status' => 'rejected', 'current_step' => 'rejected']);
$this->handleRejection($document, $comments);
} elseif ($action === 'request_changes') {
$workflow->update(['status' => 'changes_requested', 'current_step' => 'awaiting_changes']);
$this->requestChanges($document, $comments);
}
$this->sendNotifications($workflow, $document);
});
}
private function needsComplianceReview(Document $document): bool
{
// More hardcoded business logic
return in_array($document->type, ['policy_change', 'employee_handbook', 'contract'])
|| $document->contains_personal_data
|| $document->regulatory_impact;
}
private function assignToComplianceTeam(Document $document): void
{
// Hardcoded assignment logic
$complianceUsers = User::where('department', 'compliance')
->where('is_active', true)
->get();
foreach ($complianceUsers as $user) {
$user->notify(new DocumentReviewNotification($document, 'compliance_review'));
}
}
// ... hundreds more lines of hardcoded workflow logic
}
🎯 Workflow Requirements Analysis
Let's identify the complex workflow patterns:
- Dynamic State Transitions: Next steps depend on document type, amount, department
- Conditional Branching: Different paths based on business rules
- Parallel Processing: Multiple reviewers, concurrent approvals
- Role-Based Assignment: Automatic task assignment based on roles/departments
- Real-Time Updates: Live status updates across distributed teams
- Audit Trail: Complete workflow history with event sourcing
- Escalation Logic: Time-based escalations and deadline management
- External Integrations: Third-party approvals and notifications
- Rollback/Retry: Handle failures and process corrections
Step 1: Generate Workflow Specifications
Using the Artisan command to create our workflow specifications:
# Workflow state specifications
php artisan make:specification Workflow/WorkflowStateSpecification --model=WorkflowInstance
php artisan make:specification Workflow/TransitionEligibilitySpecification --model=WorkflowInstance
php artisan make:specification Workflow/StepCompletionSpecification --model=WorkflowInstance
# Business rule specifications for transitions
php artisan make:specification Workflow/ComplianceReviewRequiredSpecification --model=Document
php artisan make:specification Workflow/HighValueApprovalSpecification --model=Document
php artisan make:specification Workflow/SeniorReviewRequiredSpecification --model=Document
php artisan make:specification Workflow/ExternalApprovalRequiredSpecification --model=Document
# Assignment specifications
php artisan make:specification Workflow/ReviewerAssignmentSpecification --model=User
php artisan make:specification Workflow/EscalationRequiredSpecification --model=WorkflowInstance
php artisan make:specification Workflow/ParallelProcessingSpecification --model=WorkflowInstance
# Composite workflow specifications
php artisan make:specification Workflow/DocumentApprovalWorkflowSpecification --composite
php artisan make:specification Workflow/FinanceApprovalWorkflowSpecification --composite
php artisan make:specification Workflow/LegalReviewWorkflowSpecification --composite
Step 2: Core Workflow State Management
WorkflowStateSpecification
<?php
namespace App\Specifications\Workflow;
use App\Models\WorkflowInstance;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;
class WorkflowStateSpecification extends AbstractSpecification
{
public function __construct(
private readonly string|array $validStates,
private readonly ?string $expectedPreviousState = null
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof WorkflowInstance) {
return false;
}
$validStates = is_array($this->validStates) ? $this->validStates : [$this->validStates];
$currentStateValid = in_array($candidate->status, $validStates);
if (!$currentStateValid) {
return false;
}
// Check previous state if specified
if ($this->expectedPreviousState) {
$lastAction = $candidate->actions()->latest()->first();
return $lastAction && $lastAction->previous_state === $this->expectedPreviousState;
}
return true;
}
public function toQuery(Builder $query): Builder
{
$validStates = is_array($this->validStates) ? $this->validStates : [$this->validStates];
return $query->whereIn('status', $validStates);
}
public function canTransitionTo(WorkflowInstance $workflow, string $targetState): bool
{
$allowedTransitions = $this->getAllowedTransitions($workflow->status);
return in_array($targetState, $allowedTransitions);
}
private function getAllowedTransitions(string $currentState): array
{
return match($currentState) {
'pending' => ['in_progress', 'cancelled'],
'in_progress' => ['completed', 'paused', 'rejected', 'escalated'],
'paused' => ['in_progress', 'cancelled'],
'escalated' => ['in_progress', 'completed', 'rejected'],
'completed' => ['archived'],
'rejected' => ['pending', 'archived'], // Allow restart
'cancelled' => ['archived'],
'archived' => [], // Terminal state
default => []
};
}
}
TransitionEligibilitySpecification
<?php
namespace App\Specifications\Workflow;
use App\Models\WorkflowInstance;
use App\Models\User;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;
class TransitionEligibilitySpecification extends AbstractSpecification
{
public function __construct(
private readonly string $targetState,
private readonly User $actor,
private readonly ?array $requiredRoles = null,
private readonly ?array $requiredPermissions = null
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof WorkflowInstance) {
return false;
}
return $this->hasValidState($candidate) &&
$this->hasRequiredRole($candidate) &&
$this->hasRequiredPermissions($candidate) &&
$this->isAssignedReviewer($candidate) &&
$this->meetsBusinessRules($candidate);
}
public function toQuery(Builder $query): Builder
{
// Complex business logic - handle at collection level
return $query;
}
private function hasValidState(WorkflowInstance $workflow): bool
{
$stateSpec = new WorkflowStateSpecification(['in_progress', 'escalated']);
return $stateSpec->canTransitionTo($workflow, $this->targetState);
}
private function hasRequiredRole(WorkflowInstance $workflow): bool
{
if (!$this->requiredRoles) {
return true;
}
return $this->actor->hasAnyRole($this->requiredRoles);
}
private function hasRequiredPermissions(WorkflowInstance $workflow): bool
{
if (!$this->requiredPermissions) {
return true;
}
foreach ($this->requiredPermissions as $permission) {
if (!$this->actor->can($permission, $workflow->subject)) {
return false;
}
}
return true;
}
private function isAssignedReviewer(WorkflowInstance $workflow): bool
{
// Check if user is assigned to current step
return $workflow->currentAssignments()
->where('user_id', $this->actor->id)
->exists();
}
private function meetsBusinessRules(WorkflowInstance $workflow): bool
{
// Additional business-specific rules
if ($this->targetState === 'completed' && $workflow->type === 'document_approval') {
// Ensure all required approvals are obtained
return $workflow->hasAllRequiredApprovals();
}
if ($this->targetState === 'escalated') {
// Can only escalate if deadline is approaching or exceeded
return $workflow->isEscalationEligible();
}
return true;
}
}
Step 3: Business Rule Specifications
ComplianceReviewRequiredSpecification
<?php
namespace App\Specifications\Workflow;
use App\Models\Document;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;
class ComplianceReviewRequiredSpecification extends AbstractSpecification
{
private array $complianceRequiredTypes = [
'policy_change',
'employee_handbook',
'contract',
'data_processing_agreement',
'privacy_policy'
];
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof Document) {
return false;
}
return $this->requiresComplianceByType($candidate) ||
$this->requiresComplianceByContent($candidate) ||
$this->requiresComplianceByImpact($candidate);
}
public function toQuery(Builder $query): Builder
{
return $query->where(function ($q) {
$q->whereIn('type', $this->complianceRequiredTypes)
->orWhere('contains_personal_data', true)
->orWhere('regulatory_impact', true)
->orWhere('risk_level', '>=', 7);
});
}
private function requiresComplianceByType(Document $document): bool
{
return in_array($document->type, $this->complianceRequiredTypes);
}
private function requiresComplianceByContent(Document $document): bool
{
return $document->contains_personal_data ||
$document->contains_financial_data ||
$document->contains_confidential_info;
}
private function requiresComplianceByImpact(Document $document): bool
{
return $document->regulatory_impact ||
$document->risk_level >= 7 ||
$document->affects_multiple_departments;
}
public function getComplianceRequirements(Document $document): array
{
$requirements = [];
if ($this->requiresComplianceByType($document)) {
$requirements[] = 'Document type requires compliance review';
}
if ($document->contains_personal_data) {
$requirements[] = 'Contains personal data - GDPR compliance check required';
}
if ($document->regulatory_impact) {
$requirements[] = 'Regulatory impact assessment required';
}
if ($document->risk_level >= 7) {
$requirements[] = 'High risk level requires compliance approval';
}
return $requirements;
}
}
HighValueApprovalSpecification
<?php
namespace App\Specifications\Workflow;
use App\Models\Document;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;
class HighValueApprovalSpecification extends AbstractSpecification
{
private array $approvalThresholds = [
'finance' => [
'supervisor' => 1000,
'manager' => 10000,
'cfo' => 50000,
'board' => 100000,
],
'hr' => [
'manager' => 5000,
'hr_director' => 25000,
'ceo' => 50000,
],
'legal' => [
'senior_counsel' => 0, // All legal docs need senior review
'external_counsel' => 25000,
],
'it' => [
'it_manager' => 10000,
'cto' => 50000,
'board' => 200000,
],
];
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof Document) {
return false;
}
return $this->requiresHighValueApproval($candidate);
}
public function toQuery(Builder $query): Builder
{
// Build dynamic query based on department thresholds
return $query->where(function ($q) {
foreach ($this->approvalThresholds as $department => $thresholds) {
$minThreshold = min(array_values($thresholds));
$q->orWhere(function ($deptQuery) use ($department, $minThreshold) {
$deptQuery->where('department', $department)
->where('amount', '>', $minThreshold);
});
}
});
}
private function requiresHighValueApproval(Document $document): bool
{
$thresholds = $this->approvalThresholds[$document->department] ?? [];
if (empty($thresholds)) {
return false;
}
$minThreshold = min(array_values($thresholds));
return $document->amount > $minThreshold;
}
public function getRequiredApprovalLevel(Document $document): string
{
$thresholds = $this->approvalThresholds[$document->department] ?? [];
$amount = $document->amount;
$requiredLevel = 'supervisor'; // Default
foreach ($thresholds as $level => $threshold) {
if ($amount > $threshold) {
$requiredLevel = $level;
}
}
return $requiredLevel;
}
public function getApprovalChain(Document $document): array
{
$thresholds = $this->approvalThresholds[$document->department] ?? [];
$amount = $document->amount;
$chain = [];
foreach ($thresholds as $level => $threshold) {
if ($amount > $threshold) {
$chain[] = $level;
}
}
return $chain;
}
}
Step 4: Workflow Engine Core
Create a sophisticated workflow engine using specifications:
<?php
namespace App\Services;
use App\Models\WorkflowInstance;
use App\Models\WorkflowDefinition;
use App\Models\WorkflowStep;
use App\Models\User;
use App\Events\WorkflowStateChanged;
use App\Events\WorkflowStepAssigned;
use App\Events\WorkflowCompleted;
use App\Specifications\Workflow\TransitionEligibilitySpecification;
use App\Specifications\Workflow\ComplianceReviewRequiredSpecification;
use App\Specifications\Workflow\HighValueApprovalSpecification;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Collection;
class WorkflowEngineService
{
public function __construct(
private readonly ComplianceReviewRequiredSpecification $complianceSpec,
private readonly HighValueApprovalSpecification $highValueSpec
) {}
public function startWorkflow(string $workflowType, $subject, User $initiator, array $context = []): WorkflowInstance
{
return DB::transaction(function () use ($workflowType, $subject, $initiator, $context) {
$definition = WorkflowDefinition::where('type', $workflowType)
->where('is_active', true)
->first();
if (!$definition) {
throw new \Exception("No active workflow definition found for type: {$workflowType}");
}
$workflow = WorkflowInstance::create([
'workflow_definition_id' => $definition->id,
'type' => $workflowType,
'subject_id' => $subject->getKey(),
'subject_type' => get_class($subject),
'status' => 'pending',
'context' => $context,
'started_by' => $initiator->id,
'started_at' => now(),
]);
// Determine first step based on business rules
$firstStep = $this->determineFirstStep($workflow, $subject);
$this->transitionToStep($workflow, $firstStep);
Log::info('Workflow started', [
'workflow_id' => $workflow->id,
'type' => $workflowType,
'subject' => get_class($subject) . ':' . $subject->getKey(),
'first_step' => $firstStep->name,
]);
event(new WorkflowStateChanged($workflow, null, 'pending'));
return $workflow;
});
}
public function processTransition(
WorkflowInstance $workflow,
User $actor,
string $action,
array $data = []
): bool {
return DB::transaction(function () use ($workflow, $actor, $action, $data) {
// Validate transition eligibility
$targetState = $this->getTargetState($workflow->current_step, $action);
$transitionSpec = new TransitionEligibilitySpecification(
$targetState,
$actor,
$this->getRequiredRoles($workflow->current_step, $action),
$this->getRequiredPermissions($workflow->current_step, $action)
);
if (!$transitionSpec->isSatisfiedBy($workflow)) {
throw new \Exception('User not eligible for this transition');
}
// Record the action
$workflowAction = $workflow->actions()->create([
'user_id' => $actor->id,
'action' => $action,
'step' => $workflow->current_step,
'data' => $data,
'performed_at' => now(),
]);
$previousState = $workflow->status;
// Process the transition
if ($action === 'approve') {
$this->processApproval($workflow, $actor, $data);
} elseif ($action === 'reject') {
$this->processRejection($workflow, $actor, $data);
} elseif ($action === 'request_changes') {
$this->processChangeRequest($workflow, $actor, $data);
} elseif ($action === 'escalate') {
$this->processEscalation($workflow, $actor, $data);
}
event(new WorkflowStateChanged($workflow, $previousState, $workflow->status));
return true;
});
}
private function determineFirstStep(WorkflowInstance $workflow, $subject): WorkflowStep
{
$definition = $workflow->definition;
$firstSteps = $definition->steps()->where('is_initial', true)->get();
if ($firstSteps->count() === 1) {
return $firstSteps->first();
}
// Multiple possible first steps - use specifications to decide
foreach ($firstSteps as $step) {
if ($this->stepConditionsMet($step, $subject)) {
return $step;
}
}
throw new \Exception('No suitable first step found for workflow');
}
private function stepConditionsMet(WorkflowStep $step, $subject): bool
{
$conditions = $step->conditions ?? [];
foreach ($conditions as $condition) {
$specClass = $condition['specification'];
$spec = app($specClass, $condition['parameters'] ?? []);
if (!$spec->isSatisfiedBy($subject)) {
return false;
}
}
return true;
}
private function processApproval(WorkflowInstance $workflow, User $actor, array $data): void
{
$currentStep = $workflow->currentStep();
$nextSteps = $this->determineNextSteps($workflow, $currentStep, 'approved');
if (empty($nextSteps)) {
// No next steps - workflow complete
$workflow->update([
'status' => 'completed',
'completed_at' => now(),
]);
event(new WorkflowCompleted($workflow));
} else {
// Transition to next step(s)
foreach ($nextSteps as $nextStep) {
$this->transitionToStep($workflow, $nextStep);
}
}
}
private function processRejection(WorkflowInstance $workflow, User $actor, array $data): void
{
$workflow->update([
'status' => 'rejected',
'current_step' => null,
]);
// Clear all assignments
$workflow->assignments()->update(['completed_at' => now()]);
}
private function processChangeRequest(WorkflowInstance $workflow, User $actor, array $data): void
{
$workflow->update([
'status' => 'changes_requested',
'current_step' => 'awaiting_changes',
]);
// Notify document owner
$this->notifySubjectOwner($workflow, 'changes_requested', $data);
}
private function processEscalation(WorkflowInstance $workflow, User $actor, array $data): void
{
$escalationStep = $this->getEscalationStep($workflow);
if ($escalationStep) {
$workflow->update(['status' => 'escalated']);
$this->transitionToStep($workflow, $escalationStep);
}
}
private function determineNextSteps(WorkflowInstance $workflow, WorkflowStep $currentStep, string $outcome): array
{
$subject = $workflow->subject;
$transitions = $currentStep->transitions()
->where('trigger', $outcome)
->get();
$nextSteps = [];
foreach ($transitions as $transition) {
$targetStep = $transition->targetStep;
// Check if transition conditions are met
if ($this->stepConditionsMet($targetStep, $subject)) {
$nextSteps[] = $targetStep;
// If not parallel processing, take first valid step
if (!$transition->parallel) {
break;
}
}
}
return $nextSteps;
}
private function transitionToStep(WorkflowInstance $workflow, WorkflowStep $step): void
{
$workflow->update([
'current_step' => $step->name,
'status' => 'in_progress',
]);
// Assign reviewers for this step
$assignees = $this->getStepAssignees($workflow, $step);
foreach ($assignees as $assignee) {
$workflow->assignments()->create([
'step_name' => $step->name,
'user_id' => $assignee->id,
'assigned_at' => now(),
'due_at' => now()->addHours($step->sla_hours ?? 24),
]);
event(new WorkflowStepAssigned($workflow, $step, $assignee));
}
}
private function getStepAssignees(WorkflowInstance $workflow, WorkflowStep $step): Collection
{
$assignmentRules = $step->assignment_rules ?? [];
$assignees = collect();
foreach ($assignmentRules as $rule) {
$ruleAssignees = match($rule['type']) {
'role' => User::role($rule['role'])->get(),
'department' => User::where('department', $rule['department'])->get(),
'user' => User::whereIn('id', $rule['user_ids'])->get(),
'dynamic' => $this->getDynamicAssignees($workflow, $rule),
default => collect()
};
$assignees = $assignees->merge($ruleAssignees);
}
return $assignees->unique('id');
}
private function getDynamicAssignees(WorkflowInstance $workflow, array $rule): Collection
{
$specClass = $rule['specification'];
$spec = app($specClass, $rule['parameters'] ?? []);
return User::all()->filter(fn($user) => $spec->isSatisfiedBy($user));
}
}
Step 5: Real-Time Updates with Broadcasting
Implement real-time workflow updates:
<?php
namespace App\Events;
use App\Models\WorkflowInstance;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class WorkflowStateChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public readonly WorkflowInstance $workflow,
public readonly ?string $previousState,
public readonly string $newState
) {}
public function broadcastOn(): array
{
return [
new PresenceChannel('workflow.' . $this->workflow->id),
new Channel('workflows'),
new Channel('user.' . $this->workflow->started_by),
];
}
public function broadcastWith(): array
{
return [
'workflow_id' => $this->workflow->id,
'type' => $this->workflow->type,
'previous_state' => $this->previousState,
'new_state' => $this->newState,
'current_step' => $this->workflow->current_step,
'subject' => [
'type' => $this->workflow->subject_type,
'id' => $this->workflow->subject_id,
],
'timestamp' => now()->toISOString(),
];
}
public function broadcastAs(): string
{
return 'workflow.state.changed';
}
}
Step 6: Command Pattern Integration
Create workflow commands for complex operations:
<?php
namespace App\Commands\Workflow;
use App\Models\WorkflowInstance;
use App\Models\User;
use App\Services\WorkflowEngineService;
use Illuminate\Support\Facades\Log;
abstract class WorkflowCommand
{
protected WorkflowInstance $workflow;
protected User $actor;
protected array $context;
public function __construct(WorkflowInstance $workflow, User $actor, array $context = [])
{
$this->workflow = $workflow;
$this->actor = $actor;
$this->context = $context;
}
abstract public function execute(): bool;
public function canExecute(): bool
{
return true;
}
public function getWorkflow(): WorkflowInstance
{
return $this->workflow;
}
}
<?php
namespace App\Commands\Workflow;
use App\Specifications\Workflow\TransitionEligibilitySpecification;
class ApproveDocumentCommand extends WorkflowCommand
{
public function canExecute(): bool
{
$spec = new TransitionEligibilitySpecification(
'approved',
$this->actor,
['reviewer', 'manager', 'admin'],
['approve_documents']
);
return $spec->isSatisfiedBy($this->workflow);
}
public function execute(): bool
{
if (!$this->canExecute()) {
throw new \Exception('User not authorized to approve this document');
}
$engineService = app(WorkflowEngineService::class);
return $engineService->processTransition(
$this->workflow,
$this->actor,
'approve',
$this->context
);
}
}
Step 7: Event Sourcing Integration
Implement event sourcing for complete audit trail:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class WorkflowEvent extends Model
{
protected $fillable = [
'workflow_instance_id',
'event_type',
'event_data',
'actor_id',
'occurred_at',
'sequence_number',
];
protected $casts = [
'event_data' => 'array',
'occurred_at' => 'datetime',
];
public function workflow(): BelongsTo
{
return $this->belongsTo(WorkflowInstance::class, 'workflow_instance_id');
}
public function actor(): BelongsTo
{
return $this->belongsTo(User::class, 'actor_id');
}
}
<?php
namespace App\Services;
use App\Models\WorkflowInstance;
use App\Models\WorkflowEvent;
use App\Models\User;
class WorkflowEventStore
{
public function record(
WorkflowInstance $workflow,
string $eventType,
array $eventData,
?User $actor = null
): WorkflowEvent {
$lastSequence = WorkflowEvent::where('workflow_instance_id', $workflow->id)
->max('sequence_number') ?? 0;
return WorkflowEvent::create([
'workflow_instance_id' => $workflow->id,
'event_type' => $eventType,
'event_data' => $eventData,
'actor_id' => $actor?->id,
'occurred_at' => now(),
'sequence_number' => $lastSequence + 1,
]);
}
public function replayWorkflow(WorkflowInstance $workflow): WorkflowInstance
{
$events = WorkflowEvent::where('workflow_instance_id', $workflow->id)
->orderBy('sequence_number')
->get();
// Reset workflow to initial state
$workflow->status = 'pending';
$workflow->current_step = null;
// Replay all events
foreach ($events as $event) {
$this->applyEvent($workflow, $event);
}
return $workflow;
}
private function applyEvent(WorkflowInstance $workflow, WorkflowEvent $event): void
{
match($event->event_type) {
'workflow.started' => $workflow->status = 'pending',
'step.assigned' => $workflow->current_step = $event->event_data['step_name'],
'transition.completed' => $workflow->status = $event->event_data['new_state'],
'workflow.completed' => $workflow->status = 'completed',
'workflow.rejected' => $workflow->status = 'rejected',
default => null
};
}
}
Step 8: Comprehensive Testing
Integration Testing
<?php
namespace Tests\Feature;
use App\Models\WorkflowDefinition;
use App\Models\WorkflowStep;
use App\Models\Document;
use App\Models\User;
use App\Services\WorkflowEngineService;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class WorkflowEngineIntegrationTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function complete_document_approval_workflow_with_compliance_review()
{
// Arrange
$this->createDocumentApprovalWorkflow();
$hrManager = User::factory()->create(['role' => 'hr_manager']);
$complianceOfficer = User::factory()->create(['role' => 'compliance_officer']);
$ceo = User::factory()->create(['role' => 'ceo']);
$document = Document::factory()->create([
'type' => 'policy_change',
'department' => 'hr',
'contains_personal_data' => true,
'created_by' => $hrManager->id,
]);
$workflowService = app(WorkflowEngineService::class);
// Act & Assert
// 1. Start workflow
$workflow = $workflowService->startWorkflow('document_approval', $document, $hrManager);
$this->assertEquals('compliance_review', $workflow->current_step);
$this->assertEquals('in_progress', $workflow->status);
// 2. Compliance approval
$this->assertTrue($workflowService->processTransition(
$workflow->fresh(),
$complianceOfficer,
'approve',
['comments' => 'GDPR compliance verified']
));
$workflow->refresh();
$this->assertEquals('manager_review', $workflow->current_step);
// 3. Manager approval
$this->assertTrue($workflowService->processTransition(
$workflow,
$hrManager,
'approve',
['comments' => 'Policy changes look good']
));
$workflow->refresh();
$this->assertEquals('senior_management_review', $workflow->current_step);
// 4. Senior management approval
$this->assertTrue($workflowService->processTransition(
$workflow,
$ceo,
'approve',
['comments' => 'Final approval granted']
));
$workflow->refresh();
$this->assertEquals('completed', $workflow->status);
$this->assertNotNull($workflow->completed_at);
}
/** @test */
public function workflow_handles_rejection_correctly()
{
// Arrange
$this->createDocumentApprovalWorkflow();
$manager = User::factory()->create(['role' => 'manager']);
$document = Document::factory()->create(['department' => 'general']);
$workflowService = app(WorkflowEngineService::class);
// Act
$workflow = $workflowService->startWorkflow('document_approval', $document, $manager);
$this->assertTrue($workflowService->processTransition(
$workflow,
$manager,
'reject',
['comments' => 'Document needs significant revisions']
));
// Assert
$workflow->refresh();
$this->assertEquals('rejected', $workflow->status);
$this->assertNull($workflow->current_step);
}
/** @test */
public function workflow_enforces_role_based_access_control()
{
// Arrange
$this->createDocumentApprovalWorkflow();
$regularUser = User::factory()->create(['role' => 'user']);
$document = Document::factory()->create();
$workflowService = app(WorkflowEngineService::class);
$workflow = $workflowService->startWorkflow('document_approval', $document, $regularUser);
// Act & Assert
$this->expectException(\Exception::class);
$this->expectExceptionMessage('User not eligible for this transition');
$workflowService->processTransition(
$workflow,
$regularUser, // User without reviewer role
'approve'
);
}
private function createDocumentApprovalWorkflow(): void
{
$definition = WorkflowDefinition::create([
'name' => 'Document Approval',
'type' => 'document_approval',
'version' => 1,
'is_active' => true,
]);
// Create workflow steps
$complianceStep = WorkflowStep::create([
'workflow_definition_id' => $definition->id,
'name' => 'compliance_review',
'display_name' => 'Compliance Review',
'is_initial' => true,
'conditions' => [
['specification' => 'App\Specifications\Workflow\ComplianceReviewRequiredSpecification']
],
'assignment_rules' => [
['type' => 'role', 'role' => 'compliance_officer']
],
'sla_hours' => 48,
]);
$managerStep = WorkflowStep::create([
'workflow_definition_id' => $definition->id,
'name' => 'manager_review',
'display_name' => 'Manager Review',
'is_initial' => true,
'assignment_rules' => [
['type' => 'role', 'role' => 'manager']
],
'sla_hours' => 24,
]);
$seniorStep = WorkflowStep::create([
'workflow_definition_id' => $definition->id,
'name' => 'senior_management_review',
'display_name' => 'Senior Management Review',
'assignment_rules' => [
['type' => 'role', 'role' => 'ceo']
],
'sla_hours' => 72,
]);
// Create transitions
$complianceStep->transitions()->create([
'trigger' => 'approved',
'target_step_id' => $managerStep->id,
]);
$managerStep->transitions()->create([
'trigger' => 'approved',
'target_step_id' => $seniorStep->id,
'conditions' => [
['specification' => 'App\Specifications\Workflow\SeniorReviewRequiredSpecification']
]
]);
}
}
Key Learnings
This advanced example demonstrates:
✅ Sophisticated State Management: Complex workflow transitions with business rules
✅ Specification-Driven Logic: Dynamic workflow paths based on specifications
✅ Real-Time Updates: Live workflow status broadcasting to distributed teams
✅ Event Sourcing: Complete audit trail with event replay capabilities
✅ Command Pattern: Encapsulated workflow operations with authorization
✅ Role-Based Access: Fine-grained permissions for workflow actions
✅ Enterprise Scalability: Distributed processing with queue integration
Next Steps
With your distributed workflow engine:
- Add workflow analytics to track performance and bottlenecks
- Implement machine learning for automatic assignment optimization
- Create visual workflow designer for business users
- Add integration APIs for external system workflow triggers
Congratulations! You've mastered the most advanced specification patterns for enterprise-grade Laravel applications.