solution-forest/workflow-engine-core
Framework-agnostic workflow engine core for PHP 8.3+. Define and run workflows with type-safe steps, state tracking/persistence, plugins for actions/storage, retries, timeouts, and rich error handling. Actively developed; not production-ready.
## Getting Started
### Minimal Setup for Laravel Integration
1. **Install the package**:
```bash
composer require solution-forest/workflow-engine-core
Create a storage adapter (e.g., database or file-based):
use SolutionForest\WorkflowEngine\Contracts\StorageAdapter;
use SolutionForest\WorkflowEngine\Core\WorkflowInstance;
class DatabaseStorageAdapter implements StorageAdapter
{
public function save(WorkflowInstance $instance): void
{
// Implement DB persistence
}
public function load(string $id): WorkflowInstance
{
// Implement DB retrieval
}
// ... other required methods
}
Define a simple workflow (e.g., app/Workflows/OrderWorkflow.php):
use SolutionForest\WorkflowEngine\Core\WorkflowBuilder;
use App\Actions\ValidateOrderAction;
use App\Actions\ProcessPaymentAction;
return WorkflowBuilder::create('order-processing')
->addStep('validate', ValidateOrderAction::class)
->addStep('process-payment', ProcessPaymentAction::class)
->build();
Initialize the engine (e.g., in a service provider):
$this->app->singleton(WorkflowEngine::class, function ($app) {
$storage = new DatabaseStorageAdapter();
$eventDispatcher = new NullEventDispatcher(); // Replace with Laravel's event system
return new WorkflowEngine($storage, $eventDispatcher);
});
Trigger a workflow (e.g., in a controller):
use SolutionForest\WorkflowEngine\Core\WorkflowEngine;
public function processOrder(Request $request, WorkflowEngine $engine)
{
$definition = include app_path('Workflows/OrderWorkflow.php');
$instanceId = $engine->start(
'order-processing',
$definition->toArray(),
['order_id' => $request->order_id]
);
return response()->json(['instance_id' => $instanceId]);
}
For a Laravel developer, the most immediate use case is order processing workflows with steps like:
Example:
$workflow = WorkflowBuilder::create('order-fulfillment')
->addStep('validate-order', ValidateOrderAction::class)
->when('order.amount > 1000', function ($builder) {
$builder->addStep('run-fraud-check', FraudCheckAction::class);
})
->addStep('process-payment', ProcessPaymentAction::class, timeout: 60)
->addStep('deduct-inventory', DeductInventoryAction::class)
->email('notify-customer', 'customer@example.com', 'Order Confirmed')
->build();
Laravel-specific actions should extend BaseAction and leverage Laravel's services:
use SolutionForest\WorkflowEngine\Actions\BaseAction;
use SolutionForest\WorkflowEngine\Core\WorkflowContext;
use SolutionForest\WorkflowEngine\Core\ActionResult;
use Illuminate\Support\Facades\Log;
class ProcessPaymentAction extends BaseAction
{
public function execute(WorkflowContext $context): ActionResult
{
$paymentGateway = app(PaymentGateway::class);
$result = $paymentGateway->charge(
$context->getData('order_id'),
$context->getData('amount')
);
if ($result->successful()) {
return ActionResult::success(['transaction_id' => $result->id]);
}
Log::error('Payment failed', ['error' => $result->message]);
return ActionResult::failure('Payment processing failed');
}
}
Key patterns:
app()) for dependencies$context->getData()ActionResult with success/failure statusLaravel service integration should use the engine as a facade:
// app/Providers/WorkflowServiceProvider.php
public function register()
{
$this->app->singleton(WorkflowEngine::class, function ($app) {
return new WorkflowEngine(
new DatabaseStorageAdapter(),
new LaravelEventDispatcher($app['events'])
);
});
}
Event handling should bridge Laravel events to workflow events:
// app/Listeners/WorkflowEventListener.php
public function handle(WorkflowCompletedEvent $event)
{
// Dispatch Laravel event
event(new OrderProcessed($event->workflowId, $event->data));
// Or trigger a job
ProcessOrderConfirmation::dispatch($event->workflowId);
}
Tracking workflow state in Laravel:
// Get workflow status
$instance = $engine->getInstance($instanceId);
$state = $instance->getState();
// Check in a controller
public function checkOrderStatus(Request $request, WorkflowEngine $engine)
{
$instance = $engine->getInstance($request->instance_id);
return response()->json([
'status' => $instance->getState()->value,
'progress' => $instance->getProgress(),
'current_step' => $instance->getCurrentStep()?->id,
]);
}
UI integration (Blade example):
@php
$state = $instance->getState();
@endphp
<div class="status-badge {{ $state->color() }}">
{{ $state->label() }} {{ $state->icon() }}
</div>
<p>{{ $state->description() }}</p>
Global exception handling for workflow failures:
// app/Exceptions/Handler.php
public function register()
{
$this->renderable(function (StepExecutionException $e, $request) {
return response()->json([
'error' => 'Workflow step failed',
'step' => $e->stepId,
'message' => $e->getMessage(),
'instance_id' => $e->workflowId,
], 500);
});
}
Retry logic for transient failures:
$workflow = WorkflowBuilder::create('reliable-process')
->addStep('api-call', ApiCallAction::class, retryAttempts: 3, backoff: 'exponential')
->build();
Pest test example for workflow execution:
test('order workflow completes successfully', function () {
$definition = WorkflowBuilder::create('test-order')
->addStep('validate', MockValidationAction::class)
->addStep('process', MockProcessingAction::class)
->build();
$instanceId = $this->engine->start(
'test-order',
$definition->toArray(),
['order_id' => 123]
);
$instance = $this->engine->getInstance($instanceId);
expect($instance->getState())->toBe(WorkflowState::COMPLETED);
expect($instance->getData('processed'))->toBe(true);
});
Mock actions for isolated testing:
class MockValidationAction extends BaseAction
{
public function execute(WorkflowContext $context): ActionResult
{
return ActionResult::success(['valid' => true]);
}
}
StorageAdapter methods will cause runtime errors.
// Missing method implementation
class BadStorageAdapter implements StorageAdapter {
public function save(WorkflowInstance $instance): void { /* ... */ }
// Missing: load(), delete(), findInstances(), etc.
}
InMemoryStorage as a starting point for custom adapters.InvalidWorkflowStateException.
$engine->resume($instanceId); // Throws if state is COMPLETED
$instance->getState()->canTransitionTo($targetState) before calling transition methods.true. Enable strict mode:
// In WorkflowBuilder
->when('order.amount > 1000', function ($builder) {
// ...
}, strict: true); // Throws on invalid conditions
// Test these in isolation
->when('user.is_active === true', ...)
->when('order.items.count > 0', ...)
// Wrong: Missing 'to' key for EmailAction
$builder->email('notify', 'subject', 'body');
// Right:
$builder->email('notify', 'user@example.com',
How can I help you explore Laravel packages today?