diff --git a/composer.json b/composer.json index 03cf64a..b222214 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "illuminate/database": "^10.0||^11.0||^12.0", "illuminate/events": "^10.0||^11.0||^12.0", "illuminate/support": "^10.0||^11.0||^12.0", - "solution-forest/workflow-engine-core": "^0.0.2-alpha" + "solution-forest/workflow-engine-core": "dev-main || ^0.0.3-alpha" }, "conflict": { "laravel/framework": "<11.0.0" diff --git a/docs/advanced-features.md b/docs/advanced-features.md index 251928a..e2b4e34 100644 --- a/docs/advanced-features.md +++ b/docs/advanced-features.md @@ -74,7 +74,7 @@ class UnreliableApiAction implements WorkflowAction Available backoff strategies: - `linear` - Fixed delay between retries -- `exponential` - Exponentially increasing delay +- `exponential` - Exponentially increasing delay - `fixed` - Same delay for all retries ### Condition Attribute @@ -99,7 +99,7 @@ class ConditionalAction implements WorkflowAction Condition expressions support: - Property access: `user.email`, `order.amount` -- Comparisons: `>`, `<`, `>=`, `<=`, `=`, `!=` +- Comparisons: `>`, `<`, `>=`, `<=`, `===`, `!==`, `==`, `!=` - Null checks: `is null`, `is not null` - Operators: `and`, `or` @@ -125,30 +125,15 @@ class ProcessPaymentAction implements WorkflowAction ## Error Handling and Retries -### Automatic Retries +### Configuring Retries via Builder -Configure automatic retries for unreliable operations: +Configure automatic retries when adding steps to the workflow: ```php $workflow = WorkflowBuilder::create('robust-workflow') - ->addStep('api-call', ApiCallAction::class, [], null, 3) // 3 retry attempts - ->addStep('database-operation', DatabaseAction::class, [], null, 5) // 5 retry attempts - ->build(); -``` - -### Backoff Strategies - -- **Linear**: Fixed delay between retries -- **Exponential**: Increasing delay (1s, 2s, 4s, 8s...) -- **Custom**: Define your own backoff function - -```php -// Custom backoff function -$workflow = WorkflowBuilder::create('custom-retry') - ->addStep('operation', MyAction::class, [], null, 3) // Basic retry with 3 attempts - ->retry(attempts: 3, backoff: function($attempt) { - return $attempt * 1000; // 1s, 2s, 3s - }) + ->addStep('api-call', ApiCallAction::class, [], null, 3) // 3 retry attempts + ->addStep('database-op', DatabaseAction::class, [], null, 5) // 5 retry attempts + ->addStep('quick-task', QuickAction::class, [], '30s', 2) // 30s timeout, 2 retries ->build(); ``` @@ -165,15 +150,27 @@ class ProcessPaymentAction implements WorkflowAction $payment = $this->chargeCard($context->getData('payment')); return ActionResult::success(['payment_id' => $payment->id]); } catch (PaymentException $e) { - // Trigger compensation workflow - CompensationWorkflow::start([ - 'original_context' => $context, - 'error' => $e->getMessage() + return ActionResult::failure('Payment failed: ' . $e->getMessage(), [ + 'error_type' => 'payment_failure', + 'original_error' => $e->getMessage() ]); - - return ActionResult::failure('Payment failed: ' . $e->getMessage()); } } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('payment'); + } + + public function getName(): string + { + return 'Process Payment'; + } + + public function getDescription(): string + { + return 'Processes customer payment via configured gateway'; + } } ``` @@ -181,32 +178,21 @@ class ProcessPaymentAction implements WorkflowAction ### Step-Level Timeouts -Set timeouts for individual steps: +Set timeouts for individual steps using the builder: ```php $workflow = WorkflowBuilder::create('timed-workflow') - ->step('quick-operation', QuickAction::class) - ->timeout(seconds: 30) - ->step('slow-operation', SlowAction::class) - ->timeout(minutes: 5) + ->addStep('quick-operation', QuickAction::class, timeout: 30) // 30 seconds + ->addStep('slow-operation', SlowAction::class, timeout: '5m') // 5 minutes + ->addStep('long-task', LongTaskAction::class, timeout: '2h') // 2 hours ->build(); ``` -### Workflow-Level Timeouts - -Set a timeout for the entire workflow: - -```php -$workflow = WorkflowBuilder::create('deadline-workflow') - ->globalTimeout(hours: 2) - ->step('step1', ActionOne::class) - ->step('step2', ActionTwo::class) - ->build(); -``` +Timeout string formats: `'30s'` (seconds), `'5m'` (minutes), `'2h'` (hours), `'1d'` (days). ### Timeout Handling -Handle timeouts gracefully: +Handle timeouts gracefully in your actions: ```php class TimeSensitiveAction implements WorkflowAction @@ -215,16 +201,31 @@ class TimeSensitiveAction implements WorkflowAction public function execute(WorkflowContext $context): ActionResult { $startTime = time(); - + while (time() - $startTime < 25) { // Leave 5 seconds buffer if ($this->operationComplete()) { return ActionResult::success(); } sleep(1); } - + return ActionResult::failure('Operation timed out'); } + + public function canExecute(WorkflowContext $context): bool + { + return true; + } + + public function getName(): string + { + return 'Time Sensitive Operation'; + } + + public function getDescription(): string + { + return 'Performs a time-sensitive operation with graceful timeout'; + } } ``` @@ -232,44 +233,34 @@ class TimeSensitiveAction implements WorkflowAction ### Simple Conditions -Use simple expressions for conditions: +Use the `when()` method on the builder for conditional steps: ```php $workflow = WorkflowBuilder::create('conditional-flow') - ->when('user.type == "premium"', fn($builder) => - $builder->step('premium-benefits', PremiumBenefitsAction::class) + ->addStep('validate', ValidateAction::class) + ->when('user.type === "premium"', fn($builder) => + $builder->addStep('premium-benefits', PremiumBenefitsAction::class) ) ->when('order.total > 100', fn($builder) => - $builder->step('apply-discount', DiscountAction::class) + $builder->addStep('apply-discount', DiscountAction::class) ) ->build(); ``` -### Complex Conditions +### Condition Steps -Use complex logic with the ConditionAction: +Use the built-in `ConditionAction` for inline condition evaluation: ```php -use SolutionForest\WorkflowEngine\Actions\ConditionAction; - -$workflow = WorkflowBuilder::create('complex-conditions') - ->step('evaluate', new ConditionAction([ - 'user.age >= 18 AND user.verified == true' => [ - ['action' => VerifiedAdultAction::class], - ], - 'user.age >= 18 AND user.verified == false' => [ - ['action' => RequestVerificationAction::class], - ], - 'user.age < 18' => [ - ['action' => MinorUserAction::class], - ], - ])) +$workflow = WorkflowBuilder::create('condition-check') + ->condition('user.verified === true') + ->addStep('proceed', ProceedAction::class) ->build(); ``` ### Dynamic Conditions -Evaluate conditions at runtime: +Evaluate conditions at runtime within your action: ```php class DynamicConditionAction implements WorkflowAction @@ -277,104 +268,57 @@ class DynamicConditionAction implements WorkflowAction public function execute(WorkflowContext $context): ActionResult { $user = $context->getData('user'); - + if ($this->shouldSendWelcomeEmail($user)) { return ActionResult::success(['next_action' => 'send_welcome']); } - + if ($this->shouldRequestVerification($user)) { return ActionResult::success(['next_action' => 'request_verification']); } - + return ActionResult::success(['next_action' => 'skip']); } -} -``` - -## Parallel Execution - -### Fork and Join - -Execute multiple branches in parallel: - -```php -$workflow = WorkflowBuilder::create('parallel-processing') - ->fork([ - 'email-branch' => fn($builder) => - $builder->email('notification', to: '{{ user.email }}'), - 'sms-branch' => fn($builder) => - $builder->step('send-sms', SendSmsAction::class), - 'push-branch' => fn($builder) => - $builder->step('push-notification', PushNotificationAction::class) - ]) - ->join() - ->step('cleanup', CleanupAction::class) - ->build(); -``` - -### Async Actions -Mark actions as asynchronous: + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('user'); + } -```php -use SolutionForest\WorkflowEngine\Attributes\Async; + public function getName(): string + { + return 'Dynamic Condition Check'; + } -class UploadFileAction implements WorkflowAction -{ - #[Async] - public function execute(WorkflowContext $context): ActionResult + public function getDescription(): string { - // This action will run in a separate queue job - $file = $context->getData('file'); - $this->uploadToS3($file); - - return ActionResult::success(['upload_url' => $url]); + return 'Evaluates conditions dynamically at runtime'; } } ``` -## Queue Integration +## Quick Workflow Templates -### Background Processing - -Long-running workflows automatically use Laravel's queue system: +Use pre-built workflow patterns for common scenarios: ```php -// This workflow will run in the background -$workflow = WorkflowBuilder::create('background-workflow') - ->step('heavy-processing', HeavyProcessingAction::class) - ->delay(hours: 1) - ->step('followup', FollowupAction::class) - ->build(); +use SolutionForest\WorkflowEngine\Core\WorkflowBuilder; -// Start it -$instance = $workflow->start($data); -``` - -### Queue Configuration - -Configure queue settings in your config file: - -```php -// config/workflow-engine.php -return [ - 'queue' => [ - 'connection' => 'redis', - 'queue' => 'workflows', - 'retry_after' => 300, - 'max_attempts' => 3, - ], -]; -``` - -### Priority Queues +// User onboarding template +$onboarding = WorkflowBuilder::quick() + ->userOnboarding('premium-onboarding') + ->then(SetupPremiumFeaturesAction::class) + ->build(); -Set priorities for workflow jobs: +// Order processing template +$orderFlow = WorkflowBuilder::quick() + ->orderProcessing('express-order') + ->build(); -```php -$workflow = WorkflowBuilder::create('priority-workflow') - ->priority('high') - ->step('urgent-task', UrgentTaskAction::class) +// Document approval template +$approval = WorkflowBuilder::quick() + ->documentApproval('legal-review') + ->addStep('legal-sign-off', LegalSignOffAction::class) ->build(); ``` @@ -382,12 +326,15 @@ $workflow = WorkflowBuilder::create('priority-workflow') ### Workflow Events -Listen to workflow events: +Listen to workflow events in your Laravel application: ```php use SolutionForest\WorkflowEngine\Events\WorkflowStarted; use SolutionForest\WorkflowEngine\Events\WorkflowCompletedEvent; use SolutionForest\WorkflowEngine\Events\WorkflowFailedEvent; +use SolutionForest\WorkflowEngine\Events\WorkflowCancelled; +use SolutionForest\WorkflowEngine\Events\StepCompletedEvent; +use SolutionForest\WorkflowEngine\Events\StepFailedEvent; // In your EventServiceProvider protected $listen = [ @@ -402,101 +349,88 @@ protected $listen = [ LogWorkflowFailure::class, AlertAdministrators::class, ], + StepCompletedEvent::class => [ + TrackStepProgress::class, + ], + StepFailedEvent::class => [ + LogStepFailure::class, + ], ]; ``` -### Custom Metrics +### Workflow Status Monitoring -Track custom metrics: +Track workflow status programmatically: ```php -class MetricsAction implements WorkflowAction -{ - public function execute(WorkflowContext $context): ActionResult - { - $startTime = microtime(true); - - // Your business logic - $result = $this->performOperation(); - - $duration = microtime(true) - $startTime; - - // Track metrics - Metrics::timing('workflow.step.duration', $duration, [ - 'workflow' => $context->getWorkflowName(), - 'step' => $context->getCurrentStepId(), - ]); - - return ActionResult::success(['result' => $result]); - } -} -``` +$engine = app(WorkflowEngine::class); -### Health Checks +// Get workflow status +$status = $engine->getStatus($instanceId); +// Returns: workflow_id, name, state, current_step, progress, created_at, updated_at -Monitor workflow health: +// List workflows with filters +$running = $engine->listWorkflows(['state' => 'running']); +$recent = $engine->listWorkflows(['state' => 'failed', 'limit' => 10]); -```php -Route::get('/health/workflows', function() { - $stats = WorkflowEngine::getHealthStats(); - - return response()->json([ - 'status' => $stats['failed_count'] > 10 ? 'unhealthy' : 'healthy', - 'running_workflows' => $stats['running_count'], - 'failed_workflows' => $stats['failed_count'], - 'average_duration' => $stats['avg_duration'], - ]); -}); +// Get workflow instance details +$instance = $engine->getInstance($instanceId); +echo $instance->getProgress(); // 0.0 to 100.0 +echo $instance->getState()->label(); // 'Running', 'Completed', etc. +echo $instance->getStatusSummary(); // Full status summary array ``` ## Testing Workflows ### Unit Testing Actions -Test individual actions: +Test individual actions with a `WorkflowContext`: ```php class ProcessPaymentActionTest extends TestCase { public function test_successful_payment() { - $context = new WorkflowContext([ - 'payment' => ['amount' => 100, 'token' => 'tok_123'] - ], 'workflow-1', 'payment-step'); - + $context = new WorkflowContext( + workflowId: 'workflow-1', + stepId: 'payment-step', + data: [ + 'payment' => ['amount' => 100, 'token' => 'tok_123'] + ] + ); + $action = new ProcessPaymentAction(); $result = $action->execute($context); - - $this->assertTrue($result->success); - $this->assertArrayHasKey('payment_id', $result->data); + + $this->assertTrue($result->isSuccess()); + $this->assertNotEmpty($result->get('payment_id')); } } ``` ### Integration Testing -Test complete workflows: +Test complete workflows using the engine: ```php class OrderWorkflowTest extends TestCase { public function test_complete_order_workflow() { - $order = Order::factory()->create(); - - $workflow = WorkflowBuilder::create('test-order') - ->step('validate', ValidateOrderAction::class) - ->step('process-payment', ProcessPaymentAction::class) - ->step('fulfill', FulfillOrderAction::class) + $engine = app(WorkflowEngine::class); + + $definition = WorkflowBuilder::create('test-order') + ->addStep('validate', ValidateOrderAction::class) + ->addStep('process-payment', ProcessPaymentAction::class) + ->addStep('fulfill', FulfillOrderAction::class) ->build(); - - $instance = $workflow->start(['order' => $order]); - - // Simulate workflow execution - $instance->run(); - - $this->assertEquals(WorkflowState::Completed, $instance->getState()); - $this->assertTrue($order->fresh()->is_fulfilled); + + $instanceId = $engine->start('test-order-1', $definition->toArray(), [ + 'order' => ['id' => 1, 'total' => 99.99] + ]); + + $instance = $engine->getInstance($instanceId); + $this->assertEquals(WorkflowState::COMPLETED, $instance->getState()); } } ``` @@ -513,12 +447,17 @@ class ExternalApiActionTest extends TestCase Http::fake([ 'api.example.com/*' => Http::response(['success' => true], 200) ]); - - $context = new WorkflowContext(['data' => 'test'], 'workflow-1', 'api-step'); + + $context = new WorkflowContext( + workflowId: 'workflow-1', + stepId: 'api-step', + data: ['data' => 'test'] + ); + $action = new ExternalApiAction(); $result = $action->execute($context); - - $this->assertTrue($result->success); + + $this->assertTrue($result->isSuccess()); Http::assertSent(function ($request) { return $request->url() === 'https://api.example.com/webhook'; }); diff --git a/docs/api-reference.md b/docs/api-reference.md index ccd7a0f..bcdc521 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -6,21 +6,38 @@ The `WorkflowBuilder` class provides a fluent API for creating workflows. ### Methods -#### `create(string $name): self` +#### `create(string $name): static` -Creates a new workflow builder instance. +Creates a new workflow builder instance. Name must start with a letter and contain only letters, numbers, hyphens, and underscores. ```php $builder = WorkflowBuilder::create('my-workflow'); ``` -#### `addStep(string $id, string $actionClass, array $config = [], ?int $timeout = null, int $retryAttempts = 0): self` +#### `description(string $description): self` -Adds a custom action step to the workflow. +Sets a human-readable description for the workflow. + +```php +$builder->description('Handles complete user onboarding process'); +``` + +#### `version(string $version): self` + +Sets the workflow version for change tracking. + +```php +$builder->version('2.1.0'); +``` + +#### `addStep(string $id, string|WorkflowAction $action, array $config = [], string|int|null $timeout = null, int $retryAttempts = 0): self` + +Adds a custom action step to the workflow. Timeout accepts seconds as integer or string format (`'30s'`, `'5m'`, `'2h'`, `'1d'`). Retry attempts must be between 0 and 10. ```php $builder->addStep('process-payment', ProcessPaymentAction::class); $builder->addStep('process-payment', ProcessPaymentAction::class, ['currency' => 'USD'], 30, 3); +$builder->addStep('slow-task', SlowAction::class, timeout: '5m', retryAttempts: 3); ``` #### `email(string $template, string $to, string $subject, array $data = []): self` @@ -42,19 +59,27 @@ $builder->http('https://api.example.com/webhooks', 'POST', [ ]); ``` -#### `delay(int $seconds = null, int $minutes = null, int $hours = null, int $days = null): self` +#### `delay(int $seconds = null, int $minutes = null, int $hours = null): self` Adds a delay step to the workflow. ```php $builder->delay(minutes: 30); $builder->delay(hours: 2); -$builder->delay(days: 1); +$builder->delay(hours: 1, minutes: 30); // 1.5 hour delay +``` + +#### `condition(string $condition): self` + +Adds a condition check step for workflow branching. + +```php +$builder->condition('user.verified === true'); ``` #### `when(string $condition, callable $callback): self` -Adds conditional logic to the workflow. +Adds conditional logic to the workflow. Steps added in the callback are only executed when the condition is met. ```php $builder->when('user.age >= 18', function($builder) { @@ -62,22 +87,34 @@ $builder->when('user.age >= 18', function($builder) { }); ``` -#### `startWith(string $actionClass, array $config = [], ?int $timeout = null, int $retryAttempts = 0): self` +#### `startWith(string|WorkflowAction $action, array $config = [], string|int|null $timeout = null, int $retryAttempts = 0): self` -Adds the first step in a workflow (syntactic sugar for better readability). +Adds the first step in a workflow (syntactic sugar for better readability). Auto-generates a step ID. ```php $builder->startWith(ValidateInputAction::class, ['strict' => true]); ``` -#### `then(string $actionClass, array $config = [], ?int $timeout = null, int $retryAttempts = 0): self` +#### `then(string|WorkflowAction $action, array $config = [], string|int|null $timeout = null, int $retryAttempts = 0): self` -Adds a sequential step (syntactic sugar for better readability). +Adds a sequential step (syntactic sugar for better readability). Auto-generates a step ID. ```php $builder->then(ProcessDataAction::class)->then(SaveResultAction::class); ``` +#### `withMetadata(array $metadata): self` + +Adds custom metadata to the workflow definition. + +```php +$builder->withMetadata([ + 'author' => 'John Doe', + 'department' => 'Engineering', + 'priority' => 'high' +]); +``` + #### `build(): WorkflowDefinition` Builds and returns the workflow definition. @@ -86,75 +123,216 @@ Builds and returns the workflow definition. $workflow = $builder->build(); ``` +#### `quick(): QuickWorkflowBuilder` (static) + +Returns a `QuickWorkflowBuilder` instance for pre-built common workflow patterns. + +```php +$workflow = WorkflowBuilder::quick()->userOnboarding('new-user-flow'); +$workflow = WorkflowBuilder::quick()->orderProcessing(); +$workflow = WorkflowBuilder::quick()->documentApproval(); +``` + +## WorkflowAction Interface + +The `WorkflowAction` interface defines the contract for all workflow step implementations. + +### Methods + +#### `execute(WorkflowContext $context): ActionResult` + +Execute the workflow action with the provided context. This is the core method where the action's business logic is implemented. + +#### `canExecute(WorkflowContext $context): bool` + +Check if this action can be executed with the given context. Allows for pre-execution validation and conditional logic. + +#### `getName(): string` + +Get the human-readable display name for this action. + +#### `getDescription(): string` + +Get a detailed description of what this action does. + +```php +class CreateUserProfileAction implements WorkflowAction +{ + public function execute(WorkflowContext $context): ActionResult + { + $userData = $context->getData('user'); + $profile = UserProfile::create(['user_id' => $userData['id']]); + return ActionResult::success(['profile_id' => $profile->id]); + } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('user.id'); + } + + public function getName(): string + { + return 'Create User Profile'; + } + + public function getDescription(): string + { + return 'Creates a new user profile in the database'; + } +} +``` + ## WorkflowContext -The `WorkflowContext` class holds the data that flows through the workflow. +The `WorkflowContext` class holds the data that flows through the workflow. It is an immutable `final readonly` class. ### Properties +#### `readonly string $workflowId` + +The unique workflow instance ID. + +#### `readonly string $stepId` + +The ID of the current step. + #### `readonly array $data` The workflow data. -#### `readonly string $workflowId` +#### `readonly array $config` -The unique workflow instance ID. +Step-specific configuration parameters. -#### `readonly string $currentStepId` +#### `readonly ?WorkflowInstance $instance` -The ID of the current step. +The associated workflow instance (if available). + +#### `readonly DateTime $executedAt` + +Timestamp when this context was created. ### Methods -#### `getData(string $key = null): mixed` +#### `getData(?string $key = null, mixed $default = null): mixed` -Gets data from the context. +Gets data from the context. Without parameters returns all data. Supports dot notation for nested access. ```php $allData = $context->getData(); $user = $context->getData('user'); $userName = $context->getData('user.name'); +$email = $context->getData('user.email', 'unknown@example.com'); +``` + +#### `with(string $key, mixed $value): static` + +Creates a new context with a single data value set. Supports dot notation. + +```php +$newContext = $context->with('user.verified', true); +$newContext = $context->with('order.items.0.quantity', 2); +``` + +#### `withData(array $newData): static` + +Creates a new context with additional data merged in (immutable operation). + +```php +$newContext = $context->withData([ + 'order' => ['id' => 123, 'total' => 99.99], + 'payment' => ['method' => 'credit_card'] +]); ``` -#### `with(array $data): self` +#### `hasData(string $key): bool` -Creates a new context with additional data. +Check if a data key exists in the context. Supports dot notation. ```php -$newContext = $context->with(['step_result' => $result]); +if ($context->hasData('user.email')) { + // User email is available +} ``` -#### `withData(string $key, mixed $value): self` +#### `getConfig(?string $key = null, mixed $default = null): mixed` -Creates a new context with a single data value. +Get configuration value(s) for the current step. Supports dot notation. ```php -$newContext = $context->withData('status', 'completed'); +$timeout = $context->getConfig('timeout', 30); +$retries = $context->getConfig('retry.attempts', 3); +$allConfig = $context->getConfig(); // Gets all configuration ``` +#### `getWorkflowId(): string` + +Get the workflow identifier. + +#### `getStepId(): string` + +Get the current step identifier. + +#### `toArray(): array` + +Convert the context to an array representation for serialization. + ## WorkflowState Enum representing workflow states. ### Values -- `Pending` - Workflow is created but not started -- `Running` - Workflow is currently executing -- `Completed` - Workflow finished successfully -- `Failed` - Workflow failed with an error -- `Cancelled` - Workflow was cancelled -- `Paused` - Workflow is temporarily paused +- `PENDING` - Workflow is created but not started +- `RUNNING` - Workflow is currently executing +- `WAITING` - Workflow is waiting for external input or conditions +- `PAUSED` - Workflow is temporarily paused +- `COMPLETED` - Workflow finished successfully +- `FAILED` - Workflow failed with an error +- `CANCELLED` - Workflow was cancelled ### Methods +#### `isActive(): bool` + +Returns true for active states (`PENDING`, `RUNNING`, `WAITING`, `PAUSED`). + +```php +if ($state->isActive()) { + // Workflow can still progress +} +``` + +#### `isFinished(): bool` + +Returns true for terminal states (`COMPLETED`, `FAILED`, `CANCELLED`). + +```php +if ($state->isFinished()) { + // Workflow execution has ended +} +``` + +#### `isSuccessful(): bool` + +Returns true only for `COMPLETED` state. + +#### `isError(): bool` + +Returns true only for `FAILED` state. + #### `color(): string` -Returns a color representing the state. +Returns a color name representing the state. ```php -WorkflowState::Running->color(); // 'blue' -WorkflowState::Completed->color(); // 'green' -WorkflowState::Failed->color(); // 'red' +WorkflowState::PENDING->color(); // 'gray' +WorkflowState::RUNNING->color(); // 'blue' +WorkflowState::WAITING->color(); // 'yellow' +WorkflowState::PAUSED->color(); // 'orange' +WorkflowState::COMPLETED->color(); // 'green' +WorkflowState::FAILED->color(); // 'red' +WorkflowState::CANCELLED->color(); // 'purple' ``` #### `icon(): string` @@ -162,9 +340,9 @@ WorkflowState::Failed->color(); // 'red' Returns an emoji icon for the state. ```php -WorkflowState::Running->icon(); // '▶️' -WorkflowState::Completed->icon(); // '✅' -WorkflowState::Failed->icon(); // '❌' +WorkflowState::RUNNING->icon(); // '▶️' +WorkflowState::COMPLETED->icon(); // '✅' +WorkflowState::FAILED->icon(); // '❌' ``` #### `label(): string` @@ -172,8 +350,18 @@ WorkflowState::Failed->icon(); // '❌' Returns a human-readable label. ```php -WorkflowState::Running->label(); // 'In Progress' -WorkflowState::Completed->label(); // 'Completed' +WorkflowState::PENDING->label(); // 'Pending' +WorkflowState::RUNNING->label(); // 'Running' +WorkflowState::COMPLETED->label(); // 'Completed' +``` + +#### `description(): string` + +Returns a detailed description of what the state means. + +```php +WorkflowState::RUNNING->description(); +// 'The workflow is actively executing steps. One or more actions are currently being processed.' ``` #### `canTransitionTo(WorkflowState $state): bool` @@ -181,118 +369,231 @@ WorkflowState::Completed->label(); // 'Completed' Checks if the state can transition to another state. ```php -if ($currentState->canTransitionTo(WorkflowState::Completed)) { +if ($currentState->canTransitionTo(WorkflowState::COMPLETED)) { // Can transition to completed } ``` +Valid transitions: +- `PENDING` -> `RUNNING`, `CANCELLED` +- `RUNNING` -> `WAITING`, `PAUSED`, `COMPLETED`, `FAILED`, `CANCELLED` +- `WAITING` -> `RUNNING`, `FAILED`, `CANCELLED` +- `PAUSED` -> `RUNNING`, `CANCELLED` +- Terminal states (`COMPLETED`, `FAILED`, `CANCELLED`) -> none + +#### `getValidTransitions(): array` + +Returns all possible states that can be transitioned to from this state. + +```php +$validStates = WorkflowState::RUNNING->getValidTransitions(); +// [WAITING, PAUSED, COMPLETED, FAILED, CANCELLED] +``` + ## ActionResult -Represents the result of an action execution. +Represents the result of an action execution. This is an immutable value object. ### Static Methods -#### `success(array $data = []): self` +#### `success(array $data = [], array $metadata = []): static` -Creates a successful result. +Creates a successful result with optional data and metadata. ```php return ActionResult::success(['user_id' => 123]); +return ActionResult::success( + ['processed_count' => 50], + ['execution_time_ms' => 1250] +); ``` -#### `failure(string $message, array $data = []): self` +#### `failure(string $errorMessage, array $metadata = []): static` -Creates a failed result. +Creates a failed result with an error message and optional metadata. ```php -return ActionResult::failure('Payment failed', ['error_code' => 'CARD_DECLINED']); +return ActionResult::failure('Payment failed'); +return ActionResult::failure('API rate limit exceeded', [ + 'retry_after' => 3600, + 'requests_remaining' => 0 +]); ``` -#### `retry(string $message = 'Retrying', array $data = []): self` +### Methods + +#### `isSuccess(): bool` + +Whether the action succeeded. + +#### `isFailure(): bool` + +Whether the action failed. + +#### `getErrorMessage(): ?string` + +Returns the error message for failed results, or null for successful results. + +#### `getData(): array` + +Returns the result data array. Empty array for failed results. + +#### `hasData(): bool` -Creates a result that triggers a retry. +Returns true if the result contains any data. + +#### `getMetadata(): array` + +Returns additional execution metadata. + +#### `get(string $key, mixed $default = null): mixed` + +Get a specific data value using dot notation. ```php -return ActionResult::retry('Rate limited, retrying...', ['retry_after' => 60]); +$userId = $result->get('user.id'); +$email = $result->get('user.email', 'N/A'); ``` -### Properties +#### `withMetadata(array $metadata): static` -#### `readonly bool $success` +Creates a new result with additional metadata merged. -Whether the action succeeded. +```php +$resultWithMetadata = $result->withMetadata([ + 'execution_time' => 150, + 'cache_hit' => true +]); +``` -#### `readonly string $message` +#### `withMetadataEntry(string $key, mixed $value): self` -The result message. +Creates a new result with a single additional metadata entry. -#### `readonly array $data` +#### `mergeData(array $additionalData): self` -Additional result data. +Creates a new successful result by merging data. Throws `LogicException` if called on a failed result. -## SimpleWorkflow +```php +$result1 = ActionResult::success(['user_id' => 123]); +$result2 = $result1->mergeData(['email' => 'user@example.com']); +// result2 data: ['user_id' => 123, 'email' => 'user@example.com'] +``` -Helper class for creating common workflow patterns. +#### `toArray(): array` -### Static Methods +Convert the result to an array representation for serialization. -#### `quick(): self` +```php +$array = $result->toArray(); +// ['success' => true, 'error_message' => null, 'data' => [...], 'metadata' => [...]] +``` + +## SimpleWorkflow -Creates a new simple workflow builder. +Helper class for simplified workflow creation and execution. + +### Constructor ```php -$workflow = SimpleWorkflow::quick() - ->email('welcome', to: 'user@example.com') - ->delay(days: 1) - ->email('followup', to: 'user@example.com') - ->build(); +$simple = new SimpleWorkflow($storageAdapter, $eventDispatcher); ``` -#### `sequential(array $steps): self` +### Methods + +#### `sequential(array $steps): string` -Creates a workflow with sequential steps. +Creates and executes a workflow with sequential steps. Returns the workflow instance ID. ```php -$workflow = SimpleWorkflow::sequential([ +$instanceId = $simple->sequential([ 'step1' => StepOneAction::class, 'step2' => StepTwoAction::class, 'step3' => StepThreeAction::class -])->build(); +]); +``` + +#### `runAction(string $actionClass, array $config = [], array $context = []): ActionResult` + +Executes a single action directly without creating a workflow. + +#### `executeBuilder(WorkflowBuilder $builder, array $context = []): string` + +Executes a workflow from a builder instance. + +#### `resume(string $instanceId): WorkflowInstance` + +Resumes a paused or pending workflow. + +#### `getStatus(string $instanceId): array` + +Gets the status of a workflow instance. + +#### `getEngine(): WorkflowEngine` + +Returns the underlying workflow engine instance. + +## QuickWorkflowBuilder + +Pre-built workflow patterns for common business scenarios. Access via `WorkflowBuilder::quick()`. + +### Methods + +#### `userOnboarding(string $name = 'user-onboarding'): WorkflowBuilder` + +Creates a user onboarding workflow with standard steps (welcome email, delay, profile creation, role assignment). + +#### `orderProcessing(string $name = 'order-processing'): WorkflowBuilder` + +Creates an order processing workflow (validate, charge payment, update inventory, confirmation email). + +#### `documentApproval(string $name = 'document-approval'): WorkflowBuilder` + +Creates a document approval workflow (submit, assign reviewer, review request email, review, conditional approve/reject). + +```php +$workflow = WorkflowBuilder::quick() + ->userOnboarding('premium-onboarding') + ->then(SetupPremiumFeaturesAction::class) + ->build(); ``` ## Attributes ### @WorkflowStep -Marks a method as a workflow step. +Marks a class as a workflow step with metadata. ```php use SolutionForest\WorkflowEngine\Attributes\WorkflowStep; -class MyAction implements WorkflowAction +#[WorkflowStep( + id: 'create_profile', + name: 'Create User Profile', + description: 'Creates a new user profile in the database', + config: ['template' => 'basic'], + required: true, + order: 1 +)] +class CreateUserProfileAction implements WorkflowAction { - #[WorkflowStep('my-step')] - public function execute(WorkflowContext $context): ActionResult - { - // Action logic - } + // ... } ``` ### @Timeout -Sets a timeout for an action. +Sets a timeout for an action. Accepts `seconds`, `minutes`, and/or `hours`. Provides a calculated `totalSeconds` property. ```php use SolutionForest\WorkflowEngine\Attributes\Timeout; +#[Timeout(seconds: 30)] +#[Timeout(minutes: 5)] +#[Timeout(minutes: 5, seconds: 30)] // 5 minutes 30 seconds class MyAction implements WorkflowAction { - #[Timeout(minutes: 5)] - public function execute(WorkflowContext $context): ActionResult - { - // Action logic - } + // ... } ``` @@ -303,29 +604,102 @@ Configures retry behavior for an action. ```php use SolutionForest\WorkflowEngine\Attributes\Retry; +#[Retry(attempts: 3, backoff: 'exponential')] +#[Retry(attempts: 5, backoff: 'exponential', delay: 1000, maxDelay: 30000)] class MyAction implements WorkflowAction { - #[Retry(attempts: 3, backoff: 'exponential')] - public function execute(WorkflowContext $context): ActionResult - { - // Action logic - } + // ... } ``` +Parameters: +- `attempts` (int, default 3) - Number of retry attempts +- `backoff` (`'linear'` | `'exponential'` | `'fixed'`) - Backoff strategy +- `delay` (int, default 1000) - Base delay in milliseconds +- `maxDelay` (int, default 30000) - Maximum delay cap in milliseconds + ### @Condition -Sets a condition for when an action should execute. +Sets a condition for when an action should execute. This attribute is repeatable. ```php use SolutionForest\WorkflowEngine\Attributes\Condition; -class MyAction implements WorkflowAction +#[Condition('user.email is not null')] +#[Condition('order.amount > 100')] +#[Condition('user.premium = true', operator: 'or')] +class ConditionalAction implements WorkflowAction { - #[Condition('user.age >= 18')] - public function execute(WorkflowContext $context): ActionResult - { - // Action logic - } + // ... } ``` + +Parameters: +- `expression` (string) - The condition expression +- `operator` (`'and'` | `'or'`, default `'and'`) - How to combine with other conditions + +## Events + +The workflow engine dispatches the following events: + +### WorkflowStarted + +Dispatched when a workflow begins execution. + +Properties: `workflowId`, `name`, `context` (array) + +### WorkflowCompletedEvent + +Dispatched when a workflow finishes successfully. + +Properties: `instance` (WorkflowInstance) + +### WorkflowFailedEvent + +Dispatched when a workflow fails due to errors. + +Properties: `instance` (WorkflowInstance), `exception` (Throwable) + +### WorkflowCancelled + +Dispatched when a workflow is cancelled. + +Properties: `workflowId`, `name`, `reason` (string) + +### StepCompletedEvent + +Dispatched when a step completes successfully. + +Properties: `instance`, `step`, `result` + +### StepFailedEvent + +Dispatched when a step fails. + +Properties: `instance`, `step`, `exception` (Throwable) + +## Exceptions + +### WorkflowException (abstract base) + +Base exception for all workflow exceptions. Provides `getContext()`, `getUserMessage()`, `getDebugInfo()`, `getSuggestions()`. + +### InvalidWorkflowDefinitionException + +Thrown for validation errors in workflow definitions. Factory methods: `missingRequiredField()`, `invalidStep()`, `invalidStepId()`, `invalidRetryAttempts()`, `invalidTimeout()`, `duplicateStepId()`, `invalidName()`, `invalidCondition()`, `invalidDelay()`, `emptyWorkflow()`, `actionNotFound()`, `invalidActionClass()`. + +### WorkflowInstanceNotFoundException + +Thrown when a workflow instance cannot be found. Factory methods: `notFound()`, `malformedId()`, `storageConnectionError()`. + +### InvalidWorkflowStateException + +Thrown for invalid state transitions. Factory methods: `cannotResumeCompleted()`, `cannotCancelFailed()`, `alreadyRunning()`, `fromInstanceTransition()`. + +### ActionNotFoundException + +Thrown when an action class cannot be found or doesn't implement the interface. Factory methods: `classNotFound()`, `invalidInterface()`, `actionNotFound()`, `invalidActionClass()`. + +### StepExecutionException + +Thrown when a step fails during execution. Factory methods: `actionClassNotFound()`, `invalidActionClass()`, `timeout()`, `fromException()`, `actionFailed()`. diff --git a/docs/best-practices.md b/docs/best-practices.md index 0c62dcf..1a41420 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -7,37 +7,39 @@ Each action should have a single responsibility: ```php -// ❌ Bad - Action does too many things +// Bad - Action does too many things class ProcessOrderAction implements WorkflowAction { public function execute(WorkflowContext $context): ActionResult { $order = $context->getData('order'); - + // Validate order if (!$this->validateOrder($order)) { return ActionResult::failure('Invalid order'); } - + // Process payment $payment = $this->processPayment($order); - + // Update inventory $this->updateInventory($order); - + // Send email $this->sendConfirmationEmail($order); - + return ActionResult::success(); } + + // ... } -// ✅ Good - Break into focused actions +// Good - Break into focused actions $workflow = WorkflowBuilder::create('order-processing') - ->step('validate', ValidateOrderAction::class) - ->step('payment', ProcessPaymentAction::class) - ->step('inventory', UpdateInventoryAction::class) - ->email('confirmation', to: '{{ order.customer.email }}') + ->addStep('validate', ValidateOrderAction::class) + ->addStep('payment', ProcessPaymentAction::class) + ->addStep('inventory', UpdateInventoryAction::class) + ->email('confirmation', '{{ order.customer.email }}', 'Order Confirmed') ->build(); ``` @@ -46,17 +48,17 @@ $workflow = WorkflowBuilder::create('order-processing') Choose descriptive names for workflows and steps: ```php -// ❌ Bad - Unclear names +// Bad - Unclear names $workflow = WorkflowBuilder::create('flow1') - ->step('step1', Action1::class) - ->step('step2', Action2::class) + ->addStep('step1', Action1::class) + ->addStep('step2', Action2::class) ->build(); -// ✅ Good - Clear, descriptive names +// Good - Clear, descriptive names $workflow = WorkflowBuilder::create('user-onboarding') - ->step('create-profile', CreateUserProfileAction::class) - ->step('send-welcome-email', SendWelcomeEmailAction::class) - ->step('assign-default-permissions', AssignPermissionsAction::class) + ->addStep('create-profile', CreateUserProfileAction::class) + ->addStep('send-welcome-email', SendWelcomeEmailAction::class) + ->addStep('assign-default-permissions', AssignPermissionsAction::class) ->build(); ``` @@ -71,7 +73,7 @@ class PaymentAction implements WorkflowAction { try { $payment = $this->processPayment($context->getData('order')); - + return ActionResult::success([ 'payment_id' => $payment->id, 'status' => 'completed' @@ -83,18 +85,36 @@ class PaymentAction implements WorkflowAction 'retry_possible' => true ]); } catch (PaymentProcessorException $e) { - // Temporary error - can retry - return ActionResult::retry('Payment processor unavailable'); + // Temporary error - will be retried by the engine + return ActionResult::failure('Payment processor unavailable', [ + 'error_type' => 'temporary', + 'original_error' => $e->getMessage() + ]); } catch (\Exception $e) { // Unexpected error - log and fail Log::error('Unexpected payment error', [ 'order_id' => $context->getData('order.id'), 'error' => $e->getMessage() ]); - + return ActionResult::failure('Payment processing failed'); } } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('order'); + } + + public function getName(): string + { + return 'Process Payment'; + } + + public function getDescription(): string + { + return 'Processes customer payment through configured gateway'; + } } ``` @@ -107,20 +127,11 @@ Choose the right queue connection for your workload: ```php // config/workflow-engine.php return [ - 'workflows' => [ - 'user-onboarding' => [ - 'queue' => 'high-priority', - 'connection' => 'redis' - ], - 'data-export' => [ - 'queue' => 'low-priority', - 'connection' => 'database' - ], - 'real-time-notifications' => [ - 'queue' => 'sync', // Run immediately - 'connection' => 'sync' - ] - ] + 'queue' => [ + 'enabled' => true, + 'connection' => 'redis', + 'queue_name' => 'workflows', + ], ]; ``` @@ -135,19 +146,34 @@ class BulkEmailAction implements WorkflowAction { $recipients = $context->getData('recipients'); $template = $context->getData('template'); - + // Process in batches of 100 $batches = array_chunk($recipients, 100); - + foreach ($batches as $batch) { Mail::to($batch)->queue(new BulkEmail($template)); } - + return ActionResult::success([ 'sent_count' => count($recipients), 'batch_count' => count($batches) ]); } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('recipients') && $context->hasData('template'); + } + + public function getName(): string + { + return 'Send Bulk Emails'; + } + + public function getDescription(): string + { + return 'Sends emails in batches to multiple recipients'; + } } ``` @@ -161,18 +187,33 @@ class ProcessOrderAction implements WorkflowAction public function execute(WorkflowContext $context): ActionResult { $orderId = $context->getData('order_id'); - + // Only load the order when we need it $order = Order::with(['items', 'customer'])->find($orderId); - + if (!$order) { return ActionResult::failure('Order not found'); } - + // Process the order... - + return ActionResult::success(); } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('order_id'); + } + + public function getName(): string + { + return 'Process Order'; + } + + public function getDescription(): string + { + return 'Processes an order by loading and validating it'; + } } ``` @@ -188,42 +229,40 @@ class SecureAction implements WorkflowAction public function execute(WorkflowContext $context): ActionResult { $data = $context->getData(); - + // Validate required fields $validator = Validator::make($data, [ 'user_id' => 'required|integer|exists:users,id', 'amount' => 'required|numeric|min:0', 'currency' => 'required|string|in:USD,EUR,GBP' ]); - + if ($validator->fails()) { return ActionResult::failure('Invalid input data', [ - 'errors' => $validator->errors() + 'errors' => $validator->errors()->toArray() ]); } - + // Process validated data... - + return ActionResult::success(); } -} -``` -### Sanitize Template Data + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('user_id') && $context->hasData('amount'); + } -When using templates, sanitize the data: + public function getName(): string + { + return 'Secure Data Validation'; + } -```php -$workflow = WorkflowBuilder::create('secure-email') - ->email('notification', [ - 'to' => '{{ user.email }}', - 'subject' => 'Welcome {{ user.name|escape }}', // Escape user input - 'data' => [ - 'username' => Str::limit($user->name, 50), // Limit length - 'safe_content' => strip_tags($user->bio) // Remove HTML - ] - ]) - ->build(); + public function getDescription(): string + { + return 'Validates and processes input data securely'; + } +} ``` ### Limit Workflow Access @@ -248,12 +287,15 @@ class WorkflowPolicy public function startWorkflow(Request $request, string $workflowName) { $this->authorize('start', [Workflow::class, $workflowName]); - - $workflow = WorkflowBuilder::create($workflowName) + + $definition = WorkflowBuilder::create($workflowName) // ... build workflow ->build(); - - return $workflow->start($request->validated()); + + $engine = app(WorkflowEngine::class); + $instanceId = $engine->start($workflowName, $definition->toArray(), $request->validated()); + + return response()->json(['instance_id' => $instanceId]); } ``` @@ -270,71 +312,49 @@ class LoggingAction implements WorkflowAction { Log::info('Starting action', [ 'workflow_id' => $context->workflowId, - 'step_id' => $context->currentStepId, + 'step_id' => $context->stepId, 'data_keys' => array_keys($context->getData()) ]); - + $startTime = microtime(true); - + try { $result = $this->performAction($context); - + Log::info('Action completed', [ 'workflow_id' => $context->workflowId, - 'step_id' => $context->currentStepId, + 'step_id' => $context->stepId, 'duration_ms' => round((microtime(true) - $startTime) * 1000, 2), - 'success' => $result->success + 'success' => $result->isSuccess() ]); - + return $result; } catch (\Exception $e) { Log::error('Action failed', [ 'workflow_id' => $context->workflowId, - 'step_id' => $context->currentStepId, + 'step_id' => $context->stepId, 'duration_ms' => round((microtime(true) - $startTime) * 1000, 2), 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); - + throw $e; } } -} -``` -### Use Meaningful Metrics + public function canExecute(WorkflowContext $context): bool + { + return true; + } -Track business metrics, not just technical ones: + public function getName(): string + { + return 'Logging Action'; + } -```php -class MetricsCollectingAction implements WorkflowAction -{ - public function execute(WorkflowContext $context): ActionResult + public function getDescription(): string { - $order = $context->getData('order'); - - // Track business metrics - Metrics::increment('orders.processed', 1, [ - 'workflow' => $context->getWorkflowName(), - 'order_type' => $order['type'], - 'customer_tier' => $order['customer']['tier'] - ]); - - Metrics::histogram('order.value', $order['total'], [ - 'currency' => $order['currency'] - ]); - - $result = $this->processOrder($order); - - if ($result->success) { - Metrics::increment('orders.successful'); - } else { - Metrics::increment('orders.failed', 1, [ - 'failure_reason' => $result->message - ]); - } - - return $result; + return 'Wraps action execution with comprehensive logging'; } } ``` @@ -350,23 +370,16 @@ use SolutionForest\WorkflowEngine\Events\WorkflowFailedEvent; protected $listen = [ WorkflowFailedEvent::class => [ function (WorkflowFailedEvent $event) { + $instance = $event->instance; + $state = $instance->getState(); + // Alert if critical workflow fails - if (in_array($event->workflowName, ['payment-processing', 'order-fulfillment'])) { - Alert::critical("Critical workflow failed: {$event->workflowName}", [ - 'workflow_id' => $event->workflowId, - 'error' => $event->error, - 'step' => $event->failedStep + if ($state->isError()) { + Alert::critical("Workflow failed: {$instance->getName()}", [ + 'workflow_id' => $instance->getId(), + 'error' => $event->exception->getMessage(), ]); } - - // Alert if too many workflows are failing - $recentFailures = WorkflowExecution::where('state', 'failed') - ->where('created_at', '>', now()->subMinutes(15)) - ->count(); - - if ($recentFailures > 10) { - Alert::warning("High workflow failure rate: {$recentFailures} failures in 15 minutes"); - } } ] ]; @@ -374,44 +387,27 @@ protected $listen = [ ## Testing Strategies -### Use Factories for Test Data +### Use WorkflowContext in Tests -Create consistent test data: +Create consistent test contexts: ```php -// database/factories/WorkflowContextFactory.php -class WorkflowContextFactory extends Factory -{ - public function definition() - { - return [ - 'workflow_id' => $this->faker->uuid, - 'current_step_id' => 'test-step', - 'data' => [ - 'user' => User::factory()->make()->toArray(), - 'order' => Order::factory()->make()->toArray() - ] - ]; - } - - public function withOrder(Order $order) - { - return $this->state(['data' => ['order' => $order->toArray()]]); - } -} - -// In your tests class WorkflowTest extends TestCase { - public function test_order_processing_workflow() + public function test_order_processing_action() { $order = Order::factory()->create(['status' => 'pending']); - $context = WorkflowContext::factory()->withOrder($order)->create(); - + + $context = new WorkflowContext( + workflowId: 'test-workflow-1', + stepId: 'process-order', + data: ['order' => $order->toArray()] + ); + $action = new ProcessOrderAction(); $result = $action->execute($context); - - $this->assertTrue($result->success); + + $this->assertTrue($result->isSuccess()); } } ``` @@ -430,13 +426,18 @@ class ExternalApiTest extends TestCase 'api.payment.com/*' => Http::response(['status' => 'success'], 200), 'api.shipping.com/*' => Http::response(['tracking' => '123'], 200) ]); - - $context = WorkflowContext::factory()->create(); + + $context = new WorkflowContext( + workflowId: 'test-1', + stepId: 'api-call', + data: ['order_id' => 123] + ); + $action = new ExternalApiAction(); $result = $action->execute($context); - - $this->assertTrue($result->success); - + + $this->assertTrue($result->isSuccess()); + // Verify the right calls were made Http::assertSent(function ($request) { return str_contains($request->url(), 'api.payment.com'); @@ -458,27 +459,18 @@ class ErrorHandlingTest extends TestCase Http::fake([ 'api.payment.com/*' => Http::response(['error' => 'Card declined'], 402) ]); - - $context = WorkflowContext::factory()->create(); + + $context = new WorkflowContext( + workflowId: 'test-1', + stepId: 'payment', + data: ['payment' => ['amount' => 100]] + ); + $action = new ProcessPaymentAction(); $result = $action->execute($context); - - $this->assertFalse($result->success); - $this->assertEquals('Payment failed', $result->message); - $this->assertArrayHasKey('retry_possible', $result->data); - } - - public function test_network_timeout_handling() - { - Http::fake(function () { - throw new ConnectException('Connection timeout', new Request('GET', 'test')); - }); - - $context = WorkflowContext::factory()->create(); - $action = new ExternalApiAction(); - $result = $action->execute($context); - - $this->assertEquals('retry', $result->status); + + $this->assertTrue($result->isFailure()); + $this->assertNotNull($result->getErrorMessage()); } } ``` @@ -492,56 +484,30 @@ Always document what your workflow does: ```php /** * E-commerce Order Processing Workflow - * + * * This workflow handles the complete order processing lifecycle: * 1. Validates the order data and inventory * 2. Processes payment using the configured payment gateway * 3. Updates inventory levels * 4. Creates shipping label and arranges pickup * 5. Sends confirmation emails to customer - * 6. Schedules follow-up communications - * + * * Error handling: * - Payment failures trigger retry logic (3 attempts) - * - Inventory shortages cancel the order and notify customer - * - Shipping failures are escalated to operations team - * + * - Inventory shortages fail the workflow + * - Shipping failures are logged for manual review + * * @param array $data Must contain: order, customer, payment_method - * @return WorkflowInstance */ -function createOrderProcessingWorkflow(array $data): WorkflowDefinition +function createOrderProcessingWorkflow(): WorkflowDefinition { return WorkflowBuilder::create('order-processing') - ->step('validate-order', ValidateOrderAction::class) - ->step('process-payment', ProcessPaymentAction::class) - ->retry(attempts: 3, backoff: 'exponential') - ->step('update-inventory', UpdateInventoryAction::class) - ->step('create-shipment', CreateShipmentAction::class) - ->email('order-confirmation', to: '{{ customer.email }}') + ->description('E-commerce order processing workflow') + ->addStep('validate-order', ValidateOrderAction::class) + ->addStep('process-payment', ProcessPaymentAction::class, [], '2m', 3) + ->addStep('update-inventory', UpdateInventoryAction::class) + ->addStep('create-shipment', CreateShipmentAction::class) + ->email('order-confirmation', '{{ customer.email }}', 'Order Confirmed') ->build(); } ``` - -### Maintain Change Logs - -Keep track of workflow changes: - -```php -/** - * Order Processing Workflow - Change Log - * - * v2.1.0 (2024-01-15) - * - Added retry logic for payment processing - * - Improved error handling for inventory shortages - * - Added customer notification for shipping delays - * - * v2.0.0 (2024-01-01) - * - Migrated to new fluent API - * - Added parallel processing for notifications - * - Breaking: Changed context data structure - * - * v1.5.0 (2023-12-01) - * - Added support for international shipping - * - Improved payment gateway integration - */ -``` diff --git a/docs/getting-started.md b/docs/getting-started.md index e023443..13f504d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,7 +5,7 @@ ### Requirements - PHP 8.3 or higher -- Laravel 10.0 or higher +- Laravel 11.0 or higher - Composer ### Install the Package @@ -23,6 +23,7 @@ php artisan vendor:publish --tag="workflow-engine-config" ### Run Migrations ```bash +php artisan vendor:publish --tag="workflow-engine-migrations" php artisan migrate ``` @@ -34,11 +35,11 @@ A workflow is a series of steps that process data. Think of it as a recipe that ### Actions -Actions are the individual steps in your workflow. Each action performs a specific task. +Actions are the individual steps in your workflow. Each action performs a specific task and implements the `WorkflowAction` interface. ### Context -Context holds the data that flows through your workflow. It's immutable and type-safe. +Context holds the data that flows through your workflow. It's immutable and type-safe via the `WorkflowContext` readonly class. ## Your First Workflow @@ -48,27 +49,28 @@ Let's create a simple user registration workflow: description('New user registration process') ->addStep('create-profile', CreateUserProfileAction::class) ->email('welcome-email', '{{ user.email }}', 'Welcome!') ->delay(hours: 24) ->email('tips-email', '{{ user.email }}', 'Getting Started Tips') ->build(); -// Start the workflow -$instance = $registrationWorkflow->start([ - 'user' => $user, - 'registration_data' => $registrationData +// Start the workflow via the engine +$engine = app(WorkflowEngine::class); +$instanceId = $engine->start('user-reg-001', $definition->toArray(), [ + 'user' => ['id' => 1, 'email' => 'user@example.com', 'name' => 'John'], ]); ``` ## Creating Actions -Actions are simple PHP classes that implement the `WorkflowAction` interface: +Actions are PHP classes that implement the `WorkflowAction` interface, which requires four methods: ```php getData('user'); - + // Create user profile logic here $profile = UserProfile::create([ 'user_id' => $userData['id'], 'name' => $userData['name'], 'email' => $userData['email'], ]); - + return ActionResult::success([ 'profile_id' => $profile->id ]); } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('user.id'); + } + + public function getName(): string + { + return 'Create User Profile'; + } + + public function getDescription(): string + { + return 'Creates a new user profile in the database'; + } } ``` @@ -129,6 +146,21 @@ class CreateUserProfileAction implements WorkflowAction // Same implementation as above // Now with automatic timeout and retry handling } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('user.id'); + } + + public function getName(): string + { + return 'Create User Profile'; + } + + public function getDescription(): string + { + return 'Creates a new user profile in the database'; + } } ``` @@ -143,22 +175,49 @@ use SolutionForest\WorkflowEngine\Core\WorkflowState; $state = $instance->getState(); -echo $state->value; // 'running', 'completed', 'failed', etc. -echo $state->label(); // 'In Progress', 'Completed', 'Failed', etc. -echo $state->color(); // 'blue', 'green', 'red', etc. -echo $state->icon(); // '▶️', '✅', '❌', etc. +echo $state->value; // 'running', 'completed', 'failed', etc. +echo $state->label(); // 'Running', 'Completed', 'Failed', etc. +echo $state->color(); // 'blue', 'green', 'red', etc. +echo $state->icon(); // '▶️', '✅', '❌', etc. +echo $state->description(); // Detailed description of the state + +// Check state categories +$state->isActive(); // true for PENDING, RUNNING, WAITING, PAUSED +$state->isFinished(); // true for COMPLETED, FAILED, CANCELLED +$state->isSuccessful(); // true only for COMPLETED +$state->isError(); // true only for FAILED + +// Valid state transitions +$state->canTransitionTo(WorkflowState::COMPLETED); // Check transition validity +$state->getValidTransitions(); // Get all valid target states ``` ## Error Handling -The workflow engine automatically handles errors and provides retry mechanisms: +The workflow engine provides comprehensive error handling: ```php $workflow = WorkflowBuilder::create('robust-workflow') ->addStep('risky-operation', RiskyAction::class, [], 30, 3) // timeout: 30s, retry: 3 attempts + ->addStep('slow-task', SlowAction::class, timeout: '5m') // timeout as string format ->build(); ``` +## Using the Facade and Helpers + +```php +use SolutionForest\WorkflowEngine\Laravel\Facades\WorkflowEngine; + +// Via facade +$instanceId = WorkflowEngine::start('my-workflow', $definition, $context); +$instance = WorkflowEngine::getInstance($instanceId); +$instance = WorkflowEngine::cancel($instanceId, 'No longer needed'); + +// Via helper function +$engine = workflow(); +$instanceId = $engine->start('my-workflow', $definition, $context); +``` + ## Next Steps - Learn about [Advanced Features](advanced-features.md) diff --git a/docs/migration.md b/docs/migration.md index cad2a50..41e1490 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -28,6 +28,7 @@ php artisan vendor:publish --tag="workflow-engine-config" --force #### 3. Run New Migrations ```bash +php artisan vendor:publish --tag="workflow-engine-migrations" php artisan migrate ``` @@ -62,16 +63,16 @@ $instance = WorkflowMastery::start($orderWorkflow, $data); ```php use SolutionForest\WorkflowEngine\Core\WorkflowBuilder; +use SolutionForest\WorkflowEngine\Core\WorkflowEngine; -$workflow = WorkflowBuilder::create('order-processing') - ->step('validate', ValidateOrderAction::class) - ->step('payment', ProcessPaymentAction::class) - ->retry(attempts: 3) - ->onFailure(RefundAction::class) - ->step('shipping', CreateShipmentAction::class) +$definition = WorkflowBuilder::create('order-processing') + ->addStep('validate', ValidateOrderAction::class) + ->addStep('payment', ProcessPaymentAction::class, [], null, 3) // 3 retry attempts + ->addStep('shipping', CreateShipmentAction::class) ->build(); -$instance = $workflow->start($data); +$engine = app(WorkflowEngine::class); +$instanceId = $engine->start('order-001', $definition->toArray(), $data); ``` #### 5. Update Action Classes @@ -84,10 +85,10 @@ class ProcessPaymentAction public function execute($context) { $orderData = $context['order'] ?? []; - + // Process payment $result = $this->chargeCard($orderData); - + return [ 'success' => $result['success'], 'data' => $result['data'] ?? [], @@ -107,15 +108,32 @@ class ProcessPaymentAction implements WorkflowAction public function execute(WorkflowContext $context): ActionResult { $order = $context->getData('order'); - + // Process payment $result = $this->chargeCard($order); - + if ($result['success']) { return ActionResult::success($result['data']); } - - return ActionResult::failure('Payment failed', $result['error']); + + return ActionResult::failure('Payment failed', [ + 'error' => $result['error'] ?? 'Unknown error' + ]); + } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('order'); + } + + public function getName(): string + { + return 'Process Payment'; + } + + public function getDescription(): string + { + return 'Processes customer payment through configured gateway'; } } ``` @@ -141,18 +159,25 @@ $orderTotal = $context['order']['total']; ```php // Context is now a readonly class -$context = new WorkflowContext([ - 'user' => $user, - 'order' => $order -], $workflowId, $stepId); - -// Use getter methods +$context = new WorkflowContext( + workflowId: $workflowId, + stepId: $stepId, + data: [ + 'user' => $user, + 'order' => $order + ] +); + +// Use getter methods with dot notation $user = $context->getData('user'); $userId = $context->getData('user.id'); $orderTotal = $context->getData('order.total'); // Create new context with additional data (immutable) -$newContext = $context->with(['payment_result' => $paymentData]); +$newContext = $context->with('payment_result', $paymentData); + +// Or merge multiple values +$newContext = $context->withData(['payment_result' => $paymentData, 'status' => 'paid']); ``` #### 7. Update Error Handling @@ -177,16 +202,20 @@ try { ```php try { $result = $action->execute($context); - - match($result->status) { - 'success' => $this->proceedToNextStep($result), - 'failure' => $this->handleFailure($result), - 'retry' => $this->scheduleRetry($result), - }; + + if ($result->isSuccess()) { + $this->proceedToNextStep($result); + } else { + $this->handleFailure($result); + Log::warning('Action failed', [ + 'error' => $result->getErrorMessage(), + 'metadata' => $result->getMetadata() + ]); + } } catch (\Exception $e) { Log::error('Workflow failed', [ 'workflow_id' => $context->workflowId, - 'step_id' => $context->currentStepId, + 'step_id' => $context->stepId, 'error' => $e->getMessage() ]); } @@ -200,52 +229,81 @@ try { use SolutionForest\WorkflowEngine\Core\WorkflowState; // Rich enum with methods -$state = WorkflowState::Running; -echo $state->label(); // "In Progress" -echo $state->color(); // "blue" -echo $state->icon(); // "▶️" +$state = WorkflowState::RUNNING; +echo $state->label(); // "Running" +echo $state->color(); // "blue" +echo $state->icon(); // "▶️" +echo $state->description(); // Detailed description + +// State category checks +$state->isActive(); // true for PENDING, RUNNING, WAITING, PAUSED +$state->isFinished(); // true for COMPLETED, FAILED, CANCELLED +$state->isSuccessful(); // true only for COMPLETED +$state->isError(); // true only for FAILED // Smart state transitions -if ($state->canTransitionTo(WorkflowState::Completed)) { +if ($state->canTransitionTo(WorkflowState::COMPLETED)) { $workflow->complete(); } + +$validTargets = $state->getValidTransitions(); // Array of valid target states ``` #### 2. Attribute-Based Configuration ```php -use SolutionForest\WorkflowEngine\Attributes\{WorkflowStep, Timeout, Retry}; - +use SolutionForest\WorkflowEngine\Attributes\{WorkflowStep, Timeout, Retry, Condition}; + +#[WorkflowStep( + id: 'process-payment', + name: 'Process Payment', + description: 'Processes customer payment' +)] +#[Timeout(minutes: 5)] +#[Retry(attempts: 3, backoff: 'exponential')] +#[Condition('order.amount > 0')] class ProcessPaymentAction implements WorkflowAction { - #[WorkflowStep('process-payment')] - #[Timeout(minutes: 5)] - #[Retry(attempts: 3, backoff: 'exponential')] public function execute(WorkflowContext $context): ActionResult { // Action implementation } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('order'); + } + + public function getName(): string + { + return 'Process Payment'; + } + + public function getDescription(): string + { + return 'Processes customer payment'; + } } ``` -#### 3. Simple Workflow Helpers +#### 3. Quick Workflow Templates ```php -use SolutionForest\WorkflowEngine\Support\SimpleWorkflow; +use SolutionForest\WorkflowEngine\Core\WorkflowBuilder; -// Quick common workflows -$onboarding = SimpleWorkflow::quick() - ->email('welcome', to: 'user@example.com') - ->delay(days: 1) - ->email('tips', to: 'user@example.com') +// Pre-built workflow templates +$onboarding = WorkflowBuilder::quick() + ->userOnboarding('premium-onboarding') + ->then(SetupPremiumFeaturesAction::class) ->build(); -// Sequential workflows -$approval = SimpleWorkflow::sequential([ - 'submit' => SubmitAction::class, - 'review' => ReviewAction::class, - 'approve' => ApproveAction::class -])->build(); +$orderFlow = WorkflowBuilder::quick() + ->orderProcessing('express-order') + ->build(); + +$approval = WorkflowBuilder::quick() + ->documentApproval('legal-review') + ->build(); ``` #### 4. Smart Actions @@ -253,58 +311,20 @@ $approval = SimpleWorkflow::sequential([ ```php // HTTP actions with template processing $workflow = WorkflowBuilder::create('api-integration') - ->http('POST', 'https://api.example.com/webhook', [ + ->http('https://api.example.com/webhook', 'POST', [ 'user_id' => '{{ user.id }}', 'event' => 'user_registered', - 'timestamp' => '{{ now }}' ]) ->build(); // Conditional actions $workflow = WorkflowBuilder::create('conditional-flow') ->when('user.age >= 18', fn($builder) => - $builder->step('adult-verification', AdultVerificationAction::class) + $builder->addStep('adult-verification', AdultVerificationAction::class) ) ->build(); ``` -### Backward Compatibility - -The package maintains backward compatibility for v1.x workflows during the transition period: - -```php -// V1.x workflows still work (deprecated) -$legacyWorkflow = [ - 'name' => 'legacy-workflow', - 'steps' => [/* ... */] -]; - -// But you'll see deprecation warnings -$instance = WorkflowEngine::startLegacy($legacyWorkflow, $data); -``` - -### Migration Helper Command - -Use the built-in migration command to convert existing workflows: - -```bash -# Convert a single workflow file -php artisan workflow:migrate app/Workflows/OrderProcessing.php - -# Convert all workflows in a directory -php artisan workflow:migrate app/Workflows/ --recursive - -# Dry run to see what would change -php artisan workflow:migrate app/Workflows/ --dry-run -``` - -### Testing Your Migration - -1. **Run Your Test Suite**: Ensure all existing tests pass -2. **Check Logs**: Look for deprecation warnings -3. **Monitor Performance**: New features should improve performance -4. **Validate Functionality**: Verify workflows behave identically - ### Common Migration Issues #### Issue: Missing Type Declarations @@ -315,14 +335,18 @@ TypeError: Argument 1 passed to WorkflowContext::__construct() must be of type a ``` **Solution:** -Ensure you're passing arrays to the WorkflowContext constructor: +Use named parameters with the WorkflowContext constructor: ```php // Wrong $context = new WorkflowContext($user, $workflowId, $stepId); // Correct -$context = new WorkflowContext(['user' => $user], $workflowId, $stepId); +$context = new WorkflowContext( + workflowId: $workflowId, + stepId: $stepId, + data: ['user' => $user] +); ``` #### Issue: Property Access on Readonly Classes @@ -339,8 +363,11 @@ Use immutable methods instead of direct property modification: // Wrong $context->data['new_key'] = $value; -// Correct -$context = $context->withData('new_key', $value); +// Correct - single value +$context = $context->with('new_key', $value); + +// Correct - multiple values +$context = $context->withData(['new_key' => $value, 'other' => $other]); ``` #### Issue: Namespace Changes @@ -361,6 +388,30 @@ use SolutionForest\WorkflowMastery\Contracts\WorkflowAction; use SolutionForest\WorkflowEngine\Contracts\WorkflowAction; ``` +#### Issue: ActionResult API Changes + +**Error:** +``` +Call to undefined method ActionResult::retry() +``` + +**Solution:** +Use the new ActionResult API: + +```php +// Old API +return ActionResult::retry('Rate limited'); +$result->success; // property access +$result->message; // property access + +// New API +return ActionResult::failure('Rate limited', ['retry_after' => 60]); +$result->isSuccess(); // method call +$result->getErrorMessage(); // method call +$result->getData(); // method call +$result->getMetadata(); // method call +``` + ### Performance Improvements The new version includes several performance optimizations: @@ -374,7 +425,6 @@ The new version includes several performance optimizations: If you encounter issues during migration: -1. Check the [troubleshooting guide](troubleshooting.md) -2. Review the [examples](../src/Examples/ModernWorkflowExamples.php) +1. Review the [API Reference](api-reference.md) +2. Check the [Advanced Features](advanced-features.md) guide 3. Open an issue on [GitHub](https://github.com/solutionforest/workflow-engine-laravel/issues) -4. Join our [Discord community](https://discord.gg/workflow-engine)