From 6641a2b7eed57567d6dcfa5e0b1a80bbfe8f4da9 Mon Sep 17 00:00:00 2001 From: alan Date: Thu, 29 May 2025 01:53:46 +0800 Subject: [PATCH 1/6] wip: restructure --- composer.json | 34 +- config/workflow-mastery.php | 67 --- packages/workflow-engine-core/README.md | 65 +++ packages/workflow-engine-core/composer.json | 49 ++ .../workflow-engine-core/phpstan.neon.dist | 12 + .../workflow-engine-core/phpunit.xml.dist | 31 ++ .../src}/Actions/BaseAction.php | 10 +- .../src}/Actions/ConditionAction.php | 10 +- .../src}/Actions/DelayAction.php | 6 +- .../src}/Actions/HttpAction.php | 12 +- .../src}/Actions/LogAction.php | 6 +- .../src}/Attributes/Condition.php | 2 +- .../src}/Attributes/Retry.php | 2 +- .../src}/Attributes/Timeout.php | 2 +- .../src}/Attributes/WorkflowStep.php | 2 +- .../src}/Contracts/StorageAdapter.php | 4 +- .../src}/Contracts/WorkflowAction.php | 8 +- .../src}/Core/ActionResolver.php | 10 +- .../src}/Core/ActionResult.php | 2 +- .../src}/Core/DefinitionParser.php | 4 +- .../src}/Core/Executor.php | 16 +- .../src}/Core/StateManager.php | 6 +- .../workflow-engine-core/src}/Core/Step.php | 2 +- .../src}/Core/WorkflowBuilder.php | 6 +- .../src}/Core/WorkflowContext.php | 2 +- .../src}/Core/WorkflowDefinition.php | 4 +- .../src}/Core/WorkflowEngine.php | 14 +- .../src}/Core/WorkflowInstance.php | 2 +- .../src}/Core/WorkflowState.php | 2 +- .../src/Events/WorkflowCancelled.php | 14 + .../src/Events/WorkflowCompletedEvent.php | 16 + .../src/Events/WorkflowFailedEvent.php | 19 + .../src/Events/WorkflowStarted.php | 12 + .../Exceptions/ActionNotFoundException.php | 6 +- .../InvalidWorkflowDefinitionException.php | 2 +- .../InvalidWorkflowStateException.php | 6 +- .../Exceptions/StepExecutionException.php | 6 +- .../src}/Exceptions/WorkflowException.php | 6 +- .../WorkflowInstanceNotFoundException.php | 2 +- .../src}/Support/SimpleWorkflow.php | 8 +- .../src}/Support/Uuid.php | 2 +- .../workflow-engine-core/src}/helpers.php | 6 +- .../ECommerce/CreateShipmentAction.php | 48 ++ .../Actions/ECommerce/FraudCheckAction.php | 55 +++ .../NotificationAndCompensationActions.php | 123 +++++ .../ECommerce/ProcessPaymentAction.php | 54 +++ .../ECommerce/ReserveInventoryAction.php | 42 ++ .../Actions/ECommerce/ValidateOrderAction.php | 43 ++ .../workflow-engine-core/tests/ArchTest.php | 5 + .../tests/ExampleTest.php | 5 + .../Integration/WorkflowIntegrationTest.php | 255 +++++++++++ packages/workflow-engine-core/tests/Pest.php | 5 + .../RealWorld/CICDPipelineWorkflowTest.php | 427 ++++++++++++++++++ .../DocumentApprovalWorkflowTest.php | 373 +++++++++++++++ .../tests/RealWorld/ECommerceWorkflowTest.php | 328 ++++++++++++++ .../tests/Support/InMemoryStorage.php | 65 +++ .../workflow-engine-core/tests/TestCase.php | 60 +++ .../tests/Unit/ActionTest.php | 52 +++ .../tests/Unit/HelpersTest.php | 68 +++ .../tests/Unit/PHP83FeaturesTest.php | 158 +++++++ .../tests/Unit/WorkflowEngineTest.php | 207 +++++++++ phpstan-baseline.neon | 18 - src/Commands/LaravelWorkflowEngineCommand.php | 6 +- src/Events/StepCompletedEvent.php | 6 +- src/Events/StepFailedEvent.php | 6 +- src/Events/WorkflowCancelled.php | 2 +- src/Events/WorkflowCompleted.php | 2 +- src/Events/WorkflowCompletedEvent.php | 4 +- src/Events/WorkflowFailed.php | 2 +- src/Events/WorkflowFailedEvent.php | 4 +- src/Events/WorkflowStarted.php | 2 +- src/Events/WorkflowStartedEvent.php | 4 +- src/Examples/ModernWorkflowExamples.php | 346 -------------- src/Facades/LaravelWorkflowEngine.php | 16 - src/Facades/WorkflowMastery.php | 10 +- src/LaravelWorkflowEngine.php | 67 --- src/Models/WorkflowInstance.php | 94 ++++ .../WorkflowEngineServiceProvider.php} | 20 +- src/Storage/DatabaseStorage.php | 8 +- tests/Integration/PackageIntegrationTest.php | 6 + tests/Pest.php | 2 +- tests/TestCase.php | 8 +- 82 files changed, 2842 insertions(+), 661 deletions(-) delete mode 100644 config/workflow-mastery.php create mode 100644 packages/workflow-engine-core/README.md create mode 100644 packages/workflow-engine-core/composer.json create mode 100644 packages/workflow-engine-core/phpstan.neon.dist create mode 100644 packages/workflow-engine-core/phpunit.xml.dist rename {src => packages/workflow-engine-core/src}/Actions/BaseAction.php (97%) rename {src => packages/workflow-engine-core/src}/Actions/ConditionAction.php (92%) rename {src => packages/workflow-engine-core/src}/Actions/DelayAction.php (88%) rename {src => packages/workflow-engine-core/src}/Actions/HttpAction.php (91%) rename {src => packages/workflow-engine-core/src}/Actions/LogAction.php (91%) rename {src => packages/workflow-engine-core/src}/Attributes/Condition.php (89%) rename {src => packages/workflow-engine-core/src}/Attributes/Retry.php (91%) rename {src => packages/workflow-engine-core/src}/Attributes/Timeout.php (90%) rename {src => packages/workflow-engine-core/src}/Attributes/WorkflowStep.php (91%) rename {src => packages/workflow-engine-core/src}/Contracts/StorageAdapter.php (86%) rename {src => packages/workflow-engine-core/src}/Contracts/WorkflowAction.php (95%) rename {src => packages/workflow-engine-core/src}/Core/ActionResolver.php (97%) rename {src => packages/workflow-engine-core/src}/Core/ActionResult.php (99%) rename {src => packages/workflow-engine-core/src}/Core/DefinitionParser.php (99%) rename {src => packages/workflow-engine-core/src}/Core/Executor.php (96%) rename {src => packages/workflow-engine-core/src}/Core/StateManager.php (98%) rename {src => packages/workflow-engine-core/src}/Core/Step.php (99%) rename {src => packages/workflow-engine-core/src}/Core/WorkflowBuilder.php (99%) rename {src => packages/workflow-engine-core/src}/Core/WorkflowContext.php (99%) rename {src => packages/workflow-engine-core/src}/Core/WorkflowDefinition.php (98%) rename {src => packages/workflow-engine-core/src}/Core/WorkflowEngine.php (95%) rename {src => packages/workflow-engine-core/src}/Core/WorkflowInstance.php (99%) rename {src => packages/workflow-engine-core/src}/Core/WorkflowState.php (99%) create mode 100644 packages/workflow-engine-core/src/Events/WorkflowCancelled.php create mode 100644 packages/workflow-engine-core/src/Events/WorkflowCompletedEvent.php create mode 100644 packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php create mode 100644 packages/workflow-engine-core/src/Events/WorkflowStarted.php rename {src => packages/workflow-engine-core/src}/Exceptions/ActionNotFoundException.php (97%) rename {src => packages/workflow-engine-core/src}/Exceptions/InvalidWorkflowDefinitionException.php (99%) rename {src => packages/workflow-engine-core/src}/Exceptions/InvalidWorkflowStateException.php (97%) rename {src => packages/workflow-engine-core/src}/Exceptions/StepExecutionException.php (98%) rename {src => packages/workflow-engine-core/src}/Exceptions/WorkflowException.php (96%) rename {src => packages/workflow-engine-core/src}/Exceptions/WorkflowInstanceNotFoundException.php (99%) rename {src => packages/workflow-engine-core/src}/Support/SimpleWorkflow.php (96%) rename {src => packages/workflow-engine-core/src}/Support/Uuid.php (98%) rename {src => packages/workflow-engine-core/src}/helpers.php (89%) create mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/CreateShipmentAction.php create mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/FraudCheckAction.php create mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/NotificationAndCompensationActions.php create mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/ProcessPaymentAction.php create mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/ReserveInventoryAction.php create mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/ValidateOrderAction.php create mode 100644 packages/workflow-engine-core/tests/ArchTest.php create mode 100644 packages/workflow-engine-core/tests/ExampleTest.php create mode 100644 packages/workflow-engine-core/tests/Integration/WorkflowIntegrationTest.php create mode 100644 packages/workflow-engine-core/tests/Pest.php create mode 100644 packages/workflow-engine-core/tests/RealWorld/CICDPipelineWorkflowTest.php create mode 100644 packages/workflow-engine-core/tests/RealWorld/DocumentApprovalWorkflowTest.php create mode 100644 packages/workflow-engine-core/tests/RealWorld/ECommerceWorkflowTest.php create mode 100644 packages/workflow-engine-core/tests/Support/InMemoryStorage.php create mode 100644 packages/workflow-engine-core/tests/TestCase.php create mode 100644 packages/workflow-engine-core/tests/Unit/ActionTest.php create mode 100644 packages/workflow-engine-core/tests/Unit/HelpersTest.php create mode 100644 packages/workflow-engine-core/tests/Unit/PHP83FeaturesTest.php create mode 100644 packages/workflow-engine-core/tests/Unit/WorkflowEngineTest.php delete mode 100644 src/Examples/ModernWorkflowExamples.php delete mode 100644 src/Facades/LaravelWorkflowEngine.php delete mode 100755 src/LaravelWorkflowEngine.php create mode 100644 src/Models/WorkflowInstance.php rename src/{LaravelWorkflowEngineServiceProvider.php => Providers/WorkflowEngineServiceProvider.php} (71%) create mode 100644 tests/Integration/PackageIntegrationTest.php diff --git a/composer.json b/composer.json index 9306fe1..974939b 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,18 @@ { - "name": "solution-forest/workflow-mastery", - "description": "A powerful, framework-agnostic workflow engine for PHP with Laravel integration - enabling complex business process automation with state management, parallel execution, and extensible action system.", + "name": "solution-forest/workflow-engine-laravel", + "description": "Laravel integration for the Workflow Engine - providing Eloquent models, service providers, and artisan commands for seamless workflow management", "keywords": [ "solutionforest", "laravel", - "workflow-mastery", "workflow-engine", + "workflow-laravel", "business-process", "automation", "orchestration", - "state-machine" + "state-machine", + "laravel-package" ], - "homepage": "https://github.com/solution-forest/workflow-mastery", + "homepage": "https://github.com/solution-forest/workflow-engine-laravel", "license": "MIT", "authors": [ { @@ -21,7 +22,8 @@ } ], "require": { - "php": "^8.3", + "php": "^8.1", + "solution-forest/workflow-engine-core": "*", "spatie/laravel-package-tools": "^1.16", "illuminate/contracts": "^10.0||^11.0||^12.0", "illuminate/support": "^10.0||^11.0||^12.0", @@ -46,19 +48,21 @@ }, "autoload": { "psr-4": { - "SolutionForest\\WorkflowMastery\\": "src/", - "SolutionForest\\WorkflowMastery\\Database\\Factories\\": "database/factories/" - }, - "files": [ - "src/helpers.php" - ] + "SolutionForest\\WorkflowEngine\\Laravel\\": "src/" + } }, "autoload-dev": { "psr-4": { - "SolutionForest\\WorkflowMastery\\Tests\\": "tests/", + "SolutionForest\\WorkflowEngine\\Laravel\\Tests\\": "tests/", "Workbench\\App\\": "workbench/app/" } }, + "repositories": [ + { + "type": "path", + "url": "./packages/workflow-engine-core" + } + ], "scripts": { "post-autoload-dump": "@composer run prepare", "prepare": "@php vendor/bin/testbench package:discover --ansi", @@ -77,10 +81,10 @@ "extra": { "laravel": { "providers": [ - "SolutionForest\\WorkflowMastery\\LaravelWorkflowEngineServiceProvider" + "SolutionForest\\WorkflowEngine\\Laravel\\Providers\\WorkflowEngineServiceProvider" ], "aliases": { - "WorkflowMastery": "SolutionForest\\WorkflowMastery\\Facades\\WorkflowMastery" + "WorkflowEngine": "SolutionForest\\WorkflowEngine\\Laravel\\Facades\\WorkflowEngine" } } }, diff --git a/config/workflow-mastery.php b/config/workflow-mastery.php deleted file mode 100644 index 4839a27..0000000 --- a/config/workflow-mastery.php +++ /dev/null @@ -1,67 +0,0 @@ - [ - 'driver' => env('WORKFLOW_MASTERY_STORAGE_DRIVER', 'database'), - - 'database' => [ - 'connection' => env('WORKFLOW_MASTERY_DB_CONNECTION', config('database.default')), - 'table' => env('WORKFLOW_MASTERY_DB_TABLE', 'workflow_instances'), - ], - - 'file' => [ - 'path' => env('WORKFLOW_MASTERY_FILE_PATH', storage_path('app/workflows')), - ], - ], - - /* - |-------------------------------------------------------------------------- - | Event Configuration - |-------------------------------------------------------------------------- - | - | Configure event dispatching for workflow lifecycle events - | - */ - 'events' => [ - 'enabled' => env('WORKFLOW_MASTERY_EVENTS_ENABLED', true), - ], - - /* - |-------------------------------------------------------------------------- - | Action Configuration - |-------------------------------------------------------------------------- - | - | Configure default action settings - | - */ - 'actions' => [ - 'timeout' => env('WORKFLOW_MASTERY_ACTION_TIMEOUT', '5m'), - 'retry_attempts' => env('WORKFLOW_MASTERY_ACTION_RETRY_ATTEMPTS', 3), - 'retry_delay' => env('WORKFLOW_MASTERY_ACTION_RETRY_DELAY', '30s'), - ], - - /* - |-------------------------------------------------------------------------- - | Queue Configuration - |-------------------------------------------------------------------------- - | - | Configure queue settings for asynchronous workflow execution - | - */ - 'queue' => [ - 'enabled' => env('WORKFLOW_MASTERY_QUEUE_ENABLED', false), - 'connection' => env('WORKFLOW_MASTERY_QUEUE_CONNECTION', config('queue.default')), - 'queue_name' => env('WORKFLOW_MASTERY_QUEUE_NAME', 'workflows'), - ], -]; diff --git a/packages/workflow-engine-core/README.md b/packages/workflow-engine-core/README.md new file mode 100644 index 0000000..34dad22 --- /dev/null +++ b/packages/workflow-engine-core/README.md @@ -0,0 +1,65 @@ +# Workflow Engine Core + +A framework-agnostic workflow engine for PHP applications. This is the core library that provides workflow definition, execution, and state management without any framework dependencies. + +## Features + +- **Framework Agnostic**: Works with any PHP framework or standalone +- **Type Safe**: Full PHP 8.1+ type safety with strict typing +- **Extensible**: Plugin architecture for custom actions and storage adapters +- **State Management**: Robust workflow instance state tracking +- **Error Handling**: Comprehensive exception handling with context +- **Performance**: Optimized for high-throughput workflow execution + +## Installation + +```bash +composer require solution-forest/workflow-engine-core +``` + +## Quick Start + +```php +use SolutionForest\WorkflowEngine\Core\WorkflowBuilder; +use SolutionForest\WorkflowEngine\Core\WorkflowEngine; +use SolutionForest\WorkflowEngine\Core\WorkflowContext; + +// Define a workflow +$workflow = WorkflowBuilder::create('order-processing') + ->addStep('validate', ValidateOrderAction::class) + ->addStep('payment', ProcessPaymentAction::class) + ->addStep('fulfillment', FulfillOrderAction::class) + ->addTransition('validate', 'payment') + ->addTransition('payment', 'fulfillment') + ->build(); + +// Create execution context +$context = new WorkflowContext( + workflowId: 'order-processing', + stepId: 'validate', + data: ['order_id' => 123, 'customer_id' => 456] +); + +// Execute workflow +$engine = new WorkflowEngine(); +$instance = $engine->start($workflow, $context); +$result = $engine->executeStep($instance, $context); +``` + +## Laravel Integration + +For Laravel applications, use the Laravel integration package: + +```bash +composer require solution-forest/workflow-engine-laravel +``` + +## Documentation + +- [Getting Started](docs/getting-started.md) +- [API Reference](docs/api-reference.md) +- [Advanced Features](docs/advanced-features.md) + +## License + +MIT License. See [LICENSE](LICENSE) for details. diff --git a/packages/workflow-engine-core/composer.json b/packages/workflow-engine-core/composer.json new file mode 100644 index 0000000..d442881 --- /dev/null +++ b/packages/workflow-engine-core/composer.json @@ -0,0 +1,49 @@ +{ + "name": "solution-forest/workflow-engine-core", + "description": "Framework-agnostic workflow engine for PHP applications", + "type": "library", + "license": "MIT", + "keywords": [ + "workflow", + "state-machine", + "business-process", + "automation", + "php" + ], + "authors": [ + { + "name": "Solution Forest", + "email": "info@solutionforest.com" + } + ], + "require": { + "php": "^8.1", + "nesbot/carbon": "^2.0|^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "phpstan/phpstan": "^1.10", + "pestphp/pest": "^2.0" + }, + "autoload": { + "psr-4": { + "SolutionForest\\WorkflowEngine\\": "src/" + }, + "files": [ + "src/helpers.php" + ] + }, + "autoload-dev": { + "psr-4": { + "SolutionForest\\WorkflowEngine\\Tests\\": "tests/" + } + }, + "minimum-stability": "stable", + "prefer-stable": true, + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } + } +} diff --git a/packages/workflow-engine-core/phpstan.neon.dist b/packages/workflow-engine-core/phpstan.neon.dist new file mode 100644 index 0000000..ab1b4c3 --- /dev/null +++ b/packages/workflow-engine-core/phpstan.neon.dist @@ -0,0 +1,12 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 5 + paths: + - src + - config + - database + tmpDir: build/phpstan + checkOctaneCompatibility: true + checkModelProperties: true diff --git a/packages/workflow-engine-core/phpunit.xml.dist b/packages/workflow-engine-core/phpunit.xml.dist new file mode 100644 index 0000000..fcacdf3 --- /dev/null +++ b/packages/workflow-engine-core/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + tests + + + + + + + + ./src + + + diff --git a/src/Actions/BaseAction.php b/packages/workflow-engine-core/src/Actions/BaseAction.php similarity index 97% rename from src/Actions/BaseAction.php rename to packages/workflow-engine-core/src/Actions/BaseAction.php index f77905e..9764203 100644 --- a/src/Actions/BaseAction.php +++ b/packages/workflow-engine-core/src/Actions/BaseAction.php @@ -1,12 +1,12 @@ instance = $instance; + } +} diff --git a/packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php b/packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php new file mode 100644 index 0000000..0c26cee --- /dev/null +++ b/packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php @@ -0,0 +1,19 @@ +instance = $instance; + $this->exception = $exception; + } +} diff --git a/packages/workflow-engine-core/src/Events/WorkflowStarted.php b/packages/workflow-engine-core/src/Events/WorkflowStarted.php new file mode 100644 index 0000000..f23cb08 --- /dev/null +++ b/packages/workflow-engine-core/src/Events/WorkflowStarted.php @@ -0,0 +1,12 @@ +getData('order'); + + // Mock shipment creation + $shipmentId = 'ship_'.uniqid(); + $trackingNumber = 'TRK'.mt_rand(100000, 999999); + + $context->setData('shipment.id', $shipmentId); + $context->setData('shipment.tracking_number', $trackingNumber); + $context->setData('shipment.created', true); + + return new ActionResult( + success: true, + data: [ + 'shipment_id' => $shipmentId, + 'tracking_number' => $trackingNumber, + 'status' => 'created', + ] + ); + } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('order') && + $context->getData('payment.success') === true; + } + + public function getName(): string + { + return 'Create Shipment'; + } + + public function getDescription(): string + { + return 'Creates shipment and generates tracking number'; + } +} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/FraudCheckAction.php b/packages/workflow-engine-core/tests/Actions/ECommerce/FraudCheckAction.php new file mode 100644 index 0000000..0dddf40 --- /dev/null +++ b/packages/workflow-engine-core/tests/Actions/ECommerce/FraudCheckAction.php @@ -0,0 +1,55 @@ +getData('order'); + + // Mock fraud detection logic + $riskScore = $this->calculateRiskScore($order); + $context->setData('fraud.risk', $riskScore); + + return new ActionResult( + success: true, + data: ['risk_score' => $riskScore, 'status' => $riskScore < 0.7 ? 'safe' : 'flagged'] + ); + } + + private function calculateRiskScore(array $order): float + { + // Simple mock risk calculation + $baseRisk = 0.1; + + if ($order['total'] > 10000) { + $baseRisk += 0.3; + } + + if ($order['total'] > 50000) { + $baseRisk += 0.4; + } + + return min($baseRisk, 1.0); + } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('order') && $context->getData('order.valid') === true; + } + + public function getName(): string + { + return 'Fraud Check'; + } + + public function getDescription(): string + { + return 'Analyzes order for potential fraud indicators'; + } +} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/NotificationAndCompensationActions.php b/packages/workflow-engine-core/tests/Actions/ECommerce/NotificationAndCompensationActions.php new file mode 100644 index 0000000..c4ca6c3 --- /dev/null +++ b/packages/workflow-engine-core/tests/Actions/ECommerce/NotificationAndCompensationActions.php @@ -0,0 +1,123 @@ +getData('order'); + $shipment = $context->getData('shipment'); + + // Mock notification sending + $notificationId = 'notif_'.uniqid(); + + $context->setData('notification.id', $notificationId); + $context->setData('notification.sent', true); + $context->setData('notification.type', 'order_confirmation'); + + return new ActionResult( + success: true, + data: [ + 'notification_id' => $notificationId, + 'recipient' => $order['customer_email'] ?? 'customer@example.com', + 'tracking_number' => $shipment['tracking_number'] ?? null, + 'status' => 'sent', + ] + ); + } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('order') && + $context->getData('shipment.created') === true; + } + + public function getName(): string + { + return 'Send Order Confirmation'; + } + + public function getDescription(): string + { + return 'Sends order confirmation email to customer'; + } +} + +// Compensation Actions +class ReleaseInventoryAction implements WorkflowAction +{ + public function execute(WorkflowContext $context): ActionResult + { + $reservationId = $context->getData('inventory.reservation_id'); + + if ($reservationId) { + $context->setData('inventory.reserved', false); + $context->setData('inventory.released', true); + } + + return new ActionResult( + success: true, + data: ['reservation_id' => $reservationId, 'status' => 'released'] + ); + } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('inventory.reservation_id'); + } + + public function getName(): string + { + return 'Release Inventory'; + } + + public function getDescription(): string + { + return 'Releases previously reserved inventory'; + } +} + +class RefundPaymentAction implements WorkflowAction +{ + public function execute(WorkflowContext $context): ActionResult + { + $paymentId = $context->getData('payment.id'); + $amount = $context->getData('payment.amount'); + + if ($paymentId) { + $refundId = 'ref_'.uniqid(); + $context->setData('refund.id', $refundId); + $context->setData('refund.amount', $amount); + $context->setData('refund.processed', true); + } + + return new ActionResult( + success: true, + data: [ + 'refund_id' => $refundId ?? null, + 'amount' => $amount, + 'status' => 'processed', + ] + ); + } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('payment.id'); + } + + public function getName(): string + { + return 'Refund Payment'; + } + + public function getDescription(): string + { + return 'Processes payment refund'; + } +} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/ProcessPaymentAction.php b/packages/workflow-engine-core/tests/Actions/ECommerce/ProcessPaymentAction.php new file mode 100644 index 0000000..7f34495 --- /dev/null +++ b/packages/workflow-engine-core/tests/Actions/ECommerce/ProcessPaymentAction.php @@ -0,0 +1,54 @@ +getData('order'); + + // Mock payment processing + $paymentId = 'pay_'.uniqid(); + $success = $order['total'] < 100000; // Simulate payment failure for very large orders + + if ($success) { + $context->setData('payment.id', $paymentId); + $context->setData('payment.success', true); + $context->setData('payment.amount', $order['total']); + } else { + $context->setData('payment.success', false); + $context->setData('payment.error', 'Payment declined'); + } + + return new ActionResult( + success: $success, + data: [ + 'payment_id' => $success ? $paymentId : null, + 'amount' => $order['total'], + 'status' => $success ? 'completed' : 'failed', + ], + errorMessage: $success ? null : 'Payment processing failed' + ); + } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('order') && + $context->getData('inventory.reserved') === true; + } + + public function getName(): string + { + return 'Process Payment'; + } + + public function getDescription(): string + { + return 'Processes payment for the order'; + } +} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/ReserveInventoryAction.php b/packages/workflow-engine-core/tests/Actions/ECommerce/ReserveInventoryAction.php new file mode 100644 index 0000000..d133f4a --- /dev/null +++ b/packages/workflow-engine-core/tests/Actions/ECommerce/ReserveInventoryAction.php @@ -0,0 +1,42 @@ +getData('order'); + + // Mock inventory reservation + $reservationId = 'res_'.uniqid(); + $context->setData('inventory.reservation_id', $reservationId); + $context->setData('inventory.reserved', true); + + return new ActionResult( + success: true, + data: ['reservation_id' => $reservationId, 'status' => 'reserved'] + ); + } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('order') && + $context->getData('order.valid') === true && + ($context->getData('fraud.risk') ?? 0) < 0.7; + } + + public function getName(): string + { + return 'Reserve Inventory'; + } + + public function getDescription(): string + { + return 'Reserves inventory items for the order'; + } +} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/ValidateOrderAction.php b/packages/workflow-engine-core/tests/Actions/ECommerce/ValidateOrderAction.php new file mode 100644 index 0000000..a3322f8 --- /dev/null +++ b/packages/workflow-engine-core/tests/Actions/ECommerce/ValidateOrderAction.php @@ -0,0 +1,43 @@ +getData('order'); + + // Mock validation logic + $isValid = isset($order['items']) && + count($order['items']) > 0 && + isset($order['total']) && + $order['total'] > 0; + + $context->setData('order.valid', $isValid); + + return new ActionResult( + success: $isValid, + data: ['validation_result' => $isValid ? 'passed' : 'failed'] + ); + } + + public function canExecute(WorkflowContext $context): bool + { + return $context->hasData('order'); + } + + public function getName(): string + { + return 'Validate Order'; + } + + public function getDescription(): string + { + return 'Validates order data including items and total amount'; + } +} diff --git a/packages/workflow-engine-core/tests/ArchTest.php b/packages/workflow-engine-core/tests/ArchTest.php new file mode 100644 index 0000000..87fb64c --- /dev/null +++ b/packages/workflow-engine-core/tests/ArchTest.php @@ -0,0 +1,5 @@ +expect(['dd', 'dump', 'ray']) + ->each->not->toBeUsed(); diff --git a/packages/workflow-engine-core/tests/ExampleTest.php b/packages/workflow-engine-core/tests/ExampleTest.php new file mode 100644 index 0000000..5d36321 --- /dev/null +++ b/packages/workflow-engine-core/tests/ExampleTest.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); diff --git a/packages/workflow-engine-core/tests/Integration/WorkflowIntegrationTest.php b/packages/workflow-engine-core/tests/Integration/WorkflowIntegrationTest.php new file mode 100644 index 0000000..2e79f93 --- /dev/null +++ b/packages/workflow-engine-core/tests/Integration/WorkflowIntegrationTest.php @@ -0,0 +1,255 @@ + 'User Onboarding Workflow', + 'version' => '1.0', + 'steps' => [ + [ + 'id' => 'welcome', + 'name' => 'Welcome User', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Welcome {{name}} to our platform!', + 'level' => 'info', + ], + ], + [ + 'id' => 'setup_profile', + 'name' => 'Setup User Profile', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Setting up profile for {{name}} with email {{email}}', + 'level' => 'info', + ], + ], + [ + 'id' => 'send_confirmation', + 'name' => 'Send Confirmation', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Sending confirmation email to {{email}}', + 'level' => 'info', + ], + ], + ], + 'transitions' => [ + [ + 'from' => 'welcome', + 'to' => 'setup_profile', + ], + [ + 'from' => 'setup_profile', + 'to' => 'send_confirmation', + ], + ], + ]; + + $context = [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'userId' => 123, + ]; + + // Start the workflow using helper function + $workflowId = start_workflow('user-onboarding-123', $definition, $context); + + // Verify workflow was created and completed + expect($workflowId)->not->toBeEmpty(); + expect($workflowId)->toBe('user-onboarding-123'); + + // Get workflow instance using helper + $instance = get_workflow($workflowId); + + // Verify the workflow completed successfully + expect($instance->getState())->toBe(WorkflowState::COMPLETED); + expect($instance->getName())->toBe('User Onboarding Workflow'); + + // Verify the context contains original data plus step outputs + $workflowData = $instance->getContext()->getData(); + expect($workflowData['name'])->toBe('John Doe'); + expect($workflowData['email'])->toBe('john@example.com'); + expect($workflowData['userId'])->toBe(123); + + // Verify that all steps completed successfully + expect($instance->getCompletedSteps())->toHaveCount(3); + expect($instance->getCompletedSteps())->toContain('welcome'); + expect($instance->getCompletedSteps())->toContain('setup_profile'); + expect($instance->getCompletedSteps())->toContain('send_confirmation'); + + // Verify no failed steps + expect($instance->getFailedSteps())->toBeEmpty(); + + // Check workflow status + $status = workflow()->getStatus($workflowId); + expect($status['state'])->toBe('completed'); + expect($status['name'])->toBe('User Onboarding Workflow'); + expect($status['progress'])->toBe(100.0); // 100% complete +}); + +test('it can handle workflow cancellation', function () { + $definition = [ + 'name' => 'Cancellable Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Starting process'], + ], + ], + ]; + + // Start workflow + $workflowId = start_workflow('cancellable-workflow', $definition); + + // Cancel workflow + cancel_workflow($workflowId, 'User requested cancellation'); + + // Verify cancellation + $instance = get_workflow($workflowId); + expect($instance->getState())->toBe(WorkflowState::CANCELLED); +}); + +test('it can list and filter workflows', function () { + $definition1 = [ + 'name' => 'Workflow 1', + 'steps' => [ + ['id' => 'step1', 'action' => 'log', 'parameters' => ['message' => 'Test']], + ], + ]; + + $definition2 = [ + 'name' => 'Workflow 2', + 'steps' => [ + ['id' => 'step1', 'action' => 'log', 'parameters' => ['message' => 'Test']], + ], + ]; + + // Start two workflows + $workflow1Id = start_workflow('list-test-1', $definition1); + $workflow2Id = start_workflow('list-test-2', $definition2); + + // Cancel one + cancel_workflow($workflow2Id); + + // List all workflows + $allWorkflows = workflow()->listWorkflows(); + expect(count($allWorkflows))->toBeGreaterThanOrEqual(2); + + // Filter by state + $completedWorkflows = workflow()->listWorkflows(['state' => WorkflowState::COMPLETED]); + $cancelledWorkflows = workflow()->listWorkflows(['state' => WorkflowState::CANCELLED]); + + expect(count($completedWorkflows))->toBeGreaterThanOrEqual(1); + expect(count($cancelledWorkflows))->toBeGreaterThanOrEqual(1); + + // Verify specific workflows exist in filtered results + $completedIds = array_column($completedWorkflows, 'workflow_id'); + $cancelledIds = array_column($cancelledWorkflows, 'workflow_id'); + + expect($completedIds)->toContain($workflow1Id); + expect($cancelledIds)->toContain($workflow2Id); +}); + +test('it can execute conditional workflows', function () { + $definition = [ + 'name' => 'Conditional Approval Workflow', + 'version' => '1.0', + 'steps' => [ + [ + 'id' => 'validate_request', + 'name' => 'Validate Request', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Validating request from {{user}}', + 'level' => 'info', + ], + ], + [ + 'id' => 'auto_approve', + 'name' => 'Auto Approve', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Auto-approving request for premium user {{user}}', + 'level' => 'info', + ], + ], + [ + 'id' => 'manual_review', + 'name' => 'Manual Review Required', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Manual review required for user {{user}}', + 'level' => 'warning', + ], + ], + [ + 'id' => 'notify_completion', + 'name' => 'Notify Completion', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Process completed for {{user}}', + 'level' => 'info', + ], + ], + ], + 'transitions' => [ + [ + 'from' => 'validate_request', + 'to' => 'auto_approve', + 'condition' => 'tier === premium', + ], + [ + 'from' => 'validate_request', + 'to' => 'manual_review', + 'condition' => 'tier !== premium', + ], + [ + 'from' => 'auto_approve', + 'to' => 'notify_completion', + ], + [ + 'from' => 'manual_review', + 'to' => 'notify_completion', + ], + ], + ]; + + // Test premium user path (should auto-approve) + $premiumContext = [ + 'user' => 'Alice Premium', + 'tier' => 'premium', + 'amount' => 1000, + ]; + + $premiumWorkflowId = start_workflow('premium-approval-123', $definition, $premiumContext); + $premiumInstance = get_workflow($premiumWorkflowId); + + // Verify premium workflow took auto-approval path + expect($premiumInstance->getState())->toBe(WorkflowState::COMPLETED); + expect($premiumInstance->getCompletedSteps())->toContain('validate_request'); + expect($premiumInstance->getCompletedSteps())->toContain('auto_approve'); + expect($premiumInstance->getCompletedSteps())->toContain('notify_completion'); + expect($premiumInstance->getCompletedSteps())->not->toContain('manual_review'); + + // Test regular user path (should require manual review) + $regularContext = [ + 'user' => 'Bob Regular', + 'tier' => 'basic', + 'amount' => 5000, + ]; + + $regularWorkflowId = start_workflow('regular-approval-456', $definition, $regularContext); + $regularInstance = get_workflow($regularWorkflowId); + + // Verify regular workflow took manual review path + expect($regularInstance->getState())->toBe(WorkflowState::COMPLETED); + expect($regularInstance->getCompletedSteps())->toContain('validate_request'); + expect($regularInstance->getCompletedSteps())->toContain('manual_review'); + expect($regularInstance->getCompletedSteps())->toContain('notify_completion'); + expect($regularInstance->getCompletedSteps())->not->toContain('auto_approve'); +}); diff --git a/packages/workflow-engine-core/tests/Pest.php b/packages/workflow-engine-core/tests/Pest.php new file mode 100644 index 0000000..8b80a2d --- /dev/null +++ b/packages/workflow-engine-core/tests/Pest.php @@ -0,0 +1,5 @@ +in(__DIR__); diff --git a/packages/workflow-engine-core/tests/RealWorld/CICDPipelineWorkflowTest.php b/packages/workflow-engine-core/tests/RealWorld/CICDPipelineWorkflowTest.php new file mode 100644 index 0000000..93956f4 --- /dev/null +++ b/packages/workflow-engine-core/tests/RealWorld/CICDPipelineWorkflowTest.php @@ -0,0 +1,427 @@ +engine = app(WorkflowEngine::class); +}); + +test('cicd pipeline workflow - successful deployment flow', function () { + $definition = [ + 'name' => 'CI/CD Pipeline Workflow', + 'version' => '3.0', + 'steps' => [ + [ + 'id' => 'checkout_code', + 'name' => 'Code Checkout', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Checking out code from {{pipeline.repository}} branch {{pipeline.branch}}', + 'level' => 'info', + ], + ], + [ + 'id' => 'run_unit_tests', + 'name' => 'Unit Tests', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running unit tests for {{pipeline.project_name}}', + 'timeout' => '10m', + 'parallel_group' => 'tests', + ], + ], + [ + 'id' => 'security_scan', + 'name' => 'Security Scan', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running security vulnerability scan', + 'timeout' => '15m', + 'parallel_group' => 'tests', + ], + ], + [ + 'id' => 'run_integration_tests', + 'name' => 'Integration Tests', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running integration tests for {{pipeline.project_name}}', + 'timeout' => '20m', + ], + ], + [ + 'id' => 'build_artifacts', + 'name' => 'Build Artifacts', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Building deployment artifacts for {{pipeline.project_name}}', + 'timeout' => '30m', + ], + ], + [ + 'id' => 'deploy_staging', + 'name' => 'Deploy to Staging', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Deploying {{pipeline.project_name}} to staging environment', + 'compensation' => 'rollback_staging', + ], + ], + [ + 'id' => 'run_e2e_tests', + 'name' => 'End-to-End Tests', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running E2E tests on staging environment', + 'timeout' => '45m', + ], + ], + [ + 'id' => 'approval_gate', + 'name' => 'Production Approval Gate', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Awaiting production deployment approval for {{pipeline.project_name}}', + 'timeout' => '24h', + 'assigned_to' => 'release_manager', + ], + ], + [ + 'id' => 'deploy_production', + 'name' => 'Deploy to Production', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Deploying {{pipeline.project_name}} to production environment', + 'compensation' => 'rollback_production', + ], + ], + [ + 'id' => 'smoke_tests', + 'name' => 'Production Smoke Tests', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running smoke tests on production deployment', + 'timeout' => '5m', + ], + ], + ], + 'transitions' => [ + ['from' => 'checkout_code', 'to' => 'run_unit_tests'], + ['from' => 'checkout_code', 'to' => 'security_scan'], + ['from' => 'run_unit_tests', 'to' => 'run_integration_tests'], + ['from' => 'security_scan', 'to' => 'run_integration_tests'], + ['from' => 'run_integration_tests', 'to' => 'build_artifacts'], + ['from' => 'build_artifacts', 'to' => 'deploy_staging'], + ['from' => 'deploy_staging', 'to' => 'run_e2e_tests'], + ['from' => 'run_e2e_tests', 'to' => 'approval_gate'], + ['from' => 'approval_gate', 'to' => 'deploy_production'], + ['from' => 'deploy_production', 'to' => 'smoke_tests'], + ], + ]; + + $pipelineContext = [ + 'pipeline' => [ + 'id' => 'PIPE-001', + 'project_name' => 'workflow-engine-api', + 'repository' => 'https://github.com/company/workflow-engine-api', + 'branch' => 'main', + 'commit_sha' => 'abc123def456', + 'triggered_by' => 'developer@company.com', + 'environment' => 'production', + ], + ]; + + $workflowId = $this->engine->start('cicd-pipeline', $definition, $pipelineContext); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance)->not()->toBeNull(); + expect($instance->getState())->toBe(WorkflowState::COMPLETED); + expect($instance->getContext()->getData()['pipeline']['project_name'])->toBe('workflow-engine-api'); +}); + +test('cicd pipeline workflow - parallel testing stages', function () { + $definition = [ + 'name' => 'CI/CD Pipeline with Parallel Testing', + 'version' => '3.0', + 'steps' => [ + [ + 'id' => 'checkout_code', + 'name' => 'Source Code Checkout', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Checking out source code for parallel testing pipeline', + ], + ], + [ + 'id' => 'unit_tests_parallel', + 'name' => 'Unit Tests (Parallel)', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running unit tests in parallel - estimated 8 minutes', + 'test_suite' => 'unit', + 'parallel_workers' => 4, + ], + ], + [ + 'id' => 'security_scan_parallel', + 'name' => 'Security Scan (Parallel)', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running security vulnerability scan in parallel - estimated 12 minutes', + 'scan_type' => 'dependency_check', + 'tools' => ['snyk', 'owasp-dependency-check'], + ], + ], + [ + 'id' => 'code_quality_parallel', + 'name' => 'Code Quality (Parallel)', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running code quality analysis in parallel - estimated 5 minutes', + 'tools' => ['sonarqube', 'phpstan'], + 'quality_gates' => ['coverage', 'complexity', 'duplication'], + ], + ], + [ + 'id' => 'parallel_tests_complete', + 'name' => 'Parallel Tests Completion', + 'action' => 'log', + 'parameters' => [ + 'message' => 'All parallel test stages completed successfully', + 'join_condition' => 'all_tests_passed', + ], + ], + [ + 'id' => 'integration_tests', + 'name' => 'Integration Test Suite', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running comprehensive integration tests', + ], + ], + ], + 'parallel_groups' => [ + [ + 'name' => 'testing_phase', + 'steps' => ['unit_tests_parallel', 'security_scan_parallel', 'code_quality_parallel'], + 'join_type' => 'all_success', + ], + ], + 'transitions' => [ + ['from' => 'checkout_code', 'to' => 'unit_tests_parallel'], + ['from' => 'checkout_code', 'to' => 'security_scan_parallel'], + ['from' => 'checkout_code', 'to' => 'code_quality_parallel'], + ['from' => 'unit_tests_parallel', 'to' => 'parallel_tests_complete'], + ['from' => 'security_scan_parallel', 'to' => 'parallel_tests_complete'], + ['from' => 'code_quality_parallel', 'to' => 'parallel_tests_complete'], + ['from' => 'parallel_tests_complete', 'to' => 'integration_tests'], + ], + ]; + + $parallelPipelineContext = [ + 'pipeline' => [ + 'id' => 'PIPE-PARALLEL-001', + 'project_name' => 'microservice-api', + 'type' => 'parallel_testing', + 'test_configuration' => [ + 'parallel_workers' => 4, + 'test_timeout' => '15m', + 'quality_threshold' => 80, + ], + 'optimization' => 'parallel_execution', + ], + ]; + + $workflowId = $this->engine->start('parallel-cicd', $definition, $parallelPipelineContext); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance->getContext()->getData()['pipeline']['type'])->toBe('parallel_testing'); + expect($instance->getContext()->getData()['pipeline']['optimization'])->toBe('parallel_execution'); +}); + +test('cicd pipeline workflow - deployment failure and rollback', function () { + $definition = [ + 'name' => 'CI/CD Pipeline with Rollback', + 'version' => '3.0', + 'steps' => [ + [ + 'id' => 'build_and_test', + 'name' => 'Build and Test', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Building and testing application before deployment', + ], + ], + [ + 'id' => 'deploy_staging', + 'name' => 'Deploy to Staging', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Deploying to staging environment', + 'environment' => 'staging', + ], + ], + [ + 'id' => 'staging_tests', + 'name' => 'Staging Validation Tests', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running validation tests on staging deployment', + ], + ], + [ + 'id' => 'production_deployment', + 'name' => 'Production Deployment', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Deploying to production environment - simulating failure', + 'environment' => 'production', + 'failure_simulation' => true, + ], + ], + [ + 'id' => 'rollback_production', + 'name' => 'Production Rollback', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Rolling back production deployment due to failure', + 'rollback_type' => 'automatic', + 'target_version' => 'previous_stable', + ], + ], + [ + 'id' => 'incident_notification', + 'name' => 'Incident Notification', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Notifying teams about deployment failure and rollback', + 'notification_channels' => ['slack', 'pagerduty', 'email'], + ], + ], + ], + 'transitions' => [ + ['from' => 'build_and_test', 'to' => 'deploy_staging'], + ['from' => 'deploy_staging', 'to' => 'staging_tests'], + ['from' => 'staging_tests', 'to' => 'production_deployment'], + ['from' => 'production_deployment', 'to' => 'rollback_production', 'condition' => 'deployment.failed === true'], + ['from' => 'rollback_production', 'to' => 'incident_notification'], + ], + 'error_handling' => [ + 'strategy' => 'rollback_and_notify', + 'rollback_triggers' => ['deployment_failure', 'health_check_failure'], + 'notification_channels' => ['slack', 'pagerduty'], + ], + ]; + + $rollbackContext = [ + 'pipeline' => [ + 'id' => 'PIPE-ROLLBACK-001', + 'project_name' => 'critical-service', + 'deployment_strategy' => 'blue_green', + 'rollback_enabled' => true, + 'failure_scenario' => 'deployment_failure', + 'previous_version' => 'v2.1.0', + 'target_version' => 'v2.2.0', + ], + ]; + + $workflowId = $this->engine->start('rollback-pipeline', $definition, $rollbackContext); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance->getContext()->getData()['pipeline']['rollback_enabled'])->toBe(true); + expect($instance->getContext()->getData()['pipeline']['failure_scenario'])->toBe('deployment_failure'); +}); + +test('cicd pipeline workflow - feature branch deployment', function () { + $definition = [ + 'name' => 'Feature Branch CI/CD Pipeline', + 'version' => '3.0', + 'steps' => [ + [ + 'id' => 'feature_validation', + 'name' => 'Feature Branch Validation', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Validating feature branch {{pipeline.feature_branch}}', + ], + ], + [ + 'id' => 'run_tests', + 'name' => 'Feature Tests', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running comprehensive tests for feature {{pipeline.feature_name}}', + ], + ], + [ + 'id' => 'deploy_preview', + 'name' => 'Deploy Preview Environment', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Creating preview environment for feature {{pipeline.feature_name}}', + 'environment_type' => 'ephemeral', + 'auto_cleanup' => '7d', + ], + ], + [ + 'id' => 'qa_validation', + 'name' => 'QA Validation', + 'action' => 'log', + 'parameters' => [ + 'message' => 'QA team validating feature in preview environment', + 'assigned_to' => 'qa_team', + 'validation_checklist' => 'feature_requirements', + ], + ], + [ + 'id' => 'merge_approval', + 'name' => 'Merge Approval', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Requesting approval to merge feature {{pipeline.feature_name}} to main', + 'reviewers' => ['tech_lead', 'product_owner'], + ], + ], + ], + 'transitions' => [ + ['from' => 'feature_validation', 'to' => 'run_tests'], + ['from' => 'run_tests', 'to' => 'deploy_preview'], + ['from' => 'deploy_preview', 'to' => 'qa_validation'], + ['from' => 'qa_validation', 'to' => 'merge_approval'], + ], + 'cleanup_rules' => [ + 'preview_environment_ttl' => '7d', + 'auto_cleanup_on_merge' => true, + 'cleanup_on_branch_delete' => true, + ], + ]; + + $featureBranchContext = [ + 'pipeline' => [ + 'id' => 'PIPE-FEATURE-001', + 'type' => 'feature_branch', + 'feature_name' => 'advanced-search-functionality', + 'feature_branch' => 'feature/advanced-search', + 'base_branch' => 'develop', + 'developer' => 'developer@company.com', + 'jira_ticket' => 'PROJ-1234', + 'preview_url' => 'https://feature-advanced-search.preview.company.com', + ], + ]; + + $workflowId = $this->engine->start('feature-pipeline', $definition, $featureBranchContext); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance->getContext()->getData()['pipeline']['type'])->toBe('feature_branch'); + expect($instance->getContext()->getData()['pipeline']['feature_name'])->toBe('advanced-search-functionality'); + expect($instance->getContext()->getData()['pipeline']['jira_ticket'])->toBe('PROJ-1234'); +}); diff --git a/packages/workflow-engine-core/tests/RealWorld/DocumentApprovalWorkflowTest.php b/packages/workflow-engine-core/tests/RealWorld/DocumentApprovalWorkflowTest.php new file mode 100644 index 0000000..47df8e7 --- /dev/null +++ b/packages/workflow-engine-core/tests/RealWorld/DocumentApprovalWorkflowTest.php @@ -0,0 +1,373 @@ +engine = app(WorkflowEngine::class); +}); + +test('document approval workflow - standard approval flow', function () { + $definition = [ + 'name' => 'Document Approval Workflow', + 'version' => '1.5', + 'steps' => [ + [ + 'id' => 'submit_document', + 'name' => 'Submit Document', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Document {{document.title}} submitted for approval by {{document.author}}', + 'level' => 'info', + ], + ], + [ + 'id' => 'initial_review', + 'name' => 'Manager Review', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Initial review by manager for document {{document.title}}', + 'assigned_to' => 'manager_role', + 'timeout' => '2d', + ], + ], + [ + 'id' => 'legal_review', + 'name' => 'Legal Review', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Legal team reviewing contract document {{document.title}}', + 'assigned_to' => 'legal_team', + 'timeout' => '5d', + 'conditions' => 'document.type === "contract"', + ], + ], + [ + 'id' => 'compliance_review', + 'name' => 'Compliance Review', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Compliance review for high-value document {{document.title}}', + 'assigned_to' => 'compliance_team', + 'timeout' => '3d', + 'conditions' => 'document.value > 100000', + ], + ], + [ + 'id' => 'final_approval', + 'name' => 'Executive Approval', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Executive final approval for document {{document.title}}', + 'assigned_to' => 'executive_role', + 'timeout' => '1d', + ], + ], + [ + 'id' => 'archive_document', + 'name' => 'Archive Document', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Archiving approved document {{document.title}} to secure repository', + ], + ], + ], + 'transitions' => [ + ['from' => 'submit_document', 'to' => 'initial_review'], + ['from' => 'initial_review', 'to' => 'legal_review'], + ['from' => 'legal_review', 'to' => 'compliance_review'], + ['from' => 'compliance_review', 'to' => 'final_approval'], + ['from' => 'final_approval', 'to' => 'archive_document'], + ], + ]; + + $documentContext = [ + 'document' => [ + 'id' => 'DOC-001', + 'title' => 'Software License Agreement', + 'author' => 'legal@company.com', + 'type' => 'contract', + 'value' => 250000, + 'submitted_date' => '2024-01-15', + 'priority' => 'high', + ], + ]; + + $workflowId = $this->engine->start('document-approval', $definition, $documentContext); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance)->not()->toBeNull(); + expect($instance->getState())->toBe(WorkflowState::COMPLETED); + expect($instance->getContext()->getData()['document']['type'])->toBe('contract'); +}); + +test('document approval workflow - parallel review process', function () { + $definition = [ + 'name' => 'Parallel Document Approval', + 'version' => '1.5', + 'steps' => [ + [ + 'id' => 'submit_document', + 'name' => 'Document Submission', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Document submitted: {{document.title}} - initiating parallel reviews', + ], + ], + [ + 'id' => 'legal_review_parallel', + 'name' => 'Legal Review (Parallel)', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Legal team parallel review for {{document.title}}', + 'parallel_group' => 'reviews', + 'estimated_duration' => '3-5 days', + ], + ], + [ + 'id' => 'compliance_review_parallel', + 'name' => 'Compliance Review (Parallel)', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Compliance team parallel review for {{document.title}}', + 'parallel_group' => 'reviews', + 'estimated_duration' => '2-4 days', + ], + ], + [ + 'id' => 'technical_review_parallel', + 'name' => 'Technical Review (Parallel)', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Technical team parallel review for {{document.title}}', + 'parallel_group' => 'reviews', + 'estimated_duration' => '1-2 days', + ], + ], + [ + 'id' => 'consolidate_reviews', + 'name' => 'Consolidate Reviews', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Consolidating all parallel reviews for {{document.title}}', + 'join_condition' => 'all_reviews_complete', + ], + ], + [ + 'id' => 'final_decision', + 'name' => 'Final Decision', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Making final approval decision for {{document.title}}', + ], + ], + ], + 'parallel_groups' => [ + [ + 'name' => 'reviews', + 'steps' => ['legal_review_parallel', 'compliance_review_parallel', 'technical_review_parallel'], + 'join_type' => 'all_complete', + ], + ], + 'transitions' => [ + ['from' => 'submit_document', 'to' => 'legal_review_parallel'], + ['from' => 'submit_document', 'to' => 'compliance_review_parallel'], + ['from' => 'submit_document', 'to' => 'technical_review_parallel'], + ['from' => 'legal_review_parallel', 'to' => 'consolidate_reviews'], + ['from' => 'compliance_review_parallel', 'to' => 'consolidate_reviews'], + ['from' => 'technical_review_parallel', 'to' => 'consolidate_reviews'], + ['from' => 'consolidate_reviews', 'to' => 'final_decision'], + ], + ]; + + $complexDocumentContext = [ + 'document' => [ + 'id' => 'DOC-COMPLEX-001', + 'title' => 'Multi-Million Dollar Partnership Agreement', + 'author' => 'partnerships@company.com', + 'type' => 'partnership_agreement', + 'value' => 5000000, + 'complexity' => 'high', + 'requires_parallel_review' => true, + 'review_teams' => ['legal', 'compliance', 'technical'], + ], + ]; + + $workflowId = $this->engine->start('complex-document-approval', $definition, $complexDocumentContext); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance->getContext()->getData()['document']['requires_parallel_review'])->toBe(true); + expect($instance->getContext()->getData()['document']['value'])->toBe(5000000); +}); + +test('document approval workflow - rejection and resubmission flow', function () { + $definition = [ + 'name' => 'Document Approval with Rejection', + 'version' => '1.5', + 'steps' => [ + [ + 'id' => 'submit_document', + 'name' => 'Initial Submission', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Document {{document.title}} submitted for approval', + ], + ], + [ + 'id' => 'initial_review', + 'name' => 'Initial Review', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Reviewing document {{document.title}} for initial compliance', + ], + ], + [ + 'id' => 'rejection_notification', + 'name' => 'Rejection Notification', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Document {{document.title}} rejected - notifying author of required changes', + 'rejection_reasons' => 'compliance_issues', + 'action_required' => 'revision_needed', + ], + ], + [ + 'id' => 'revision_period', + 'name' => 'Revision Period', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Author has 7 days to revise and resubmit {{document.title}}', + 'timeout' => '7d', + 'auto_escalate' => true, + ], + ], + [ + 'id' => 'resubmission_review', + 'name' => 'Resubmission Review', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Reviewing resubmitted document {{document.title}}', + ], + ], + ], + 'transitions' => [ + ['from' => 'submit_document', 'to' => 'initial_review'], + ['from' => 'initial_review', 'to' => 'rejection_notification', 'condition' => 'review.approved === false'], + ['from' => 'rejection_notification', 'to' => 'revision_period'], + ['from' => 'revision_period', 'to' => 'resubmission_review', 'condition' => 'document.resubmitted === true'], + ], + 'error_handling' => [ + 'rejection_workflow' => 'revision_and_resubmit', + 'max_revisions' => 3, + 'escalation_after_rejections' => 2, + ], + ]; + + $rejectedDocumentContext = [ + 'document' => [ + 'id' => 'DOC-REVISION-001', + 'title' => 'Non-Compliant Service Agreement', + 'author' => 'sales@company.com', + 'type' => 'service_agreement', + 'value' => 50000, + 'initial_submission' => true, + 'compliance_issues' => [ + 'missing_liability_clauses', + 'incorrect_termination_terms', + 'data_protection_gaps', + ], + ], + ]; + + $workflowId = $this->engine->start('rejection-flow', $definition, $rejectedDocumentContext); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance->getContext()->getData()['document']['initial_submission'])->toBe(true); + expect(count($instance->getContext()->getData()['document']['compliance_issues']))->toBe(3); +}); + +test('document approval workflow - escalation and timeout handling', function () { + $definition = [ + 'name' => 'Document Approval with Escalation', + 'version' => '1.5', + 'steps' => [ + [ + 'id' => 'submit_document', + 'name' => 'Document Submission', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Critical document {{document.title}} submitted with escalation rules', + ], + ], + [ + 'id' => 'manager_review', + 'name' => 'Manager Review (Timed)', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Manager reviewing {{document.title}} - must complete within 24 hours', + 'timeout' => '24h', + 'escalation_target' => 'director_level', + ], + ], + [ + 'id' => 'director_escalation', + 'name' => 'Director Escalation', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Document {{document.title}} escalated to director due to timeout', + 'escalation_reason' => 'manager_timeout', + 'priority' => 'urgent', + ], + ], + [ + 'id' => 'executive_override', + 'name' => 'Executive Override', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Executive override for time-critical document {{document.title}}', + 'override_reason' => 'business_critical', + 'expedited_approval' => true, + ], + ], + ], + 'transitions' => [ + ['from' => 'submit_document', 'to' => 'manager_review'], + ['from' => 'manager_review', 'to' => 'director_escalation', 'condition' => 'timeout_occurred'], + ['from' => 'director_escalation', 'to' => 'executive_override', 'condition' => 'escalation_required'], + ], + 'escalation_rules' => [ + 'timeout_escalation' => true, + 'escalation_levels' => ['manager', 'director', 'executive'], + 'notification_channels' => ['email', 'slack', 'sms'], + ], + ]; + + $urgentDocumentContext = [ + 'document' => [ + 'id' => 'DOC-URGENT-001', + 'title' => 'Emergency Vendor Contract', + 'author' => 'procurement@company.com', + 'type' => 'emergency_contract', + 'value' => 2000000, + 'priority' => 'critical', + 'business_impact' => 'production_blocker', + 'deadline' => '2024-01-20T23:59:59Z', + 'escalation_enabled' => true, + ], + ]; + + $workflowId = $this->engine->start('escalation-flow', $definition, $urgentDocumentContext); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance->getContext()->getData()['document']['priority'])->toBe('critical'); + expect($instance->getContext()->getData()['document']['escalation_enabled'])->toBe(true); + expect($instance->getContext()->getData()['document']['business_impact'])->toBe('production_blocker'); +}); diff --git a/packages/workflow-engine-core/tests/RealWorld/ECommerceWorkflowTest.php b/packages/workflow-engine-core/tests/RealWorld/ECommerceWorkflowTest.php new file mode 100644 index 0000000..64becb6 --- /dev/null +++ b/packages/workflow-engine-core/tests/RealWorld/ECommerceWorkflowTest.php @@ -0,0 +1,328 @@ +engine = app(WorkflowEngine::class); +}); + +test('e-commerce order processing workflow - successful order flow', function () { + // Create workflow definition based on ARCHITECTURE.md example + $definition = [ + 'name' => 'E-Commerce Order Processing', + 'version' => '2.0', + 'steps' => [ + [ + 'id' => 'validate_order', + 'name' => 'Validate Order', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Validating order {{order.id}} with total {{order.total}}', + 'level' => 'info', + ], + ], + [ + 'id' => 'check_fraud', + 'name' => 'Fraud Check', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running fraud check for order {{order.id}}', + 'level' => 'info', + ], + ], + [ + 'id' => 'reserve_inventory', + 'name' => 'Reserve Inventory', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Reserving inventory for order {{order.id}}', + 'level' => 'info', + ], + ], + [ + 'id' => 'process_payment', + 'name' => 'Process Payment', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Processing payment for order {{order.id}} amount {{order.total}}', + 'level' => 'info', + ], + ], + [ + 'id' => 'create_shipment', + 'name' => 'Create Shipment', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Creating shipment for order {{order.id}}', + 'level' => 'info', + ], + ], + [ + 'id' => 'send_notification', + 'name' => 'Send Order Confirmation', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Sending order confirmation for {{order.id}} to {{order.customer_email}}', + 'level' => 'info', + ], + ], + ], + 'transitions' => [ + ['from' => 'validate_order', 'to' => 'check_fraud'], + ['from' => 'check_fraud', 'to' => 'reserve_inventory'], + ['from' => 'reserve_inventory', 'to' => 'process_payment'], + ['from' => 'process_payment', 'to' => 'create_shipment'], + ['from' => 'create_shipment', 'to' => 'send_notification'], + ], + ]; + + // Valid order data + $orderContext = [ + 'order' => [ + 'id' => 'ORD-12345', + 'customer_email' => 'customer@example.com', + 'items' => [ + ['sku' => 'ITEM-001', 'quantity' => 2, 'price' => 50.00], + ['sku' => 'ITEM-002', 'quantity' => 1, 'price' => 100.00], + ], + 'total' => 200.00, + 'currency' => 'USD', + ], + ]; + + // Start workflow + $workflowId = $this->engine->start('ecommerce-order', $definition, $orderContext); + + expect($workflowId)->not()->toBeEmpty(); + + // Get workflow instance to check state + $instance = $this->engine->getInstance($workflowId); + expect($instance)->not()->toBeNull(); + expect($instance->getState())->toBe(WorkflowState::COMPLETED); +}); + +test('e-commerce order processing workflow - workflow definition structure', function () { + $definition = [ + 'name' => 'E-Commerce Order Processing', + 'version' => '2.0', + 'steps' => [ + [ + 'id' => 'validate_order', + 'name' => 'Validate Order', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Order validation: checking items, total, and customer data', + 'conditions' => [ + 'order.items.count > 0', + 'order.total > 0', + 'order.customer_email is set', + ], + ], + ], + [ + 'id' => 'fraud_check', + 'name' => 'Fraud Detection', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Fraud check: analyzing order patterns and risk factors', + 'timeout' => '2m', + 'risk_threshold' => 0.7, + ], + ], + [ + 'id' => 'inventory_reservation', + 'name' => 'Inventory Management', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Inventory: reserving items for order fulfillment', + 'compensation' => 'release_inventory_action', + ], + ], + [ + 'id' => 'payment_processing', + 'name' => 'Payment Gateway', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Payment: processing order payment through secure gateway', + 'retry_attempts' => 3, + 'retry_delay' => '30s', + 'compensation' => 'refund_payment_action', + ], + ], + [ + 'id' => 'shipment_creation', + 'name' => 'Shipping Coordination', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Shipping: creating shipment and generating tracking info', + ], + ], + [ + 'id' => 'customer_notification', + 'name' => 'Customer Communication', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Notification: sending order confirmation and tracking details', + 'async' => true, + 'channels' => ['email', 'sms'], + ], + ], + ], + 'transitions' => [ + ['from' => 'validate_order', 'to' => 'fraud_check', 'condition' => 'order.valid === true'], + ['from' => 'fraud_check', 'to' => 'inventory_reservation', 'condition' => 'fraud.risk < 0.7'], + ['from' => 'inventory_reservation', 'to' => 'payment_processing'], + ['from' => 'payment_processing', 'to' => 'shipment_creation', 'condition' => 'payment.success === true'], + ['from' => 'shipment_creation', 'to' => 'customer_notification'], + ], + 'error_handling' => [ + 'on_failure' => 'compensate_and_notify', + 'notification_channels' => ['email', 'slack', 'webhook'], + ], + ]; + + $context = [ + 'order' => [ + 'id' => 'ORD-COMPLEX-001', + 'customer_email' => 'test@ecommerce.com', + 'total' => 1500.00, + 'items' => [ + ['sku' => 'PREMIUM-ITEM', 'quantity' => 1, 'price' => 1500.00], + ], + ], + ]; + + $workflowId = $this->engine->start('ecommerce-complex', $definition, $context); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance)->not()->toBeNull(); + expect($instance->getContext()->getData()['order']['id'])->toBe('ORD-COMPLEX-001'); +}); + +test('e-commerce order processing workflow - high value order scenarios', function () { + $definition = [ + 'name' => 'High Value Order Processing', + 'version' => '2.0', + 'steps' => [ + [ + 'id' => 'order_validation', + 'name' => 'Enhanced Order Validation', + 'action' => 'log', + 'parameters' => [ + 'message' => 'High-value order validation with enhanced security checks', + 'security_level' => 'enhanced', + ], + ], + [ + 'id' => 'fraud_analysis', + 'name' => 'Advanced Fraud Analysis', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Running advanced fraud detection for high-value transaction', + 'analysis_level' => 'advanced', + 'manual_review_threshold' => 50000, + ], + ], + [ + 'id' => 'executive_approval', + 'name' => 'Executive Approval Gate', + 'action' => 'log', + 'parameters' => [ + 'message' => 'High-value order requiring executive approval', + 'approval_required' => true, + 'timeout' => '24h', + ], + ], + ], + 'transitions' => [ + ['from' => 'order_validation', 'to' => 'fraud_analysis'], + ['from' => 'fraud_analysis', 'to' => 'executive_approval', 'condition' => 'order.total > 50000'], + ], + ]; + + $highValueContext = [ + 'order' => [ + 'id' => 'ORD-HIGH-VALUE-001', + 'customer_email' => 'enterprise@bigcompany.com', + 'total' => 75000.00, + 'items' => [ + ['sku' => 'ENTERPRISE-LICENSE', 'quantity' => 1, 'price' => 75000.00], + ], + 'approval_level' => 'executive', + ], + ]; + + $workflowId = $this->engine->start('high-value-order', $definition, $highValueContext); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance->getContext()->getData()['order']['total'])->toBe(75000); +}); + +test('e-commerce order processing workflow - error handling scenarios', function () { + $definition = [ + 'name' => 'Order Processing with Error Handling', + 'version' => '2.0', + 'steps' => [ + [ + 'id' => 'validate_order', + 'name' => 'Order Validation', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Validating order - simulating validation failure scenario', + ], + ], + [ + 'id' => 'error_notification', + 'name' => 'Error Notification', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Sending error notification to customer and operations team', + ], + ], + [ + 'id' => 'cleanup_resources', + 'name' => 'Resource Cleanup', + 'action' => 'log', + 'parameters' => [ + 'message' => 'Cleaning up any allocated resources due to order failure', + ], + ], + ], + 'transitions' => [ + ['from' => 'validate_order', 'to' => 'error_notification'], + ['from' => 'error_notification', 'to' => 'cleanup_resources'], + ], + 'error_handling' => [ + 'strategy' => 'compensate_and_retry', + 'max_retries' => 3, + 'retry_delay' => '5m', + 'compensation_actions' => [ + 'release_inventory', + 'cancel_payment_authorization', + 'notify_customer', + ], + ], + ]; + + $failedOrderContext = [ + 'order' => [ + 'id' => 'ORD-FAILED-001', + 'customer_email' => 'test@failed-order.com', + 'total' => 0, // Invalid total to trigger failure + 'items' => [], + 'error_scenario' => true, + ], + ]; + + $workflowId = $this->engine->start('failed-order', $definition, $failedOrderContext); + + expect($workflowId)->not()->toBeEmpty(); + + $instance = $this->engine->getInstance($workflowId); + expect($instance->getContext()->getData()['order']['error_scenario'])->toBe(true); +}); diff --git a/packages/workflow-engine-core/tests/Support/InMemoryStorage.php b/packages/workflow-engine-core/tests/Support/InMemoryStorage.php new file mode 100644 index 0000000..9dc3cbf --- /dev/null +++ b/packages/workflow-engine-core/tests/Support/InMemoryStorage.php @@ -0,0 +1,65 @@ +instances[$instance->getId()] = $instance; + } + + public function load(string $id): WorkflowInstance + { + if (! isset($this->instances[$id])) { + throw new \InvalidArgumentException("Workflow instance not found: {$id}"); + } + + return $this->instances[$id]; + } + + public function findInstances(array $criteria = []): array + { + if (empty($criteria)) { + return array_values($this->instances); + } + + return array_filter($this->instances, function ($instance) use ($criteria) { + foreach ($criteria as $key => $value) { + // Simple implementation for basic filtering + if ($key === 'state' && $instance->getState()->value !== $value) { + return false; + } + } + + return true; + }); + } + + public function delete(string $id): void + { + unset($this->instances[$id]); + } + + public function exists(string $id): bool + { + return isset($this->instances[$id]); + } + + public function updateState(string $id, array $updates): void + { + if (! isset($this->instances[$id])) { + throw new \InvalidArgumentException("Workflow instance not found: {$id}"); + } + + // Simple update implementation + // In a real implementation, this would update specific fields + $instance = $this->instances[$id]; + $this->instances[$id] = $instance; + } +} diff --git a/packages/workflow-engine-core/tests/TestCase.php b/packages/workflow-engine-core/tests/TestCase.php new file mode 100644 index 0000000..6583592 --- /dev/null +++ b/packages/workflow-engine-core/tests/TestCase.php @@ -0,0 +1,60 @@ + 'SolutionForest\\WorkflowMastery\\Database\\Factories\\'.class_basename($modelName).'Factory' + ); + + $this->setUpDatabase(); + } + + protected function getPackageProviders($app): array + { + return [ + LaravelWorkflowEngineServiceProvider::class, + ]; + } + + public function getEnvironmentSetUp($app): void + { + config()->set('database.default', 'testing'); + config()->set('database.connections.testing', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + + protected function setUpDatabase(): void + { + Schema::create('workflow_instances', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('definition_name'); + $table->string('definition_version'); + $table->json('definition_data'); + $table->string('state'); + $table->json('data'); + $table->string('current_step_id')->nullable(); + $table->json('completed_steps'); + $table->json('failed_steps'); + $table->text('error_message')->nullable(); + $table->timestamps(); + + $table->index('state'); + $table->index('definition_name'); + }); + } +} diff --git a/packages/workflow-engine-core/tests/Unit/ActionTest.php b/packages/workflow-engine-core/tests/Unit/ActionTest.php new file mode 100644 index 0000000..ee6ae9a --- /dev/null +++ b/packages/workflow-engine-core/tests/Unit/ActionTest.php @@ -0,0 +1,52 @@ + 'Hello {{name}}']); + $context = new WorkflowContext('test-workflow', 'test-step', ['name' => 'John']); + + $result = $action->execute($context); + + expect($result)->toBeInstanceOf(ActionResult::class); + expect($result->isSuccess())->toBeTrue(); + expect($result->getErrorMessage())->toBeNull(); +}); + +test('delay action can execute', function () { + $action = new DelayAction(['seconds' => 1]); + $context = new WorkflowContext('test-workflow', 'test-step'); + + $start = microtime(true); + $result = $action->execute($context); + $end = microtime(true); + + expect($result)->toBeInstanceOf(ActionResult::class); + expect($result->isSuccess())->toBeTrue(); + expect($result->getErrorMessage())->toBeNull(); + + // Check that at least 1 second passed (with some tolerance) + expect($end - $start)->toBeGreaterThanOrEqual(0.9); +}); + +test('log action handles invalid template', function () { + $action = new LogAction(['message' => 'Hello {{invalid_variable}}']); + $context = new WorkflowContext('test-workflow', 'test-step', ['name' => 'John']); + + $result = $action->execute($context); + + expect($result->isSuccess())->toBeTrue(); +}); + +test('delay action handles invalid seconds', function () { + $action = new DelayAction(['seconds' => 'invalid']); + $context = new WorkflowContext('test-workflow', 'test-step'); + + $result = $action->execute($context); + + expect($result->isSuccess())->toBeFalse(); + expect($result->getErrorMessage())->toContain('Invalid delay seconds'); +}); diff --git a/packages/workflow-engine-core/tests/Unit/HelpersTest.php b/packages/workflow-engine-core/tests/Unit/HelpersTest.php new file mode 100644 index 0000000..0891254 --- /dev/null +++ b/packages/workflow-engine-core/tests/Unit/HelpersTest.php @@ -0,0 +1,68 @@ +toBeInstanceOf(WorkflowEngine::class); +}); + +test('start workflow helper works', function () { + $definition = [ + 'name' => 'Helper Test Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Hello from helper'], + ], + ], + ]; + + $workflowId = start_workflow('helper-test', $definition); + + expect($workflowId)->not->toBeEmpty(); + expect($workflowId)->toBe('helper-test'); +}); + +test('get workflow helper works', function () { + $definition = [ + 'name' => 'Helper Test Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Hello from helper'], + ], + ], + ]; + + $workflowId = start_workflow('helper-test-get', $definition); + $instance = get_workflow($workflowId); + + expect($instance->getId())->toBe($workflowId); + expect($instance->getName())->toBe('Helper Test Workflow'); +}); + +test('cancel workflow helper works', function () { + $definition = [ + 'name' => 'Helper Test Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Hello from helper'], + ], + ], + ]; + + $workflowId = start_workflow('helper-test-cancel', $definition); + cancel_workflow($workflowId, 'Test cancellation'); + + $instance = get_workflow($workflowId); + expect($instance->getState()->value)->toBe('cancelled'); +}); diff --git a/packages/workflow-engine-core/tests/Unit/PHP83FeaturesTest.php b/packages/workflow-engine-core/tests/Unit/PHP83FeaturesTest.php new file mode 100644 index 0000000..c6749bc --- /dev/null +++ b/packages/workflow-engine-core/tests/Unit/PHP83FeaturesTest.php @@ -0,0 +1,158 @@ +color())->toBe('blue'); + expect($state->icon())->toBe('▶️'); + expect($state->label())->toBe('Running'); + expect($state->canTransitionTo(WorkflowState::COMPLETED))->toBeTrue(); + expect($state->canTransitionTo(WorkflowState::PENDING))->toBeFalse(); + }); + + it('can create workflow with fluent builder API', function () { + $workflow = WorkflowBuilder::create('test-workflow') + ->description('Test workflow with fluent API') + ->version('2.0') + ->startWith(LogAction::class, ['message' => 'Starting workflow']) + ->then(DelayAction::class, ['seconds' => 1]) + ->email( + template: 'test', + to: '{{ user.email }}', + subject: 'Test Email' + ) + ->withMetadata(['created_by' => 'test']) + ->build(); + + expect($workflow->getName())->toBe('test-workflow'); + expect($workflow->getVersion())->toBe('2.0'); + expect($workflow->getMetadata())->toHaveKey('description'); + expect($workflow->getMetadata())->toHaveKey('created_by'); + expect($workflow->getSteps())->toHaveCount(3); + }); + + it('can use conditional workflow building', function () { + $workflow = WorkflowBuilder::create('conditional-test') + ->startWith(LogAction::class, ['message' => 'Start']) + ->when('user.premium', function ($builder) { + $builder->then(LogAction::class, ['message' => 'Premium user step']); + }) + ->then(LogAction::class, ['message' => 'Final step']) + ->build(); + + expect($workflow->getSteps())->toHaveCount(3); + }); + + it('can create quick template workflows', function () { + $builder = WorkflowBuilder::quick()->userOnboarding(); + $workflow = $builder->build(); + + expect($workflow->getName())->toBe('user-onboarding'); + expect($workflow->getSteps())->not()->toBeEmpty(); + }); + + it('validates state transitions correctly', function () { + // Test valid transitions + expect(WorkflowState::PENDING->canTransitionTo(WorkflowState::RUNNING))->toBeTrue(); + expect(WorkflowState::RUNNING->canTransitionTo(WorkflowState::COMPLETED))->toBeTrue(); + expect(WorkflowState::RUNNING->canTransitionTo(WorkflowState::FAILED))->toBeTrue(); + + // Test invalid transitions + expect(WorkflowState::COMPLETED->canTransitionTo(WorkflowState::RUNNING))->toBeFalse(); + expect(WorkflowState::FAILED->canTransitionTo(WorkflowState::RUNNING))->toBeFalse(); + expect(WorkflowState::CANCELLED->canTransitionTo(WorkflowState::RUNNING))->toBeFalse(); + }); + + it('provides UI-friendly state information', function () { + $testCases = [ + [WorkflowState::PENDING, 'gray', '⏳', 'Pending'], + [WorkflowState::RUNNING, 'blue', '▶️', 'Running'], + [WorkflowState::COMPLETED, 'green', '✅', 'Completed'], + [WorkflowState::FAILED, 'red', '❌', 'Failed'], + ]; + + foreach ($testCases as [$state, $expectedColor, $expectedIcon, $expectedLabel]) { + expect($state->color())->toBe($expectedColor); + expect($state->icon())->toBe($expectedIcon); + expect($state->label())->toBe($expectedLabel); + } + }); + +}); + +describe('Simplified Learning Curve', function () { + + it('can create workflow with common patterns using helper methods', function () { + $workflow = WorkflowBuilder::create('helper-test') + ->email( + template: 'welcome', + to: 'user@example.com', + subject: 'Welcome!' + ) + ->delay(minutes: 5) + ->http( + url: 'https://api.example.com/webhook', + method: 'POST', + data: ['event' => 'user_registered'] + ) + ->condition('user.verified') + ->build(); + + $steps = array_values($workflow->getSteps()); // Convert to numeric array + expect($steps)->toHaveCount(4); + + // Check email step + expect($steps[0]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\EmailAction'); + expect($steps[0]->getConfig()['template'])->toBe('welcome'); + + // Check delay step + expect($steps[1]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\DelayAction'); + + // Check HTTP step + expect($steps[2]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\HttpAction'); + expect($steps[2]->getConfig()['method'])->toBe('POST'); + + // Check condition step + expect($steps[3]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\ConditionAction'); + expect($steps[3]->getConfig()['condition'])->toBe('user.verified'); + }); + + it('provides quick workflow templates', function () { + $templates = ['userOnboarding', 'orderProcessing', 'documentApproval']; + + foreach ($templates as $template) { + $workflow = WorkflowBuilder::quick()->$template()->build(); + expect($workflow->getSteps())->not()->toBeEmpty(); + expect($workflow->getMetadata()['description'])->not()->toBeEmpty(); + } + }); + + it('can use named arguments for better readability', function () { + // This test demonstrates the improved API readability + // The actual testing is implicit in the successful execution + $workflow = WorkflowBuilder::create(name: 'named-args-test') + ->description(description: 'Testing named arguments') + ->version(version: '1.0') + ->email( + template: 'test', + to: 'test@example.com', + subject: 'Test Subject', + data: ['key' => 'value'] + ) + ->delay( + minutes: 5 + ) + ->build(); + + expect($workflow->getName())->toBe('named-args-test'); + expect($workflow->getVersion())->toBe('1.0'); + }); + +}); diff --git a/packages/workflow-engine-core/tests/Unit/WorkflowEngineTest.php b/packages/workflow-engine-core/tests/Unit/WorkflowEngineTest.php new file mode 100644 index 0000000..b8fc9bb --- /dev/null +++ b/packages/workflow-engine-core/tests/Unit/WorkflowEngineTest.php @@ -0,0 +1,207 @@ +engine = app(WorkflowEngine::class); +}); + +test('it can start a workflow', function () { + $definition = [ + 'name' => 'Test Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Hello World'], + ], + ], + ]; + + $workflowId = $this->engine->start('test-workflow', $definition); + + expect($workflowId)->not->toBeEmpty(); + + // Verify the workflow instance was created + $instance = $this->engine->getWorkflow($workflowId); + expect($instance)->toBeInstanceOf(WorkflowInstance::class); + expect($instance->getState())->toBe(WorkflowState::COMPLETED); // Log action completes immediately + expect($instance->getName())->toBe('Test Workflow'); +}); + +test('it can start a workflow with context', function () { + Event::fake(); + + $definition = [ + 'name' => 'Test Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Hello {{name}}'], + ], + ], + ]; + + $context = ['name' => 'John']; + $workflowId = $this->engine->start('test-workflow', $definition, $context); + + $instance = $this->engine->getWorkflow($workflowId); + $workflowData = $instance->getContext()->getData(); + + // Should contain original context plus any data added by actions + expect($workflowData['name'])->toBe('John'); + expect($workflowData)->toHaveKey('logged_message'); // Added by LogAction + expect($workflowData)->toHaveKey('logged_at'); // Added by LogAction +}); + +test('it can resume a paused workflow', function () { + Event::fake(); + + // Create a workflow with multiple steps + $definition = [ + 'name' => 'Test Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Hello World'], + ], + [ + 'id' => 'step2', + 'name' => 'Second Step', + 'action' => 'log', + 'parameters' => ['message' => 'Second step'], + ], + ], + ]; + + $workflowId = $this->engine->start('test-workflow', $definition); + + // Manually pause it + $storage = app(StorageAdapter::class); + $instance = $storage->load($workflowId); + $instance->setState(WorkflowState::PAUSED); + $storage->save($instance); + + // Resume it + $this->engine->resume($workflowId); + + $instance = $this->engine->getWorkflow($workflowId); + // After resume, it should be completed since we have simple log actions + expect($instance->getState())->toBe(WorkflowState::COMPLETED); +}); + +test('it can cancel a workflow', function () { + $definition = [ + 'name' => 'Test Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Hello World'], + ], + ], + ]; + + $workflowId = $this->engine->start('test-workflow', $definition); + $this->engine->cancel($workflowId, 'User cancelled'); + + $instance = $this->engine->getWorkflow($workflowId); + expect($instance->getState())->toBe(WorkflowState::CANCELLED); +}); + +test('it can get workflow status', function () { + $definition = [ + 'name' => 'Test Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Hello World'], + ], + ], + ]; + + $workflowId = $this->engine->start('test-workflow', $definition); + $status = $this->engine->getStatus($workflowId); + + expect($status)->toBeArray(); + expect($status['state'])->toBe(WorkflowState::COMPLETED->value); + expect($status['name'])->toBe('Test Workflow'); + expect($status)->toHaveKey('current_step'); + expect($status)->toHaveKey('progress'); +}); + +test('it throws exception for invalid workflow definition', function () { + $invalidDefinition = [ + 'steps' => [], + ]; + + $this->engine->start('test-workflow', $invalidDefinition); +})->throws(InvalidWorkflowDefinitionException::class, 'Required field \'name\' is missing from workflow definition'); + +test('it throws exception for nonexistent workflow', function () { + $this->engine->getWorkflow('nonexistent'); +})->throws(WorkflowInstanceNotFoundException::class, 'Workflow instance \'nonexistent\' was not found'); + +test('it can list workflows', function () { + $definition = [ + 'name' => 'Test Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Hello World'], + ], + ], + ]; + + $workflowId1 = $this->engine->start('test-workflow-1', $definition); + $workflowId2 = $this->engine->start('test-workflow-2', $definition); + + $workflows = $this->engine->listWorkflows(); + + expect($workflows)->toHaveCount(2); + expect(array_column($workflows, 'workflow_id'))->toContain($workflowId1); + expect(array_column($workflows, 'workflow_id'))->toContain($workflowId2); +}); + +test('it can filter workflows by state', function () { + $definition = [ + 'name' => 'Test Workflow', + 'steps' => [ + [ + 'id' => 'step1', + 'name' => 'First Step', + 'action' => 'log', + 'parameters' => ['message' => 'Hello World'], + ], + ], + ]; + + $completedId = $this->engine->start('completed-workflow', $definition); + $cancelledId = $this->engine->start('cancelled-workflow', $definition); + + $this->engine->cancel($cancelledId); + + $completedWorkflows = $this->engine->listWorkflows(['state' => WorkflowState::COMPLETED]); + $cancelledWorkflows = $this->engine->listWorkflows(['state' => WorkflowState::CANCELLED]); + + expect($completedWorkflows)->toHaveCount(1); + expect($cancelledWorkflows)->toHaveCount(1); + expect($completedWorkflows[0]['workflow_id'])->toBe($completedId); + expect($cancelledWorkflows[0]['workflow_id'])->toBe($cancelledId); +}); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cdf90f7..c182016 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5,21 +5,3 @@ parameters: identifier: larastan.noEnvCallsOutsideOfConfig count: 11 path: config/workflow-engine.php - - - - message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' - identifier: larastan.noEnvCallsOutsideOfConfig - count: 11 - path: config/workflow-mastery.php - - - - message: '#^Match arm comparison between ''\<\='' and ''\<\='' is always true\.$#' - identifier: match.alwaysTrue - count: 1 - path: src/Core/Step.php - - - - message: '#^Match arm comparison between ''\<\='' and ''\<\='' is always true\.$#' - identifier: match.alwaysTrue - count: 1 - path: src/Core/WorkflowDefinition.php diff --git a/src/Commands/LaravelWorkflowEngineCommand.php b/src/Commands/LaravelWorkflowEngineCommand.php index 0999b1c..025906d 100644 --- a/src/Commands/LaravelWorkflowEngineCommand.php +++ b/src/Commands/LaravelWorkflowEngineCommand.php @@ -1,14 +1,14 @@ description('Modern user onboarding with PHP 8.3+ features') - ->version('2.0') - - // Email step with template variables - ->email( - template: 'welcome', - to: '{{ user.email }}', - subject: 'Welcome to {{ app.name }}!', - data: ['welcome_bonus' => 50] - ) - - // Delay with named arguments - ->delay(minutes: 5) - - // HTTP request to external service - ->http( - url: 'https://api.analytics.com/track', - method: 'POST', - data: [ - 'event' => 'user_registered', - 'user_id' => '{{ user.id }}', - 'timestamp' => '{{ now() }}', - ] - ) - - // Conditional logic - ->when('user.premium', function ($builder) { - $builder->email( - template: 'premium-welcome', - to: '{{ user.email }}', - subject: 'Welcome to Premium!' - ); - }) - - ->build(); - - // Start with named arguments - $engine = app(\SolutionForest\WorkflowMastery\Core\WorkflowEngine::class); - - return $engine->start( - workflowId: 'onboarding-'.uniqid(), - definition: $workflow->toArray(), - context: ['user' => ['id' => 123, 'email' => 'user@example.com', 'premium' => true]] - ); - } - - /** - * Example 2: Quick templates for common patterns - DISABLED (requires implementation) - */ - public function quickTemplateExample(): string - { - // Note: This example is disabled as quick templates would need additional implementation - // For now, use the WorkflowBuilder directly for complex workflows - throw new \BadMethodCallException('Quick templates are not yet implemented. Use WorkflowBuilder directly.'); - } - - /** - * Example 3: Sequential workflow using SimpleWorkflow instance - */ - public function sequentialExample(): string - { - // Create SimpleWorkflow instance using Laravel's service container - $storage = app(\SolutionForest\WorkflowMastery\Contracts\StorageAdapter::class); - $eventDispatcher = app(\Illuminate\Contracts\Events\Dispatcher::class); - $simple = new SimpleWorkflow($storage, $eventDispatcher); - - return $simple->sequential( - name: 'order-fulfillment', - actions: [ - ValidateOrderAction::class, - ChargePaymentAction::class, - UpdateInventoryAction::class, - ShipOrderAction::class, - ], - context: ['order_id' => 12345] - ); - } - - /** - * Example 4: Conditional workflow - DISABLED (requires implementation) - */ - public function conditionalExample(): string - { - // Note: Conditional workflows need more sophisticated implementation - // For now, use WorkflowBuilder with conditional steps - throw new \BadMethodCallException('Conditional workflows are not yet implemented in SimpleWorkflow. Use WorkflowBuilder directly.'); - } - - /** - * Example 5: Single action execution using SimpleWorkflow - */ - public function singleActionExample(): string - { - // Create SimpleWorkflow instance using Laravel's service container - $storage = app(\SolutionForest\WorkflowMastery\Contracts\StorageAdapter::class); - $eventDispatcher = app(\Illuminate\Contracts\Events\Dispatcher::class); - $simple = new SimpleWorkflow($storage, $eventDispatcher); - - return $simple->runAction( - actionClass: SendEmailAction::class, - context: [ - 'to' => 'admin@example.com', - 'subject' => 'Test Email', - 'body' => 'This is a test email sent via workflow.', - ] - ); - } -} - -/** - * Example action using PHP 8.3+ attributes - */ -#[WorkflowStep( - id: 'validate_order', - name: 'Validate Order', - description: 'Validates order data and checks inventory' -)] -#[Timeout(seconds: 30)] -#[Retry(attempts: 3, backoff: 'exponential')] -#[Condition('order.amount > 0')] -#[Condition('order.items is not empty')] -class ValidateOrderAction extends BaseAction -{ - public function getName(): string - { - return 'Validate Order'; - } - - public function getDescription(): string - { - return 'Validates order data and checks inventory availability'; - } - - protected function doExecute(WorkflowContext $context): ActionResult - { - $orderData = $context->getData()['order'] ?? []; - - // Validation logic using PHP 8.3+ match expressions - $validationResult = match (true) { - empty($orderData) => ['valid' => false, 'error' => 'Order data is missing'], - ($orderData['amount'] ?? 0) <= 0 => ['valid' => false, 'error' => 'Invalid order amount'], - empty($orderData['items']) => ['valid' => false, 'error' => 'No items in order'], - default => ['valid' => true, 'error' => null] - }; - - if (! $validationResult['valid']) { - return ActionResult::failure($validationResult['error']); - } - - return ActionResult::success([ - 'validated_order' => $orderData, - 'validation_timestamp' => now()->toISOString(), - ]); - } -} - -// Additional example actions -class ChargePaymentAction extends BaseAction -{ - public function getName(): string - { - return 'Charge Payment'; - } - - public function getDescription(): string - { - return 'Charges the customer payment method'; - } - - protected function doExecute(WorkflowContext $context): ActionResult - { - // Implementation would go here - return ActionResult::success(['payment_status' => 'charged']); - } -} - -class UpdateInventoryAction extends BaseAction -{ - public function getName(): string - { - return 'Update Inventory'; - } - - public function getDescription(): string - { - return 'Updates inventory levels'; - } - - protected function doExecute(WorkflowContext $context): ActionResult - { - return ActionResult::success(['inventory_updated' => true]); - } -} - -class ShipOrderAction extends BaseAction -{ - public function getName(): string - { - return 'Ship Order'; - } - - public function getDescription(): string - { - return 'Initiates order shipping'; - } - - protected function doExecute(WorkflowContext $context): ActionResult - { - return ActionResult::success(['tracking_number' => 'TRK'.rand(100000, 999999)]); - } -} - -class SendEmailAction extends BaseAction -{ - public function getName(): string - { - return 'Send Email'; - } - - public function getDescription(): string - { - return 'Sends an email'; - } - - protected function doExecute(WorkflowContext $context): ActionResult - { - return ActionResult::success(['email_sent' => true]); - } -} - -// Additional example actions for conditional workflow -class ValidatePaymentAction extends BaseAction -{ - public function getName(): string - { - return 'Validate Payment'; - } - - public function getDescription(): string - { - return 'Validates payment information'; - } - - protected function doExecute(WorkflowContext $context): ActionResult - { - return ActionResult::success(['payment_valid' => true]); - } -} - -class ChargeCardAction extends BaseAction -{ - public function getName(): string - { - return 'Charge Card'; - } - - public function getDescription(): string - { - return 'Charges the credit card'; - } - - protected function doExecute(WorkflowContext $context): ActionResult - { - return ActionResult::success(['payment' => ['success' => true]]); - } -} - -class ConfirmOrderAction extends BaseAction -{ - public function getName(): string - { - return 'Confirm Order'; - } - - public function getDescription(): string - { - return 'Confirms the order'; - } - - protected function doExecute(WorkflowContext $context): ActionResult - { - return ActionResult::success(['order_confirmed' => true]); - } -} - -class SendReceiptAction extends BaseAction -{ - public function getName(): string - { - return 'Send Receipt'; - } - - public function getDescription(): string - { - return 'Sends payment receipt'; - } - - protected function doExecute(WorkflowContext $context): ActionResult - { - return ActionResult::success(['receipt_sent' => true]); - } -} - -class RetryPaymentAction extends BaseAction -{ - public function getName(): string - { - return 'Retry Payment'; - } - - public function getDescription(): string - { - return 'Retries failed payment'; - } - - protected function doExecute(WorkflowContext $context): ActionResult - { - return ActionResult::success(['payment_retried' => true]); - } -} diff --git a/src/Facades/LaravelWorkflowEngine.php b/src/Facades/LaravelWorkflowEngine.php deleted file mode 100644 index 854b5f0..0000000 --- a/src/Facades/LaravelWorkflowEngine.php +++ /dev/null @@ -1,16 +0,0 @@ -engine = $engine; - } - - /** - * Start a new workflow - */ - public function start(array|string $definition, array $initialData = []): WorkflowInstance - { - $workflowId = 'workflow-'.uniqid(); - $this->engine->start($workflowId, $definition, $initialData); - - return $this->engine->getWorkflow($workflowId); - } - - /** - * Resume an existing workflow - */ - public function resume(string $instanceId): WorkflowInstance - { - return $this->engine->resume($instanceId); - } - - /** - * Get workflow instance - */ - public function getInstance(string $instanceId): WorkflowInstance - { - return $this->engine->getInstance($instanceId); - } - - /** - * Get all workflow instances - */ - public function getInstances(array $filters = []): array - { - return $this->engine->getInstances($filters); - } - - /** - * Cancel a workflow - */ - public function cancel(string $instanceId): WorkflowInstance - { - return $this->engine->cancel($instanceId); - } - - /** - * Get the underlying engine - */ - public function getEngine(): WorkflowEngine - { - return $this->engine; - } -} diff --git a/src/Models/WorkflowInstance.php b/src/Models/WorkflowInstance.php new file mode 100644 index 0000000..83ea27d --- /dev/null +++ b/src/Models/WorkflowInstance.php @@ -0,0 +1,94 @@ + 'array', + 'data' => 'array', + 'completed_steps' => 'array', + 'failed_steps' => 'array', + 'state' => WorkflowState::class, + ]; + + public $incrementing = false; + + protected $keyType = 'string'; + + /** + * Convert this Eloquent model to a core WorkflowInstance + */ + public function toCoreInstance(): CoreWorkflowInstance + { + $definition = WorkflowDefinition::fromArray($this->definition_data); + + return CoreWorkflowInstance::fromArray([ + 'id' => $this->id, + 'state' => $this->state->value, + 'data' => $this->data, + 'current_step_id' => $this->current_step_id, + 'completed_steps' => $this->completed_steps, + 'failed_steps' => $this->failed_steps, + 'error_message' => $this->error_message, + 'created_at' => $this->created_at->toISOString(), + 'updated_at' => $this->updated_at->toISOString(), + ], $definition); + } + + /** + * Create an Eloquent model from a core WorkflowInstance + */ + public static function fromCoreInstance(CoreWorkflowInstance $instance): self + { + return new self([ + 'id' => $instance->getId(), + 'definition_name' => $instance->getDefinition()->getName(), + 'definition_version' => $instance->getDefinition()->getVersion(), + 'definition_data' => $instance->getDefinition()->toArray(), + 'state' => $instance->getState(), + 'data' => $instance->getData(), + 'current_step_id' => $instance->getCurrentStepId(), + 'completed_steps' => $instance->getCompletedSteps(), + 'failed_steps' => $instance->getFailedSteps(), + 'error_message' => $instance->getErrorMessage(), + 'created_at' => $instance->getCreatedAt(), + 'updated_at' => $instance->getUpdatedAt(), + ]); + } +} diff --git a/src/LaravelWorkflowEngineServiceProvider.php b/src/Providers/WorkflowEngineServiceProvider.php similarity index 71% rename from src/LaravelWorkflowEngineServiceProvider.php rename to src/Providers/WorkflowEngineServiceProvider.php index 5375457..5bab84b 100644 --- a/src/LaravelWorkflowEngineServiceProvider.php +++ b/src/Providers/WorkflowEngineServiceProvider.php @@ -1,17 +1,17 @@ name('workflow-mastery') - ->hasConfigFile('workflow-mastery') + ->name('workflow-engine') + ->hasConfigFile('workflow-engine') ->hasViews() ->hasMigration('create_workflow_instances_table') ->hasCommand(LaravelWorkflowEngineCommand::class); @@ -34,12 +34,12 @@ public function register(): void // Register storage adapter $this->app->singleton(StorageAdapter::class, function ($app): StorageAdapter { - $driver = config('workflow-mastery.storage.driver', 'database'); + $driver = config('workflow-engine.storage.driver', 'database'); return match ($driver) { 'database' => new DatabaseStorage( $app->make(DatabaseManager::class), - config('workflow-mastery.storage.database.table', 'workflow_instances') + config('workflow-engine.storage.database.table', 'workflow_instances') ), default => throw new \InvalidArgumentException("Unsupported storage driver: {$driver}") }; diff --git a/src/Storage/DatabaseStorage.php b/src/Storage/DatabaseStorage.php index 9eb8970..55076cb 100644 --- a/src/Storage/DatabaseStorage.php +++ b/src/Storage/DatabaseStorage.php @@ -1,12 +1,12 @@ toBeTrue(); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 8b80a2d..9531b24 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,5 @@ in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php index 6583592..4633eb8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,12 +1,12 @@ 'SolutionForest\\WorkflowMastery\\Database\\Factories\\'.class_basename($modelName).'Factory' + fn (string $modelName) => 'SolutionForest\\WorkflowEngine\\Laravel\\Database\\Factories\\'.class_basename($modelName).'Factory' ); $this->setUpDatabase(); @@ -24,7 +24,7 @@ protected function setUp(): void protected function getPackageProviders($app): array { return [ - LaravelWorkflowEngineServiceProvider::class, + WorkflowEngineServiceProvider::class, ]; } From 0acab8ddb0275a57f5ba7f650c56c4a76385565e Mon Sep 17 00:00:00 2001 From: lam0819 <68211972+lam0819@users.noreply.github.com> Date: Wed, 28 May 2025 17:54:05 +0000 Subject: [PATCH 2/6] Fix styling --- packages/workflow-engine-core/src/Events/WorkflowCancelled.php | 2 -- .../workflow-engine-core/src/Events/WorkflowCompletedEvent.php | 1 - .../workflow-engine-core/src/Events/WorkflowFailedEvent.php | 1 - src/Models/WorkflowInstance.php | 1 - src/Providers/WorkflowEngineServiceProvider.php | 2 +- 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/workflow-engine-core/src/Events/WorkflowCancelled.php b/packages/workflow-engine-core/src/Events/WorkflowCancelled.php index ff90cb4..c9e8b06 100644 --- a/packages/workflow-engine-core/src/Events/WorkflowCancelled.php +++ b/packages/workflow-engine-core/src/Events/WorkflowCancelled.php @@ -2,10 +2,8 @@ namespace SolutionForest\WorkflowEngine\Events; - class WorkflowCancelled { - public function __construct( public readonly string $workflowId, public readonly string $name, diff --git a/packages/workflow-engine-core/src/Events/WorkflowCompletedEvent.php b/packages/workflow-engine-core/src/Events/WorkflowCompletedEvent.php index e132b5b..42014d2 100644 --- a/packages/workflow-engine-core/src/Events/WorkflowCompletedEvent.php +++ b/packages/workflow-engine-core/src/Events/WorkflowCompletedEvent.php @@ -6,7 +6,6 @@ class WorkflowCompletedEvent { - public WorkflowInstance $instance; public function __construct(WorkflowInstance $instance) diff --git a/packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php b/packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php index 0c26cee..0598d69 100644 --- a/packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php +++ b/packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php @@ -6,7 +6,6 @@ class WorkflowFailedEvent { - public WorkflowInstance $instance; public \Exception $exception; diff --git a/src/Models/WorkflowInstance.php b/src/Models/WorkflowInstance.php index 83ea27d..80b4dc9 100644 --- a/src/Models/WorkflowInstance.php +++ b/src/Models/WorkflowInstance.php @@ -3,7 +3,6 @@ namespace SolutionForest\WorkflowEngine\Laravel\Models; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Casts\Attribute; use SolutionForest\WorkflowEngine\Core\WorkflowDefinition; use SolutionForest\WorkflowEngine\Core\WorkflowInstance as CoreWorkflowInstance; use SolutionForest\WorkflowEngine\Core\WorkflowState; diff --git a/src/Providers/WorkflowEngineServiceProvider.php b/src/Providers/WorkflowEngineServiceProvider.php index 5bab84b..7302e8a 100644 --- a/src/Providers/WorkflowEngineServiceProvider.php +++ b/src/Providers/WorkflowEngineServiceProvider.php @@ -4,9 +4,9 @@ use Illuminate\Contracts\Events\Dispatcher as EventDispatcher; use Illuminate\Database\DatabaseManager; -use SolutionForest\WorkflowEngine\Laravel\Commands\LaravelWorkflowEngineCommand; use SolutionForest\WorkflowEngine\Contracts\StorageAdapter; use SolutionForest\WorkflowEngine\Core\WorkflowEngine; +use SolutionForest\WorkflowEngine\Laravel\Commands\LaravelWorkflowEngineCommand; use SolutionForest\WorkflowEngine\Laravel\Storage\DatabaseStorage; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; From c5e51ebb9f2fadebb2e9830e4c00845de59ef242 Mon Sep 17 00:00:00 2001 From: alan Date: Thu, 29 May 2025 12:34:48 +0800 Subject: [PATCH 3/6] wip --- .gitignore | 2 + composer.json | 7 +- src/Adapters/LaravelEventDispatcher.php | 26 ++++ src/Adapters/LaravelLogger.php | 41 ++++++ src/Models/WorkflowInstance.php | 1 - .../WorkflowEngineServiceProvider.php | 19 ++- src/helpers.php | 56 ++++++++ .../ECommerce/CreateShipmentAction.php | 48 ------- tests/Actions/ECommerce/FraudCheckAction.php | 55 -------- .../NotificationAndCompensationActions.php | 123 ------------------ .../ECommerce/ProcessPaymentAction.php | 8 +- .../ECommerce/ReserveInventoryAction.php | 42 ------ .../Actions/ECommerce/ValidateOrderAction.php | 43 ------ tests/Integration/WorkflowIntegrationTest.php | 2 +- tests/RealWorld/CICDPipelineWorkflowTest.php | 4 +- .../DocumentApprovalWorkflowTest.php | 4 +- tests/RealWorld/ECommerceWorkflowTest.php | 4 +- tests/Support/InMemoryStorage.php | 4 +- tests/Unit/ActionTest.php | 8 +- tests/Unit/HelpersTest.php | 2 +- tests/Unit/PHP83FeaturesTest.php | 16 +-- tests/Unit/WorkflowEngineTest.php | 12 +- 22 files changed, 178 insertions(+), 349 deletions(-) create mode 100644 src/Adapters/LaravelEventDispatcher.php create mode 100644 src/Adapters/LaravelLogger.php create mode 100644 src/helpers.php delete mode 100644 tests/Actions/ECommerce/CreateShipmentAction.php delete mode 100644 tests/Actions/ECommerce/FraudCheckAction.php delete mode 100644 tests/Actions/ECommerce/NotificationAndCompensationActions.php delete mode 100644 tests/Actions/ECommerce/ReserveInventoryAction.php delete mode 100644 tests/Actions/ECommerce/ValidateOrderAction.php diff --git a/.gitignore b/.gitignore index ea915dc..edc5b09 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ phpstan.neon testbench.yaml # /docs /coverage + +/packages/** diff --git a/composer.json b/composer.json index 974939b..1dd107f 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } ], "require": { - "php": "^8.1", + "php": "^8.3", "solution-forest/workflow-engine-core": "*", "spatie/laravel-package-tools": "^1.16", "illuminate/contracts": "^10.0||^11.0||^12.0", @@ -49,7 +49,10 @@ "autoload": { "psr-4": { "SolutionForest\\WorkflowEngine\\Laravel\\": "src/" - } + }, + "files": [ + "src/helpers.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/src/Adapters/LaravelEventDispatcher.php b/src/Adapters/LaravelEventDispatcher.php new file mode 100644 index 0000000..fd8e563 --- /dev/null +++ b/src/Adapters/LaravelEventDispatcher.php @@ -0,0 +1,26 @@ +dispatcher->dispatch($event); + } +} diff --git a/src/Adapters/LaravelLogger.php b/src/Adapters/LaravelLogger.php new file mode 100644 index 0000000..396bc68 --- /dev/null +++ b/src/Adapters/LaravelLogger.php @@ -0,0 +1,41 @@ +logger->info($message, $context); + } + + public function error(string $message, array $context = []): void + { + $this->logger->error($message, $context); + } + + public function debug(string $message, array $context = []): void + { + $this->logger->debug($message, $context); + } + + public function warning(string $message, array $context = []): void + { + $this->logger->warning($message, $context); + } +} diff --git a/src/Models/WorkflowInstance.php b/src/Models/WorkflowInstance.php index 83ea27d..80b4dc9 100644 --- a/src/Models/WorkflowInstance.php +++ b/src/Models/WorkflowInstance.php @@ -3,7 +3,6 @@ namespace SolutionForest\WorkflowEngine\Laravel\Models; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Casts\Attribute; use SolutionForest\WorkflowEngine\Core\WorkflowDefinition; use SolutionForest\WorkflowEngine\Core\WorkflowInstance as CoreWorkflowInstance; use SolutionForest\WorkflowEngine\Core\WorkflowState; diff --git a/src/Providers/WorkflowEngineServiceProvider.php b/src/Providers/WorkflowEngineServiceProvider.php index 5bab84b..d59bdcb 100644 --- a/src/Providers/WorkflowEngineServiceProvider.php +++ b/src/Providers/WorkflowEngineServiceProvider.php @@ -2,11 +2,10 @@ namespace SolutionForest\WorkflowEngine\Laravel\Providers; -use Illuminate\Contracts\Events\Dispatcher as EventDispatcher; use Illuminate\Database\DatabaseManager; -use SolutionForest\WorkflowEngine\Laravel\Commands\LaravelWorkflowEngineCommand; use SolutionForest\WorkflowEngine\Contracts\StorageAdapter; use SolutionForest\WorkflowEngine\Core\WorkflowEngine; +use SolutionForest\WorkflowEngine\Laravel\Commands\LaravelWorkflowEngineCommand; use SolutionForest\WorkflowEngine\Laravel\Storage\DatabaseStorage; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -45,11 +44,25 @@ public function register(): void }; }); + // Register event dispatcher adapter + $this->app->singleton(\SolutionForest\WorkflowEngine\Contracts\EventDispatcher::class, function ($app) { + return new \SolutionForest\WorkflowEngine\Laravel\Adapters\LaravelEventDispatcher( + $app->make(\Illuminate\Contracts\Events\Dispatcher::class) + ); + }); + + // Register logger adapter + $this->app->singleton(\SolutionForest\WorkflowEngine\Contracts\Logger::class, function ($app) { + return new \SolutionForest\WorkflowEngine\Laravel\Adapters\LaravelLogger( + $app->make(\Illuminate\Log\LogManager::class) + ); + }); + // Register workflow engine $this->app->singleton(WorkflowEngine::class, function ($app): WorkflowEngine { return new WorkflowEngine( $app->make(StorageAdapter::class), - $app->make(EventDispatcher::class) + $app->make(\SolutionForest\WorkflowEngine\Contracts\EventDispatcher::class) ); }); diff --git a/src/helpers.php b/src/helpers.php new file mode 100644 index 0000000..366b288 --- /dev/null +++ b/src/helpers.php @@ -0,0 +1,56 @@ +start($id, $definition, $context); + } +} + +if (! function_exists('get_workflow')) { + /** + * Get a workflow instance by ID. + */ + function get_workflow(string $id): WorkflowInstance + { + return workflow()->getInstance($id); + } +} + +if (! function_exists('cancel_workflow')) { + /** + * Cancel a workflow. + */ + function cancel_workflow(string $id, string $reason = 'Cancelled'): void + { + workflow()->cancel($id, $reason); + } +} + +if (! function_exists('list_workflows')) { + /** + * List workflows. + */ + function list_workflows(array $filters = []): array + { + return workflow()->listWorkflows($filters); + } +} diff --git a/tests/Actions/ECommerce/CreateShipmentAction.php b/tests/Actions/ECommerce/CreateShipmentAction.php deleted file mode 100644 index 5b3a86b..0000000 --- a/tests/Actions/ECommerce/CreateShipmentAction.php +++ /dev/null @@ -1,48 +0,0 @@ -getData('order'); - - // Mock shipment creation - $shipmentId = 'ship_'.uniqid(); - $trackingNumber = 'TRK'.mt_rand(100000, 999999); - - $context->setData('shipment.id', $shipmentId); - $context->setData('shipment.tracking_number', $trackingNumber); - $context->setData('shipment.created', true); - - return new ActionResult( - success: true, - data: [ - 'shipment_id' => $shipmentId, - 'tracking_number' => $trackingNumber, - 'status' => 'created', - ] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order') && - $context->getData('payment.success') === true; - } - - public function getName(): string - { - return 'Create Shipment'; - } - - public function getDescription(): string - { - return 'Creates shipment and generates tracking number'; - } -} diff --git a/tests/Actions/ECommerce/FraudCheckAction.php b/tests/Actions/ECommerce/FraudCheckAction.php deleted file mode 100644 index 0dddf40..0000000 --- a/tests/Actions/ECommerce/FraudCheckAction.php +++ /dev/null @@ -1,55 +0,0 @@ -getData('order'); - - // Mock fraud detection logic - $riskScore = $this->calculateRiskScore($order); - $context->setData('fraud.risk', $riskScore); - - return new ActionResult( - success: true, - data: ['risk_score' => $riskScore, 'status' => $riskScore < 0.7 ? 'safe' : 'flagged'] - ); - } - - private function calculateRiskScore(array $order): float - { - // Simple mock risk calculation - $baseRisk = 0.1; - - if ($order['total'] > 10000) { - $baseRisk += 0.3; - } - - if ($order['total'] > 50000) { - $baseRisk += 0.4; - } - - return min($baseRisk, 1.0); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order') && $context->getData('order.valid') === true; - } - - public function getName(): string - { - return 'Fraud Check'; - } - - public function getDescription(): string - { - return 'Analyzes order for potential fraud indicators'; - } -} diff --git a/tests/Actions/ECommerce/NotificationAndCompensationActions.php b/tests/Actions/ECommerce/NotificationAndCompensationActions.php deleted file mode 100644 index c4ca6c3..0000000 --- a/tests/Actions/ECommerce/NotificationAndCompensationActions.php +++ /dev/null @@ -1,123 +0,0 @@ -getData('order'); - $shipment = $context->getData('shipment'); - - // Mock notification sending - $notificationId = 'notif_'.uniqid(); - - $context->setData('notification.id', $notificationId); - $context->setData('notification.sent', true); - $context->setData('notification.type', 'order_confirmation'); - - return new ActionResult( - success: true, - data: [ - 'notification_id' => $notificationId, - 'recipient' => $order['customer_email'] ?? 'customer@example.com', - 'tracking_number' => $shipment['tracking_number'] ?? null, - 'status' => 'sent', - ] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order') && - $context->getData('shipment.created') === true; - } - - public function getName(): string - { - return 'Send Order Confirmation'; - } - - public function getDescription(): string - { - return 'Sends order confirmation email to customer'; - } -} - -// Compensation Actions -class ReleaseInventoryAction implements WorkflowAction -{ - public function execute(WorkflowContext $context): ActionResult - { - $reservationId = $context->getData('inventory.reservation_id'); - - if ($reservationId) { - $context->setData('inventory.reserved', false); - $context->setData('inventory.released', true); - } - - return new ActionResult( - success: true, - data: ['reservation_id' => $reservationId, 'status' => 'released'] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('inventory.reservation_id'); - } - - public function getName(): string - { - return 'Release Inventory'; - } - - public function getDescription(): string - { - return 'Releases previously reserved inventory'; - } -} - -class RefundPaymentAction implements WorkflowAction -{ - public function execute(WorkflowContext $context): ActionResult - { - $paymentId = $context->getData('payment.id'); - $amount = $context->getData('payment.amount'); - - if ($paymentId) { - $refundId = 'ref_'.uniqid(); - $context->setData('refund.id', $refundId); - $context->setData('refund.amount', $amount); - $context->setData('refund.processed', true); - } - - return new ActionResult( - success: true, - data: [ - 'refund_id' => $refundId ?? null, - 'amount' => $amount, - 'status' => 'processed', - ] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('payment.id'); - } - - public function getName(): string - { - return 'Refund Payment'; - } - - public function getDescription(): string - { - return 'Processes payment refund'; - } -} diff --git a/tests/Actions/ECommerce/ProcessPaymentAction.php b/tests/Actions/ECommerce/ProcessPaymentAction.php index 7f34495..6cd7a0d 100644 --- a/tests/Actions/ECommerce/ProcessPaymentAction.php +++ b/tests/Actions/ECommerce/ProcessPaymentAction.php @@ -1,10 +1,10 @@ getData('order'); - - // Mock inventory reservation - $reservationId = 'res_'.uniqid(); - $context->setData('inventory.reservation_id', $reservationId); - $context->setData('inventory.reserved', true); - - return new ActionResult( - success: true, - data: ['reservation_id' => $reservationId, 'status' => 'reserved'] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order') && - $context->getData('order.valid') === true && - ($context->getData('fraud.risk') ?? 0) < 0.7; - } - - public function getName(): string - { - return 'Reserve Inventory'; - } - - public function getDescription(): string - { - return 'Reserves inventory items for the order'; - } -} diff --git a/tests/Actions/ECommerce/ValidateOrderAction.php b/tests/Actions/ECommerce/ValidateOrderAction.php deleted file mode 100644 index a3322f8..0000000 --- a/tests/Actions/ECommerce/ValidateOrderAction.php +++ /dev/null @@ -1,43 +0,0 @@ -getData('order'); - - // Mock validation logic - $isValid = isset($order['items']) && - count($order['items']) > 0 && - isset($order['total']) && - $order['total'] > 0; - - $context->setData('order.valid', $isValid); - - return new ActionResult( - success: $isValid, - data: ['validation_result' => $isValid ? 'passed' : 'failed'] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order'); - } - - public function getName(): string - { - return 'Validate Order'; - } - - public function getDescription(): string - { - return 'Validates order data including items and total amount'; - } -} diff --git a/tests/Integration/WorkflowIntegrationTest.php b/tests/Integration/WorkflowIntegrationTest.php index 2e79f93..cbac305 100644 --- a/tests/Integration/WorkflowIntegrationTest.php +++ b/tests/Integration/WorkflowIntegrationTest.php @@ -1,6 +1,6 @@ engine = app(WorkflowEngine::class); diff --git a/tests/RealWorld/DocumentApprovalWorkflowTest.php b/tests/RealWorld/DocumentApprovalWorkflowTest.php index 47df8e7..297bd2e 100644 --- a/tests/RealWorld/DocumentApprovalWorkflowTest.php +++ b/tests/RealWorld/DocumentApprovalWorkflowTest.php @@ -1,7 +1,7 @@ engine = app(WorkflowEngine::class); diff --git a/tests/RealWorld/ECommerceWorkflowTest.php b/tests/RealWorld/ECommerceWorkflowTest.php index 64becb6..99e281c 100644 --- a/tests/RealWorld/ECommerceWorkflowTest.php +++ b/tests/RealWorld/ECommerceWorkflowTest.php @@ -1,7 +1,7 @@ engine = app(WorkflowEngine::class); diff --git a/tests/Support/InMemoryStorage.php b/tests/Support/InMemoryStorage.php index 9dc3cbf..30804bd 100644 --- a/tests/Support/InMemoryStorage.php +++ b/tests/Support/InMemoryStorage.php @@ -2,8 +2,8 @@ namespace Tests\Support; -use SolutionForest\WorkflowMastery\Contracts\StorageAdapter; -use SolutionForest\WorkflowMastery\Core\WorkflowInstance; +use SolutionForest\WorkflowEngine\Contracts\StorageAdapter; +use SolutionForest\WorkflowEngine\Core\WorkflowInstance; class InMemoryStorage implements StorageAdapter { diff --git a/tests/Unit/ActionTest.php b/tests/Unit/ActionTest.php index ee6ae9a..83540af 100644 --- a/tests/Unit/ActionTest.php +++ b/tests/Unit/ActionTest.php @@ -1,9 +1,9 @@ 'Hello {{name}}']); diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php index 0891254..622e9c3 100644 --- a/tests/Unit/HelpersTest.php +++ b/tests/Unit/HelpersTest.php @@ -1,6 +1,6 @@ toHaveCount(4); // Check email step - expect($steps[0]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\EmailAction'); + expect($steps[0]->getActionClass())->toBe('SolutionForest\\WorkflowEngine\\Actions\\EmailAction'); expect($steps[0]->getConfig()['template'])->toBe('welcome'); // Check delay step - expect($steps[1]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\DelayAction'); + expect($steps[1]->getActionClass())->toBe('SolutionForest\\WorkflowEngine\\Actions\\DelayAction'); // Check HTTP step - expect($steps[2]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\HttpAction'); + expect($steps[2]->getActionClass())->toBe('SolutionForest\\WorkflowEngine\\Actions\\HttpAction'); expect($steps[2]->getConfig()['method'])->toBe('POST'); // Check condition step - expect($steps[3]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\ConditionAction'); + expect($steps[3]->getActionClass())->toBe('SolutionForest\\WorkflowEngine\\Actions\\ConditionAction'); expect($steps[3]->getConfig()['condition'])->toBe('user.verified'); }); diff --git a/tests/Unit/WorkflowEngineTest.php b/tests/Unit/WorkflowEngineTest.php index b8fc9bb..edac90c 100644 --- a/tests/Unit/WorkflowEngineTest.php +++ b/tests/Unit/WorkflowEngineTest.php @@ -1,12 +1,12 @@ engine = app(WorkflowEngine::class); From e670a286ad0a03b3671346290876e4da61d5abe1 Mon Sep 17 00:00:00 2001 From: alan Date: Thu, 29 May 2025 19:09:58 +0800 Subject: [PATCH 4/6] wip --- .github/ISSUE_TEMPLATE/config.yml | 6 +- CHANGELOG.md | 2 +- README.md | 8 +- composer.json | 18 +- docs/README.md | 6 +- docs/advanced-features.md | 8 +- docs/best-practices.md | 6 +- docs/getting-started.md | 2 +- docs/migration.md | 4 +- packages/workflow-engine-core/README.md | 65 -- packages/workflow-engine-core/composer.json | 49 -- .../workflow-engine-core/phpstan.neon.dist | 12 - .../workflow-engine-core/phpunit.xml.dist | 31 - .../src/Actions/BaseAction.php | 394 ------------ .../src/Actions/ConditionAction.php | 117 ---- .../src/Actions/DelayAction.php | 49 -- .../src/Actions/HttpAction.php | 114 ---- .../src/Actions/LogAction.php | 57 -- .../src/Attributes/Condition.php | 22 - .../src/Attributes/Retry.php | 24 - .../src/Attributes/Timeout.php | 27 - .../src/Attributes/WorkflowStep.php | 29 - .../src/Contracts/StorageAdapter.php | 38 -- .../src/Contracts/WorkflowAction.php | 166 ----- .../src/Core/ActionResolver.php | 336 ---------- .../src/Core/ActionResult.php | 412 ------------ .../src/Core/DefinitionParser.php | 556 ---------------- .../src/Core/Executor.php | 327 ---------- .../src/Core/StateManager.php | 326 ---------- .../workflow-engine-core/src/Core/Step.php | 271 -------- .../src/Core/WorkflowBuilder.php | 604 ------------------ .../src/Core/WorkflowContext.php | 252 -------- .../src/Core/WorkflowDefinition.php | 397 ------------ .../src/Core/WorkflowEngine.php | 322 ---------- .../src/Core/WorkflowInstance.php | 527 --------------- .../src/Core/WorkflowState.php | 374 ----------- .../src/Events/WorkflowCancelled.php | 14 - .../src/Events/WorkflowCompletedEvent.php | 16 - .../src/Events/WorkflowFailedEvent.php | 19 - .../src/Events/WorkflowStarted.php | 12 - .../Exceptions/ActionNotFoundException.php | 203 ------ .../InvalidWorkflowDefinitionException.php | 341 ---------- .../InvalidWorkflowStateException.php | 214 ------- .../src/Exceptions/StepExecutionException.php | 244 ------- .../src/Exceptions/WorkflowException.php | 153 ----- .../WorkflowInstanceNotFoundException.php | 168 ----- .../src/Support/SimpleWorkflow.php | 206 ------ .../workflow-engine-core/src/Support/Uuid.php | 105 --- packages/workflow-engine-core/src/helpers.php | 65 -- .../ECommerce/CreateShipmentAction.php | 48 -- .../Actions/ECommerce/FraudCheckAction.php | 55 -- .../NotificationAndCompensationActions.php | 123 ---- .../ECommerce/ProcessPaymentAction.php | 54 -- .../ECommerce/ReserveInventoryAction.php | 42 -- .../Actions/ECommerce/ValidateOrderAction.php | 43 -- .../workflow-engine-core/tests/ArchTest.php | 5 - .../tests/ExampleTest.php | 5 - .../Integration/WorkflowIntegrationTest.php | 255 -------- packages/workflow-engine-core/tests/Pest.php | 5 - .../RealWorld/CICDPipelineWorkflowTest.php | 427 ------------- .../DocumentApprovalWorkflowTest.php | 373 ----------- .../tests/RealWorld/ECommerceWorkflowTest.php | 328 ---------- .../tests/Support/InMemoryStorage.php | 65 -- .../workflow-engine-core/tests/TestCase.php | 60 -- .../tests/Unit/ActionTest.php | 52 -- .../tests/Unit/HelpersTest.php | 68 -- .../tests/Unit/PHP83FeaturesTest.php | 158 ----- .../tests/Unit/WorkflowEngineTest.php | 207 ------ src/Events/StepCompletedEvent.php | 23 - src/Events/StepFailedEvent.php | 26 - src/Events/WorkflowCancelled.php | 17 - src/Events/WorkflowCompleted.php | 17 - src/Events/WorkflowCompletedEvent.php | 19 - src/Events/WorkflowFailed.php | 18 - src/Events/WorkflowFailedEvent.php | 22 - src/Events/WorkflowStarted.php | 17 - src/Events/WorkflowStartedEvent.php | 19 - 77 files changed, 27 insertions(+), 10242 deletions(-) delete mode 100644 packages/workflow-engine-core/README.md delete mode 100644 packages/workflow-engine-core/composer.json delete mode 100644 packages/workflow-engine-core/phpstan.neon.dist delete mode 100644 packages/workflow-engine-core/phpunit.xml.dist delete mode 100644 packages/workflow-engine-core/src/Actions/BaseAction.php delete mode 100644 packages/workflow-engine-core/src/Actions/ConditionAction.php delete mode 100644 packages/workflow-engine-core/src/Actions/DelayAction.php delete mode 100644 packages/workflow-engine-core/src/Actions/HttpAction.php delete mode 100644 packages/workflow-engine-core/src/Actions/LogAction.php delete mode 100644 packages/workflow-engine-core/src/Attributes/Condition.php delete mode 100644 packages/workflow-engine-core/src/Attributes/Retry.php delete mode 100644 packages/workflow-engine-core/src/Attributes/Timeout.php delete mode 100644 packages/workflow-engine-core/src/Attributes/WorkflowStep.php delete mode 100644 packages/workflow-engine-core/src/Contracts/StorageAdapter.php delete mode 100644 packages/workflow-engine-core/src/Contracts/WorkflowAction.php delete mode 100644 packages/workflow-engine-core/src/Core/ActionResolver.php delete mode 100644 packages/workflow-engine-core/src/Core/ActionResult.php delete mode 100644 packages/workflow-engine-core/src/Core/DefinitionParser.php delete mode 100644 packages/workflow-engine-core/src/Core/Executor.php delete mode 100644 packages/workflow-engine-core/src/Core/StateManager.php delete mode 100644 packages/workflow-engine-core/src/Core/Step.php delete mode 100644 packages/workflow-engine-core/src/Core/WorkflowBuilder.php delete mode 100644 packages/workflow-engine-core/src/Core/WorkflowContext.php delete mode 100644 packages/workflow-engine-core/src/Core/WorkflowDefinition.php delete mode 100644 packages/workflow-engine-core/src/Core/WorkflowEngine.php delete mode 100644 packages/workflow-engine-core/src/Core/WorkflowInstance.php delete mode 100644 packages/workflow-engine-core/src/Core/WorkflowState.php delete mode 100644 packages/workflow-engine-core/src/Events/WorkflowCancelled.php delete mode 100644 packages/workflow-engine-core/src/Events/WorkflowCompletedEvent.php delete mode 100644 packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php delete mode 100644 packages/workflow-engine-core/src/Events/WorkflowStarted.php delete mode 100644 packages/workflow-engine-core/src/Exceptions/ActionNotFoundException.php delete mode 100644 packages/workflow-engine-core/src/Exceptions/InvalidWorkflowDefinitionException.php delete mode 100644 packages/workflow-engine-core/src/Exceptions/InvalidWorkflowStateException.php delete mode 100644 packages/workflow-engine-core/src/Exceptions/StepExecutionException.php delete mode 100644 packages/workflow-engine-core/src/Exceptions/WorkflowException.php delete mode 100644 packages/workflow-engine-core/src/Exceptions/WorkflowInstanceNotFoundException.php delete mode 100644 packages/workflow-engine-core/src/Support/SimpleWorkflow.php delete mode 100644 packages/workflow-engine-core/src/Support/Uuid.php delete mode 100644 packages/workflow-engine-core/src/helpers.php delete mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/CreateShipmentAction.php delete mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/FraudCheckAction.php delete mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/NotificationAndCompensationActions.php delete mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/ProcessPaymentAction.php delete mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/ReserveInventoryAction.php delete mode 100644 packages/workflow-engine-core/tests/Actions/ECommerce/ValidateOrderAction.php delete mode 100644 packages/workflow-engine-core/tests/ArchTest.php delete mode 100644 packages/workflow-engine-core/tests/ExampleTest.php delete mode 100644 packages/workflow-engine-core/tests/Integration/WorkflowIntegrationTest.php delete mode 100644 packages/workflow-engine-core/tests/Pest.php delete mode 100644 packages/workflow-engine-core/tests/RealWorld/CICDPipelineWorkflowTest.php delete mode 100644 packages/workflow-engine-core/tests/RealWorld/DocumentApprovalWorkflowTest.php delete mode 100644 packages/workflow-engine-core/tests/RealWorld/ECommerceWorkflowTest.php delete mode 100644 packages/workflow-engine-core/tests/Support/InMemoryStorage.php delete mode 100644 packages/workflow-engine-core/tests/TestCase.php delete mode 100644 packages/workflow-engine-core/tests/Unit/ActionTest.php delete mode 100644 packages/workflow-engine-core/tests/Unit/HelpersTest.php delete mode 100644 packages/workflow-engine-core/tests/Unit/PHP83FeaturesTest.php delete mode 100644 packages/workflow-engine-core/tests/Unit/WorkflowEngineTest.php delete mode 100644 src/Events/StepCompletedEvent.php delete mode 100644 src/Events/StepFailedEvent.php delete mode 100644 src/Events/WorkflowCancelled.php delete mode 100644 src/Events/WorkflowCompleted.php delete mode 100644 src/Events/WorkflowCompletedEvent.php delete mode 100644 src/Events/WorkflowFailed.php delete mode 100644 src/Events/WorkflowFailedEvent.php delete mode 100644 src/Events/WorkflowStarted.php delete mode 100644 src/Events/WorkflowStartedEvent.php diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b6a111d..6ba7e3e 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ blank_issues_enabled: false contact_links: - name: Ask a question - url: https://github.com//workflow-mastery/discussions/new?category=q-a + url: https://github.com/solutionforest/workflow-engine-laravel/discussions/new?category=q-a about: Ask the community for help - name: Request a feature - url: https://github.com//workflow-mastery/discussions/new?category=ideas + url: https://github.com/solutionforest/workflow-engine-laravel/discussions/new?category=ideas about: Share ideas for new features - name: Report a security issue - url: https://github.com//workflow-mastery/security/policy + url: https://github.com/solutionforest/workflow-engine-laravel/security/policy about: Learn how to notify us for sensitive bugs diff --git a/CHANGELOG.md b/CHANGELOG.md index 15b4b9f..7cc97bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ # Changelog -All notable changes to `workflow-mastery` will be documented in this file. +All notable changes to `workflow-engine-laravel` will be documented in this file. diff --git a/README.md b/README.md index 52360ca..5f9f685 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Laravel Workflow Engine -[![Latest Version on Packagist](https://img.shields.io/packagist/v/solution-forest/laravel-workflow-engine.svg?style=flat-square)](https://packagist.org/packages/solution-forest/laravel-workflow-engine) -[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/solution-forest/laravel-workflow-engine/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/solution-forest/laravel-workflow-engine/actions?query=workflow%3Arun-tests+branch%3Amain) -[![Total Downloads](https://img.shields.io/packagist/dt/solution-forest/laravel-workflow-engine.svg?style=flat-square)](https://packagist.org/packages/solution-forest/laravel-workflow-engine) +[![Latest Version on Packagist](https://img.shields.io/packagist/v/solution-forest/workflow-engine-laravel.svg?style=flat-square)](https://packagist.org/packages/solution-forest/workflow-engine-laravel) +[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/solutionforest/workflow-engine-laravel/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/solutionforest/workflow-engine-laravel/actions?query=workflow%3Arun-tests+branch%3Amain) +[![Total Downloads](https://img.shields.io/packagist/dt/solution-forest/workflow-engine-laravel.svg?style=flat-square)](https://packagist.org/packages/solution-forest/workflow-engine-laravel) **A modern, type-safe workflow engine for Laravel built with PHP 8.3+ features** @@ -23,7 +23,7 @@ Create powerful business workflows with a beautiful, fluent API. Turn complex pr ### Installation ```bash -composer require solution-forest/laravel-workflow-engine +composer require solution-forest/workflow-engine-laravel php artisan vendor:publish --tag="workflow-engine-config" php artisan migrate ``` diff --git a/composer.json b/composer.json index 1dd107f..0c3ccb0 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "state-machine", "laravel-package" ], - "homepage": "https://github.com/solution-forest/workflow-engine-laravel", + "homepage": "https://github.com/solutionforest/workflow-engine-laravel", "license": "MIT", "authors": [ { @@ -23,12 +23,12 @@ ], "require": { "php": "^8.3", - "solution-forest/workflow-engine-core": "*", - "spatie/laravel-package-tools": "^1.16", "illuminate/contracts": "^10.0||^11.0||^12.0", - "illuminate/support": "^10.0||^11.0||^12.0", "illuminate/database": "^10.0||^11.0||^12.0", - "illuminate/events": "^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": "dev-main", + "spatie/laravel-package-tools": "^1.16" }, "conflict": { "laravel/framework": "<11.0.0" @@ -60,12 +60,6 @@ "Workbench\\App\\": "workbench/app/" } }, - "repositories": [ - { - "type": "path", - "url": "./packages/workflow-engine-core" - } - ], "scripts": { "post-autoload-dump": "@composer run prepare", "prepare": "@php vendor/bin/testbench package:discover --ansi", @@ -92,5 +86,5 @@ } }, "minimum-stability": "dev", - "prefer-stable": true + "prefer-stable": false } diff --git a/docs/README.md b/docs/README.md index f33a151..497cb1a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,7 +25,7 @@ For contributors and those interested in the internals: ## Quick Links -- [GitHub Repository](https://github.com/solution-forest/laravel-workflow-engine) -- [Packagist Package](https://packagist.org/packages/solution-forest/laravel-workflow-engine) -- [Issue Tracker](https://github.com/solution-forest/laravel-workflow-engine/issues) +- [GitHub Repository](https://github.com/solutionforest/workflow-engine-laravel) +- [Packagist Package](https://packagist.org/packages/solution-forest/workflow-engine-laravel) +- [Issue Tracker](https://github.com/solutionforest/workflow-engine-laravel/issues) - [Contributing Guidelines](../CONTRIBUTING.md) diff --git a/docs/advanced-features.md b/docs/advanced-features.md index 28bf29d..d7b547e 100644 --- a/docs/advanced-features.md +++ b/docs/advanced-features.md @@ -265,19 +265,19 @@ Listen to workflow events: ```php use SolutionForest\WorkflowEngine\Events\WorkflowStarted; -use SolutionForest\WorkflowEngine\Events\WorkflowCompleted; -use SolutionForest\WorkflowEngine\Events\WorkflowFailed; +use SolutionForest\WorkflowEngine\Events\WorkflowCompletedEvent; +use SolutionForest\WorkflowEngine\Events\WorkflowFailedEvent; // In your EventServiceProvider protected $listen = [ WorkflowStarted::class => [ LogWorkflowStarted::class, ], - WorkflowCompleted::class => [ + WorkflowCompletedEvent::class => [ LogWorkflowCompleted::class, SendCompletionNotification::class, ], - WorkflowFailed::class => [ + WorkflowFailedEvent::class => [ LogWorkflowFailure::class, AlertAdministrators::class, ], diff --git a/docs/best-practices.md b/docs/best-practices.md index 0ad1f25..0c62dcf 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -345,11 +345,11 @@ Alert on workflow failures and performance issues: ```php // In your EventServiceProvider -use SolutionForest\WorkflowEngine\Events\WorkflowFailed; +use SolutionForest\WorkflowEngine\Events\WorkflowFailedEvent; protected $listen = [ - WorkflowFailed::class => [ - function (WorkflowFailed $event) { + WorkflowFailedEvent::class => [ + function (WorkflowFailedEvent $event) { // Alert if critical workflow fails if (in_array($event->workflowName, ['payment-processing', 'order-fulfillment'])) { Alert::critical("Critical workflow failed: {$event->workflowName}", [ diff --git a/docs/getting-started.md b/docs/getting-started.md index ede0af9..338d9fa 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -11,7 +11,7 @@ ### Install the Package ```bash -composer require solution-forest/laravel-workflow-engine +composer require solution-forest/workflow-engine-laravel ``` ### Publish Configuration diff --git a/docs/migration.md b/docs/migration.md index c21a673..cad2a50 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -16,7 +16,7 @@ This guide helps you migrate from the array-based workflow configuration to the #### 1. Update Composer Dependencies ```bash -composer update solution-forest/laravel-workflow-engine +composer update solution-forest/workflow-engine-laravel ``` #### 2. Publish New Configuration @@ -376,5 +376,5 @@ If you encounter issues during migration: 1. Check the [troubleshooting guide](troubleshooting.md) 2. Review the [examples](../src/Examples/ModernWorkflowExamples.php) -3. Open an issue on [GitHub](https://github.com/solution-forest/laravel-workflow-engine/issues) +3. Open an issue on [GitHub](https://github.com/solutionforest/workflow-engine-laravel/issues) 4. Join our [Discord community](https://discord.gg/workflow-engine) diff --git a/packages/workflow-engine-core/README.md b/packages/workflow-engine-core/README.md deleted file mode 100644 index 34dad22..0000000 --- a/packages/workflow-engine-core/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# Workflow Engine Core - -A framework-agnostic workflow engine for PHP applications. This is the core library that provides workflow definition, execution, and state management without any framework dependencies. - -## Features - -- **Framework Agnostic**: Works with any PHP framework or standalone -- **Type Safe**: Full PHP 8.1+ type safety with strict typing -- **Extensible**: Plugin architecture for custom actions and storage adapters -- **State Management**: Robust workflow instance state tracking -- **Error Handling**: Comprehensive exception handling with context -- **Performance**: Optimized for high-throughput workflow execution - -## Installation - -```bash -composer require solution-forest/workflow-engine-core -``` - -## Quick Start - -```php -use SolutionForest\WorkflowEngine\Core\WorkflowBuilder; -use SolutionForest\WorkflowEngine\Core\WorkflowEngine; -use SolutionForest\WorkflowEngine\Core\WorkflowContext; - -// Define a workflow -$workflow = WorkflowBuilder::create('order-processing') - ->addStep('validate', ValidateOrderAction::class) - ->addStep('payment', ProcessPaymentAction::class) - ->addStep('fulfillment', FulfillOrderAction::class) - ->addTransition('validate', 'payment') - ->addTransition('payment', 'fulfillment') - ->build(); - -// Create execution context -$context = new WorkflowContext( - workflowId: 'order-processing', - stepId: 'validate', - data: ['order_id' => 123, 'customer_id' => 456] -); - -// Execute workflow -$engine = new WorkflowEngine(); -$instance = $engine->start($workflow, $context); -$result = $engine->executeStep($instance, $context); -``` - -## Laravel Integration - -For Laravel applications, use the Laravel integration package: - -```bash -composer require solution-forest/workflow-engine-laravel -``` - -## Documentation - -- [Getting Started](docs/getting-started.md) -- [API Reference](docs/api-reference.md) -- [Advanced Features](docs/advanced-features.md) - -## License - -MIT License. See [LICENSE](LICENSE) for details. diff --git a/packages/workflow-engine-core/composer.json b/packages/workflow-engine-core/composer.json deleted file mode 100644 index d442881..0000000 --- a/packages/workflow-engine-core/composer.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "solution-forest/workflow-engine-core", - "description": "Framework-agnostic workflow engine for PHP applications", - "type": "library", - "license": "MIT", - "keywords": [ - "workflow", - "state-machine", - "business-process", - "automation", - "php" - ], - "authors": [ - { - "name": "Solution Forest", - "email": "info@solutionforest.com" - } - ], - "require": { - "php": "^8.1", - "nesbot/carbon": "^2.0|^3.0" - }, - "require-dev": { - "phpunit/phpunit": "^10.0", - "phpstan/phpstan": "^1.10", - "pestphp/pest": "^2.0" - }, - "autoload": { - "psr-4": { - "SolutionForest\\WorkflowEngine\\": "src/" - }, - "files": [ - "src/helpers.php" - ] - }, - "autoload-dev": { - "psr-4": { - "SolutionForest\\WorkflowEngine\\Tests\\": "tests/" - } - }, - "minimum-stability": "stable", - "prefer-stable": true, - "config": { - "sort-packages": true, - "allow-plugins": { - "pestphp/pest-plugin": true - } - } -} diff --git a/packages/workflow-engine-core/phpstan.neon.dist b/packages/workflow-engine-core/phpstan.neon.dist deleted file mode 100644 index ab1b4c3..0000000 --- a/packages/workflow-engine-core/phpstan.neon.dist +++ /dev/null @@ -1,12 +0,0 @@ -includes: - - phpstan-baseline.neon - -parameters: - level: 5 - paths: - - src - - config - - database - tmpDir: build/phpstan - checkOctaneCompatibility: true - checkModelProperties: true diff --git a/packages/workflow-engine-core/phpunit.xml.dist b/packages/workflow-engine-core/phpunit.xml.dist deleted file mode 100644 index fcacdf3..0000000 --- a/packages/workflow-engine-core/phpunit.xml.dist +++ /dev/null @@ -1,31 +0,0 @@ - - - - - tests - - - - - - - - ./src - - - diff --git a/packages/workflow-engine-core/src/Actions/BaseAction.php b/packages/workflow-engine-core/src/Actions/BaseAction.php deleted file mode 100644 index 9764203..0000000 --- a/packages/workflow-engine-core/src/Actions/BaseAction.php +++ /dev/null @@ -1,394 +0,0 @@ -getData(), 'user.id'); - * $status = $this->getConfig('status', 'active'); - * - * User::where('id', $userId)->update(['status' => $status]); - * - * return ActionResult::success(['user_id' => $userId, 'status' => $status]); - * } - * - * public function canExecute(WorkflowContext $context): bool - * { - * return data_get($context->getData(), 'user.id') !== null; - * } - * - * public function getName(): string - * { - * return 'Update User Status'; - * } - * } - * ``` - * - * ### Action with Complex Validation - * ```php - * class ProcessPaymentAction extends BaseAction - * { - * public function canExecute(WorkflowContext $context): bool - * { - * $data = $context->getData(); - * - * // Check required data - * if (!data_get($data, 'payment.amount') || !data_get($data, 'payment.method')) { - * return false; - * } - * - * // Check configuration - * return !empty($this->getConfig('gateway_key')); - * } - * } - * ``` - * - * @see WorkflowAction For the interface definition - * @see ActionResult For return value structure - * @see StepExecutionException For error handling patterns - */ -abstract class BaseAction implements WorkflowAction -{ - /** @var array Action-specific configuration parameters */ - protected array $config; - - /** - * Create a new base action with optional configuration. - * - * @param array $config Action-specific configuration - */ - public function __construct(array $config = []) - { - $this->config = $config; - } - - /** - * Execute the workflow action with comprehensive logging and error handling. - * - * This method implements the template method pattern, providing a consistent - * execution flow while allowing customization through the abstract doExecute - * method and optional canExecute validation. - * - * ## Execution Flow - * 1. **Logging**: Log action start with context information - * 2. **Validation**: Check prerequisites via canExecute() - * 3. **Execution**: Run business logic via doExecute() - * 4. **Success Logging**: Log successful completion with results - * 5. **Error Handling**: Catch and log any exceptions - * - * ## Error Handling - * All exceptions are caught and converted to failed ActionResults. - * For custom error handling, override this method or throw - * StepExecutionException with specific context. - * - * @param WorkflowContext $context The current workflow execution context - * @return ActionResult Success or failure result with data and messages - * - * @throws StepExecutionException When action execution fails - * - * @example Basic execution flow - * ```php - * $action = new MyAction(['config' => 'value']); - * $result = $action->execute($context); - * - * if ($result->isSuccess()) { - * echo "Action completed: " . json_encode($result->getData()); - * } else { - * echo "Action failed: " . $result->getMessage(); - * } - * ``` - */ - public function execute(WorkflowContext $context): ActionResult - { - Log::info('Executing action', [ - 'action' => static::class, - 'action_name' => $this->getName(), - 'workflow_id' => $context->getWorkflowId(), - 'step_id' => $context->getStepId(), - 'config' => $this->config, - ]); - - try { - // Validate prerequisites before execution - if (! $this->canExecute($context)) { - $message = sprintf( - 'Action prerequisites not met for %s in workflow %s step %s', - $this->getName(), - $context->getWorkflowId(), - $context->getStepId() - ); - - Log::warning('Action prerequisites failed', [ - 'action' => static::class, - 'workflow_id' => $context->getWorkflowId(), - 'step_id' => $context->getStepId(), - ]); - - return ActionResult::failure($message); - } - - // Execute the action's business logic - $result = $this->doExecute($context); - - // Log successful completion with result data - Log::info('Action completed successfully', [ - 'action' => static::class, - 'action_name' => $this->getName(), - 'workflow_id' => $context->getWorkflowId(), - 'step_id' => $context->getStepId(), - 'success' => $result->isSuccess(), - 'result_data' => $result->getData(), - ]); - - return $result; - - } catch (StepExecutionException $e) { - // Re-throw StepExecutionException to preserve context - Log::error('Action failed with step execution exception', [ - 'action' => static::class, - 'workflow_id' => $context->getWorkflowId(), - 'step_id' => $context->getStepId(), - 'error' => $e->getMessage(), - 'context' => $e->getContext(), - ]); - - throw $e; - } catch (\Exception $e) { - // Log and return failure result for general exceptions - // The Executor will convert this to a StepExecutionException with Step context - Log::error('Action failed with unexpected exception', [ - 'action' => static::class, - 'workflow_id' => $context->getWorkflowId(), - 'step_id' => $context->getStepId(), - 'error' => $e->getMessage(), - 'exception_class' => get_class($e), - 'trace' => $e->getTraceAsString(), - ]); - - return ActionResult::failure( - sprintf( - 'Action %s failed: %s', - $this->getName(), - $e->getMessage() - ), - [ - 'exception_class' => get_class($e), - 'exception_code' => $e->getCode(), - 'exception_file' => $e->getFile(), - 'exception_line' => $e->getLine(), - ] - ); - } - } - - /** - * Check if the action can be executed with the given context. - * - * This method provides a validation hook before action execution. - * Override this method to implement custom validation logic, such as - * checking required data fields, external service availability, or - * action-specific prerequisites. - * - * ## Validation Examples - * - **Data Validation**: Check required fields in context data - * - **Configuration**: Verify required configuration values - * - **External Dependencies**: Test connectivity to external services - * - **Business Rules**: Apply domain-specific validation logic - * - * @param WorkflowContext $context The current workflow execution context - * @return bool True if the action can be executed, false otherwise - * - * @example Data validation - * ```php - * public function canExecute(WorkflowContext $context): bool - * { - * $data = $context->getData(); - * - * // Check required fields - * if (!data_get($data, 'user.id') || !data_get($data, 'email')) { - * return false; - * } - * - * // Check configuration - * return !empty($this->getConfig('api_key')); - * } - * ``` - */ - public function canExecute(WorkflowContext $context): bool - { - return true; // Default implementation allows execution - } - - /** - * Get a human-readable name for this action. - * - * Override this method to provide a descriptive name for the action - * that will be used in logging, debugging, and user interfaces. - * The default implementation returns the class name without namespace. - * - * @return string The action name - * - * @example Custom action name - * ```php - * public function getName(): string - * { - * return 'Send Welcome Email'; - * } - * ``` - */ - public function getName(): string - { - return class_basename(static::class); - } - - /** - * Get a detailed description of what this action does. - * - * Override this method to provide a comprehensive description of the - * action's purpose, behavior, and effects. This is useful for - * documentation, debugging, and workflow visualization tools. - * - * @return string The action description - * - * @example Detailed description - * ```php - * public function getDescription(): string - * { - * return 'Sends a personalized welcome email to new users with account setup instructions and verification link.'; - * } - * ``` - */ - public function getDescription(): string - { - return 'Base workflow action implementation with logging and error handling'; - } - - /** - * Implement the actual action logic in this method. - * - * This is the main method where action-specific business logic should be - * implemented. It will be called by the execute() method after validation - * and logging setup. The method should return an ActionResult indicating - * success or failure. - * - * ## Implementation Guidelines - * - **Return ActionResult**: Always return success() or failure() result - * - **Exception Handling**: Let exceptions bubble up for consistent logging - * - **Data Access**: Use context data and action configuration - * - **Side Effects**: Perform the action's business operations - * - * @param WorkflowContext $context The current workflow execution context - * @return ActionResult The result of the action execution - * - * @throws \Exception Any exceptions during action execution - * - * @example Implementation pattern - * ```php - * protected function doExecute(WorkflowContext $context): ActionResult - * { - * // Get data from context - * $userId = data_get($context->getData(), 'user.id'); - * $email = data_get($context->getData(), 'user.email'); - * - * // Get configuration - * $template = $this->getConfig('email_template', 'welcome'); - * - * // Perform business logic - * $result = EmailService::send($email, $template, ['user_id' => $userId]); - * - * // Return appropriate result - * if ($result['sent']) { - * return ActionResult::success([ - * 'email_id' => $result['id'], - * 'sent_at' => now()->toISOString() - * ]); - * } else { - * return ActionResult::failure('Failed to send email: ' . $result['error']); - * } - * } - * ``` - */ - abstract protected function doExecute(WorkflowContext $context): ActionResult; - - /** - * Get a configuration value with optional default. - * - * Retrieves a value from the action configuration using dot notation. - * This is a convenience method for accessing action-specific settings - * that were provided during action instantiation. - * - * @param string $key The configuration key (supports dot notation) - * @param mixed $default The default value if key is not found - * @return mixed The configuration value or default - * - * @example Configuration access - * ```php - * // Simple key - * $apiKey = $this->getConfig('api_key'); - * - * // Nested key with dot notation - * $timeout = $this->getConfig('http.timeout', 30); - * - * // Array key with default - * $retries = $this->getConfig('retry.attempts', 3); - * ``` - */ - protected function getConfig(string $key, $default = null) - { - return data_get($this->config, $key, $default); - } - - /** - * Get all action configuration. - * - * Returns the complete configuration array that was provided - * during action construction. Useful for debugging or when - * you need to access multiple configuration values. - * - * @return array The complete configuration array - * - * @example Configuration access - * ```php - * $config = $this->getAllConfig(); - * - * // Log all configuration for debugging - * Log::debug('Action config', $config); - * - * // Check if any configuration was provided - * if (empty($config)) { - * // Use default behavior - * } - * ``` - */ - protected function getAllConfig(): array - { - return $this->config; - } -} diff --git a/packages/workflow-engine-core/src/Actions/ConditionAction.php b/packages/workflow-engine-core/src/Actions/ConditionAction.php deleted file mode 100644 index e1e03e1..0000000 --- a/packages/workflow-engine-core/src/Actions/ConditionAction.php +++ /dev/null @@ -1,117 +0,0 @@ -getConfig('condition'); - $onTrue = $this->getConfig('on_true', null); - $onFalse = $this->getConfig('on_false', null); - - if (! $condition) { - return ActionResult::failure('Condition is required'); - } - - try { - $result = $this->evaluateCondition($condition, $context->getData()); - - return ActionResult::success([ - 'condition' => $condition, - 'result' => $result, - 'next_action' => $result ? $onTrue : $onFalse, - ]); - - } catch (\Exception $e) { - return ActionResult::failure( - "Condition evaluation failed: {$e->getMessage()}", - ['condition' => $condition] - ); - } - } - - /** - * Enhanced condition evaluation with PHP 8.3+ match expressions - */ - private function evaluateCondition(string $condition, array $data): bool - { - // Simple expression parser for common patterns - if (preg_match('/^(.+?)\s*(=|!=|>|<|>=|<=|is|is not)\s*(.+)$/', $condition, $matches)) { - $left = trim($matches[1]); - $operator = trim($matches[2]); - $right = trim($matches[3]); - - $leftValue = $this->getValue($left, $data); - $rightValue = $this->getValue($right, $data); - - return match ($operator) { - '=' => $leftValue == $rightValue, - '!=' => $leftValue != $rightValue, - '>' => $leftValue > $rightValue, - '<' => $leftValue < $rightValue, - '>=' => $leftValue >= $rightValue, - '<=' => $leftValue <= $rightValue, - 'is' => $leftValue === $rightValue, - 'is not' => $leftValue !== $rightValue, - default => throw new \InvalidArgumentException("Unsupported operator: {$operator}") - }; - } - - // Check for boolean values - if (in_array(strtolower($condition), ['true', '1', 'yes'])) { - return true; - } - - if (in_array(strtolower($condition), ['false', '0', 'no'])) { - return false; - } - - // Direct data access - return (bool) $this->getValue($condition, $data); - } - - private function getValue(string $expression, array $data): mixed - { - // Remove quotes for string literals - if (preg_match('/^["\'](.+)["\']$/', $expression, $matches)) { - return $matches[1]; - } - - // Check for numeric values - if (is_numeric($expression)) { - return str_contains($expression, '.') ? (float) $expression : (int) $expression; - } - - // Check for boolean literals - return match (strtolower($expression)) { - 'true', 'yes' => true, - 'false', 'no' => false, - 'null', 'empty' => null, - default => data_get($data, $expression) - }; - } -} diff --git a/packages/workflow-engine-core/src/Actions/DelayAction.php b/packages/workflow-engine-core/src/Actions/DelayAction.php deleted file mode 100644 index 883d597..0000000 --- a/packages/workflow-engine-core/src/Actions/DelayAction.php +++ /dev/null @@ -1,49 +0,0 @@ -getConfig('seconds', 1); - $microseconds = $this->getConfig('microseconds', 0); - - if (! is_numeric($seconds) || $seconds < 0) { - return ActionResult::failure('Invalid delay seconds specified'); - } - - if (! is_numeric($microseconds) || $microseconds < 0) { - return ActionResult::failure('Invalid delay microseconds specified'); - } - - // Convert to total microseconds - $totalMicroseconds = ($seconds * 1000000) + $microseconds; - - if ($totalMicroseconds > 0) { - usleep((int) $totalMicroseconds); - } - - return ActionResult::success([ - 'delayed_seconds' => $seconds, - 'delayed_microseconds' => $microseconds, - 'delayed_at' => now()->toISOString(), - ]); - } -} diff --git a/packages/workflow-engine-core/src/Actions/HttpAction.php b/packages/workflow-engine-core/src/Actions/HttpAction.php deleted file mode 100644 index 51c9e0f..0000000 --- a/packages/workflow-engine-core/src/Actions/HttpAction.php +++ /dev/null @@ -1,114 +0,0 @@ -getConfig('url'); - $method = strtoupper($this->getConfig('method', 'GET')); - $data = $this->getConfig('data', []); - $headers = $this->getConfig('headers', []); - $timeout = $this->getConfig('timeout', 30); - - if (! $url) { - return ActionResult::failure('URL is required for HTTP action'); - } - - // Process template variables in URL and data - $url = $this->processTemplate($url, $context->getData()); - $data = $this->processArrayTemplates($data, $context->getData()); - - try { - $response = match ($method) { - 'GET' => Http::timeout($timeout)->withHeaders($headers)->get($url, $data), - 'POST' => Http::timeout($timeout)->withHeaders($headers)->post($url, $data), - 'PUT' => Http::timeout($timeout)->withHeaders($headers)->put($url, $data), - 'PATCH' => Http::timeout($timeout)->withHeaders($headers)->patch($url, $data), - 'DELETE' => Http::timeout($timeout)->withHeaders($headers)->delete($url, $data), - default => throw new \InvalidArgumentException("Unsupported HTTP method: {$method}") - }; - - if ($response->successful()) { - return ActionResult::success([ - 'status_code' => $response->status(), - 'response_data' => $response->json(), - 'headers' => $response->headers(), - 'url' => $url, - 'method' => $method, - ]); - } - - return ActionResult::failure( - "HTTP request failed with status {$response->status()}: {$response->body()}", - [ - 'status_code' => $response->status(), - 'response_body' => $response->body(), - 'url' => $url, - 'method' => $method, - ] - ); - - } catch (\Exception $e) { - return ActionResult::failure( - "HTTP request exception: {$e->getMessage()}", - [ - 'exception' => $e->getMessage(), - 'url' => $url, - 'method' => $method, - ] - ); - } - } - - private function processTemplate(string $template, array $data): string - { - return preg_replace_callback('/\{\{\s*([^}]+)\s*\}\}/', function ($matches) use ($data) { - return data_get($data, trim($matches[1]), $matches[0]); - }, $template); - } - - private function processArrayTemplates(array $array, array $data): array - { - $result = []; - foreach ($array as $key => $value) { - if (is_string($value)) { - $result[$key] = $this->processTemplate($value, $data); - } elseif (is_array($value)) { - $result[$key] = $this->processArrayTemplates($value, $data); - } else { - $result[$key] = $value; - } - } - - return $result; - } -} diff --git a/packages/workflow-engine-core/src/Actions/LogAction.php b/packages/workflow-engine-core/src/Actions/LogAction.php deleted file mode 100644 index 5d6a17b..0000000 --- a/packages/workflow-engine-core/src/Actions/LogAction.php +++ /dev/null @@ -1,57 +0,0 @@ -getConfig('message', 'Default log message'); - $level = $this->getConfig('level', 'info'); - - // Replace placeholders in message with workflow data - $processedMessage = $this->processMessage($message, $context->getData()); - - // Log with appropriate level - match (strtolower($level)) { - 'debug' => Log::debug($processedMessage, $context->toArray()), - 'info' => Log::info($processedMessage, $context->toArray()), - 'warning' => Log::warning($processedMessage, $context->toArray()), - 'error' => Log::error($processedMessage, $context->toArray()), - default => Log::info($processedMessage, $context->toArray()), - }; - - return ActionResult::success([ - 'logged_message' => $processedMessage, - 'logged_at' => now()->toISOString(), - ]); - } - - private function processMessage(string $message, array $data): string - { - // Simple placeholder replacement: {key.subkey} - return preg_replace_callback('/\{([^}]+)\}/', function ($matches) use ($data) { - $key = $matches[1]; - $value = data_get($data, $key, $matches[0]); // Keep placeholder if key not found - - return is_scalar($value) ? (string) $value : json_encode($value); - }, $message); - } -} diff --git a/packages/workflow-engine-core/src/Attributes/Condition.php b/packages/workflow-engine-core/src/Attributes/Condition.php deleted file mode 100644 index ad12027..0000000 --- a/packages/workflow-engine-core/src/Attributes/Condition.php +++ /dev/null @@ -1,22 +0,0 @@ - 100')] - * #[Condition('user.premium = true')] - */ -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -readonly class Condition -{ - public function __construct( - public string $expression, - public string $operator = 'and' // 'and', 'or' - ) {} -} diff --git a/packages/workflow-engine-core/src/Attributes/Retry.php b/packages/workflow-engine-core/src/Attributes/Retry.php deleted file mode 100644 index 775c49b..0000000 --- a/packages/workflow-engine-core/src/Attributes/Retry.php +++ /dev/null @@ -1,24 +0,0 @@ -totalSeconds = ($seconds ?? 0) + (($minutes ?? 0) * 60) + (($hours ?? 0) * 3600); - } -} diff --git a/packages/workflow-engine-core/src/Attributes/WorkflowStep.php b/packages/workflow-engine-core/src/Attributes/WorkflowStep.php deleted file mode 100644 index 343a482..0000000 --- a/packages/workflow-engine-core/src/Attributes/WorkflowStep.php +++ /dev/null @@ -1,29 +0,0 @@ -getConfig(); - * $emailService = app(EmailService::class); - * - * try { - * $result = $emailService->send($config['to'], $config['subject'], $config['body']); - * - * return ActionResult::success(['message_id' => $result->getId()]); - * } catch (EmailException $e) { - * throw StepExecutionException::actionFailed( - * $context->getStepId(), - * $e, - * ['email_config' => $config] - * ); - * } - * } - * - * public function canExecute(WorkflowContext $context): bool - * { - * $config = $context->getConfig(); - * return !empty($config['to']) && !empty($config['subject']); - * } - * } - * ``` - * - * ### Conditional Action - * ```php - * class PremiumFeatureAction implements WorkflowAction - * { - * public function canExecute(WorkflowContext $context): bool - * { - * $userData = data_get($context->data, 'user'); - * return $userData['plan'] === 'premium' && $userData['active'] === true; - * } - * } - * ``` - * - * @see BaseAction For a convenient base implementation - * @see ActionResult For action execution results - * @see StepExecutionException For action error handling - */ -interface WorkflowAction -{ - /** - * Execute the workflow action with the provided context. - * - * This is the core method where the action's business logic is implemented. - * It should perform the intended operation and return an appropriate result. - * - * @param WorkflowContext $context The workflow execution context - * @return ActionResult The result of the action execution - * - * @throws StepExecutionException When the action fails to execute properly - * - * @example - * ```php - * public function execute(WorkflowContext $context): ActionResult - * { - * $config = $context->getConfig(); - * $data = $context->getData(); - * - * // Perform action logic - * $result = $this->performOperation($config, $data); - * - * return ActionResult::success(['result' => $result]); - * } - * ``` - */ - public function execute(WorkflowContext $context): ActionResult; - - /** - * Check if this action can be executed with the given context. - * - * This method allows for pre-execution validation and conditional logic. - * It should check prerequisites, validate configuration, and ensure the - * action can be safely executed. - * - * @param WorkflowContext $context The workflow execution context - * @return bool True if the action can be executed, false otherwise - * - * @example - * ```php - * public function canExecute(WorkflowContext $context): bool - * { - * $config = $context->getConfig(); - * - * // Check required configuration - * if (empty($config['api_key'])) { - * return false; - * } - * - * // Check data prerequisites - * $data = $context->getData(); - * return isset($data['user']['id']); - * } - * ``` - */ - public function canExecute(WorkflowContext $context): bool; - - /** - * Get the human-readable display name for this action. - * - * Used for workflow visualization, debugging, and user interfaces. - * Should be descriptive and concise. - * - * @return string The action display name - * - * @example - * ```php - * public function getName(): string - * { - * return 'Send Welcome Email'; - * } - * ``` - */ - public function getName(): string; - - /** - * Get a detailed description of what this action does. - * - * Used for documentation, workflow visualization, and debugging. - * Should explain the action's purpose and behavior clearly. - * - * @return string The action description - * - * @example - * ```php - * public function getDescription(): string - * { - * return 'Sends a personalized welcome email to the user using the configured template'; - * } - * ``` - */ - public function getDescription(): string; -} diff --git a/packages/workflow-engine-core/src/Core/ActionResolver.php b/packages/workflow-engine-core/src/Core/ActionResolver.php deleted file mode 100644 index 7fe24c5..0000000 --- a/packages/workflow-engine-core/src/Core/ActionResolver.php +++ /dev/null @@ -1,336 +0,0 @@ -> Predefined action mappings */ - private const ACTION_MAP = [ - 'log' => LogAction::class, - 'delay' => DelayAction::class, - ]; - - /** @var array Custom action mappings registered at runtime */ - private static array $customActions = []; - - /** - * Resolve action name to concrete action class. - * - * This method attempts to resolve an action name using multiple strategies: - * 1. Check if it's already a valid class name - * 2. Look for predefined action shortcuts - * 3. Check custom registered actions - * 4. Attempt automatic class name construction - * - * @param string $actionName The action name or class to resolve - * @return string The fully qualified action class name - * - * @throws InvalidWorkflowDefinitionException If the action cannot be resolved or is invalid - * - * @example Basic resolution - * ```php - * // Predefined actions - * $class = ActionResolver::resolve('log'); // LogAction::class - * - * // Direct class names - * $class = ActionResolver::resolve(MyCustomAction::class); - * - * // Automatic construction - * $class = ActionResolver::resolve('sendEmail'); // SendEmailAction::class - * ``` - */ - public static function resolve(string $actionName): string - { - // Strategy 1: If it's already a full class name, validate and return - if (class_exists($actionName)) { - if (! self::isValidAction($actionName)) { - throw InvalidWorkflowDefinitionException::invalidActionClass( - $actionName, - WorkflowAction::class - ); - } - - return $actionName; - } - - // Strategy 2: Check predefined action map - if (isset(self::ACTION_MAP[$actionName])) { - return self::ACTION_MAP[$actionName]; - } - - // Strategy 3: Check custom registered actions - if (isset(self::$customActions[$actionName])) { - return self::$customActions[$actionName]; - } - - // Strategy 4: Try automatic class name construction - $className = 'SolutionForest\\WorkflowMastery\\Actions\\'.self::normalizeActionName($actionName).'Action'; - if (class_exists($className)) { - if (! self::isValidAction($className)) { - throw InvalidWorkflowDefinitionException::invalidActionClass( - $className, - WorkflowAction::class - ); - } - - return $className; - } - - // Strategy 5: Try in global namespace for user-defined actions - $globalClassName = self::normalizeActionName($actionName).'Action'; - if (class_exists($globalClassName)) { - if (! self::isValidAction($globalClassName)) { - throw InvalidWorkflowDefinitionException::invalidActionClass( - $globalClassName, - WorkflowAction::class - ); - } - - return $globalClassName; - } - - // If all strategies fail, throw descriptive exception - throw InvalidWorkflowDefinitionException::actionNotFound($actionName, [ - 'tried_classes' => [ - $className, - $globalClassName, - ], - 'predefined_actions' => array_keys(self::ACTION_MAP), - 'custom_actions' => array_keys(self::$customActions), - 'suggestion' => "Create a class named '{$className}' that implements WorkflowAction, or register a custom action with ActionResolver::register()", - ]); - } - - /** - * Register a custom action mapping. - * - * Allows registration of custom action classes with short names for - * easier reference in workflow definitions. Useful for application-specific - * actions or third-party action libraries. - * - * @param string $name The short name for the action - * @param string $className The fully qualified class name - * - * @throws InvalidWorkflowDefinitionException If the class doesn't implement WorkflowAction - * - * @example Custom action registration - * ```php - * // Register application-specific actions - * ActionResolver::register('sendWelcomeEmail', App\Actions\SendWelcomeEmailAction::class); - * ActionResolver::register('processStripePayment', App\Actions\StripePaymentAction::class); - * - * // Now use in workflow definitions - * $builder->addStep('welcome', 'sendWelcomeEmail', ['template' => 'welcome']); - * ``` - */ - public static function register(string $name, string $className): void - { - if (! class_exists($className)) { - throw InvalidWorkflowDefinitionException::actionNotFound($className); - } - - if (! self::isValidAction($className)) { - throw InvalidWorkflowDefinitionException::invalidActionClass( - $className, - WorkflowAction::class - ); - } - - self::$customActions[$name] = $className; - } - - /** - * Check if a class is a valid workflow action. - * - * Validates that a given class exists and implements the WorkflowAction interface. - * - * @param string $className The class name to validate - * @return bool True if the class is a valid action, false otherwise - * - * @example Action validation - * ```php - * if (ActionResolver::isValidAction(MyAction::class)) { - * $action = new MyAction($config); - * } else { - * throw new Exception('Invalid action class'); - * } - * ``` - */ - public static function isValidAction(string $className): bool - { - if (! class_exists($className)) { - return false; - } - - return in_array(WorkflowAction::class, class_implements($className) ?: []); - } - - /** - * Get all available predefined actions. - * - * Returns a mapping of action names to their corresponding class names - * for all predefined actions built into the workflow engine. - * - * @return array> Predefined action mappings - * - * @example List available actions - * ```php - * $actions = ActionResolver::getAvailableActions(); - * foreach ($actions as $name => $class) { - * echo "Action '{$name}' maps to {$class}\n"; - * } - * ``` - */ - public static function getAvailableActions(): array - { - return self::ACTION_MAP; - } - - /** - * Get all custom registered actions. - * - * Returns a mapping of custom action names to their corresponding class names - * that have been registered via the register() method. - * - * @return array Custom action mappings - * - * @example List custom actions - * ```php - * $customActions = ActionResolver::getCustomActions(); - * foreach ($customActions as $name => $class) { - * echo "Custom action '{$name}' maps to {$class}\n"; - * } - * ``` - */ - public static function getCustomActions(): array - { - return self::$customActions; - } - - /** - * Clear all custom action registrations. - * - * Removes all custom action mappings. Useful for testing or when - * you need to reset the action resolver state. - * - * @example Reset custom actions - * ```php - * // Register some actions - * ActionResolver::register('custom1', CustomAction1::class); - * ActionResolver::register('custom2', CustomAction2::class); - * - * // Clear all custom registrations - * ActionResolver::clearCustomActions(); - * - * // Now only predefined actions are available - * ``` - */ - public static function clearCustomActions(): void - { - self::$customActions = []; - } - - /** - * Check if an action name is registered. - * - * @param string $actionName The action name to check - * @return bool True if the action is available, false otherwise - * - * @example Check action availability - * ```php - * if (ActionResolver::has('log')) { - * // Action is available - * } - * - * if (ActionResolver::has('myCustomAction')) { - * // Custom action is registered - * } - * ``` - */ - public static function has(string $actionName): bool - { - return isset(self::ACTION_MAP[$actionName]) || - isset(self::$customActions[$actionName]) || - class_exists($actionName); - } - - /** - * Normalize action names for automatic class name construction. - * - * Converts action names from various formats (camelCase, snake_case, kebab-case) - * to PascalCase for class name construction. - * - * @param string $actionName The action name to normalize - * @return string The normalized class name part - * - * @example Name normalization - * ```php - * ActionResolver::normalizeActionName('send_email'); // 'SendEmail' - * ActionResolver::normalizeActionName('send-email'); // 'SendEmail' - * ActionResolver::normalizeActionName('sendEmail'); // 'SendEmail' - * ActionResolver::normalizeActionName('SEND_EMAIL'); // 'SendEmail' - * ``` - */ - private static function normalizeActionName(string $actionName): string - { - // Convert to snake_case first, then to PascalCase - $snakeCase = strtolower(preg_replace('/[A-Z]/', '_$0', $actionName)); - $snakeCase = str_replace(['-', ' '], '_', $snakeCase); - $snakeCase = trim($snakeCase, '_'); - - return str_replace(' ', '', ucwords(str_replace('_', ' ', $snakeCase))); - } -} diff --git a/packages/workflow-engine-core/src/Core/ActionResult.php b/packages/workflow-engine-core/src/Core/ActionResult.php deleted file mode 100644 index db2ff10..0000000 --- a/packages/workflow-engine-core/src/Core/ActionResult.php +++ /dev/null @@ -1,412 +0,0 @@ - 123, - * 'email_sent' => true, - * 'timestamp' => now()->toISOString() - * ]); - * - * // Success with data and metadata - * $result = ActionResult::success( - * ['processed_count' => 50], - * ['execution_time_ms' => 1250, 'memory_peak_mb' => 12.5] - * ); - * ``` - * - * ### Failure Results - * ```php - * // Simple failure - * $result = ActionResult::failure('Database connection failed'); - * - * // Failure with metadata for debugging - * $result = ActionResult::failure( - * 'API rate limit exceeded', - * [ - * 'retry_after' => 3600, - * 'requests_remaining' => 0, - * 'reset_time' => '2024-01-01T15:00:00Z' - * ] - * ); - * ``` - * - * ### Conditional Results - * ```php - * $users = User::where('active', true)->get(); - * - * if ($users->count() > 0) { - * return ActionResult::success([ - * 'users' => $users->toArray(), - * 'count' => $users->count() - * ]); - * } else { - * return ActionResult::failure('No active users found'); - * } - * ``` - * - * @see WorkflowAction For the interface that returns ActionResult - * @see BaseAction For the base implementation using ActionResult - */ -final class ActionResult -{ - /** - * Create a new action result. - * - * @param bool $success Whether the action execution was successful - * @param string|null $errorMessage Error message for failed actions - * @param array $data Result data for successful actions - * @param array $metadata Additional context and debugging information - */ - public function __construct( - private readonly bool $success, - private readonly ?string $errorMessage = null, - private readonly array $data = [], - private readonly array $metadata = [] - ) {} - - /** - * Create a successful action result. - * - * Use this factory method to create a result indicating successful - * action execution. The data array should contain any information - * that needs to be passed to subsequent workflow steps. - * - * @param array $data Result data to pass to next steps - * @param array $metadata Additional execution metadata - * @return static A successful action result - * - * @example Basic success - * ```php - * return ActionResult::success([ - * 'order_id' => $order->id, - * 'total_amount' => $order->total, - * 'status' => 'completed' - * ]); - * ``` - * @example Success with metadata - * ```php - * return ActionResult::success( - * ['emails_sent' => $count], - * ['execution_time' => $duration, 'memory_used' => $memory] - * ); - * ``` - */ - public static function success(array $data = [], array $metadata = []): static - { - return new self(true, null, $data, $metadata); - } - - /** - * Create a failed action result. - * - * Use this factory method to create a result indicating failed - * action execution. The error message should be descriptive and - * helpful for debugging. Metadata can include additional context. - * - * @param string $errorMessage Descriptive error message - * @param array $metadata Additional error context and debugging info - * @return static A failed action result - * - * @example Basic failure - * ```php - * return ActionResult::failure('User with ID 123 not found'); - * ``` - * @example Failure with debugging metadata - * ```php - * return ActionResult::failure( - * 'Payment gateway timeout', - * [ - * 'gateway' => 'stripe', - * 'response_time' => 30000, - * 'attempt_number' => 3 - * ] - * ); - * ``` - */ - public static function failure(string $errorMessage, array $metadata = []): static - { - return new self(false, $errorMessage, [], $metadata); - } - - /** - * Check if the action execution was successful. - * - * @return bool True if the action succeeded, false otherwise - * - * @example Conditional processing - * ```php - * $result = $action->execute($context); - * - * if ($result->isSuccess()) { - * $data = $result->getData(); - * // Process success data - * } - * ``` - */ - public function isSuccess(): bool - { - return $this->success; - } - - /** - * Check if the action execution failed. - * - * @return bool True if the action failed, false otherwise - * - * @example Error handling - * ```php - * if ($result->isFailure()) { - * Log::error('Action failed: ' . $result->getErrorMessage()); - * } - * ``` - */ - public function isFailure(): bool - { - return ! $this->success; - } - - /** - * Get the error message for failed results. - * - * Returns the error message if the action failed, or null if it succeeded. - * The error message should provide clear information about what went wrong. - * - * @return string|null The error message, or null for successful results - * - * @example Error message access - * ```php - * if ($result->isFailure()) { - * $errorMessage = $result->getErrorMessage(); - * throw new \Exception("Action failed: {$errorMessage}"); - * } - * ``` - */ - public function getErrorMessage(): ?string - { - return $this->errorMessage; - } - - /** - * Get the result data from successful actions. - * - * Returns the data array containing information produced by the action. - * For failed actions, this will always be an empty array. - * - * @return array The result data - * - * @example Data access - * ```php - * if ($result->isSuccess()) { - * $data = $result->getData(); - * $userId = data_get($data, 'user.id'); - * $email = data_get($data, 'user.email'); - * } - * ``` - */ - public function getData(): array - { - return $this->data; - } - - /** - * Check if the result contains any data. - * - * @return bool True if the result has data, false if empty - * - * @example Data presence check - * ```php - * if ($result->hasData()) { - * $this->processResultData($result->getData()); - * } - * ``` - */ - public function hasData(): bool - { - return ! empty($this->data); - } - - /** - * Get the metadata for additional context. - * - * Metadata contains additional information about the action execution, - * such as performance metrics, debugging information, or other context - * that doesn't belong in the main result data. - * - * @return array The metadata array - * - * @example Metadata access - * ```php - * $metadata = $result->getMetadata(); - * $executionTime = data_get($metadata, 'execution_time_ms'); - * $memoryUsage = data_get($metadata, 'memory_peak_mb'); - * ``` - */ - public function getMetadata(): array - { - return $this->metadata; - } - - /** - * Create a new result with additional metadata. - * - * Since ActionResult is immutable, this method returns a new instance - * with the provided metadata merged with the existing metadata. - * - * @param array $metadata Additional metadata to merge - * @return static A new ActionResult instance with merged metadata - * - * @example Adding metadata - * ```php - * $result = ActionResult::success(['user_id' => 123]); - * $resultWithMetadata = $result->withMetadata([ - * 'execution_time' => 150, - * 'cache_hit' => true - * ]); - * ``` - */ - public function withMetadata(array $metadata): static - { - return new self( - $this->success, - $this->errorMessage, - $this->data, - array_merge($this->metadata, $metadata) - ); - } - - /** - * Create a new result with an additional metadata entry. - * - * Convenience method for adding a single metadata entry. Returns a new - * ActionResult instance with the additional metadata. - * - * @param string $key The metadata key - * @param mixed $value The metadata value - * @return static A new ActionResult instance with the additional metadata - * - * @example Adding single metadata - * ```php - * $result = ActionResult::success(['data' => 'value']); - * $resultWithTimer = $result->withMetadataEntry('duration_ms', 250); - * ``` - */ - public function withMetadataEntry(string $key, $value): self - { - return $this->withMetadata([$key => $value]); - } - - /** - * Convert the result to an array representation. - * - * Creates a plain array representation of the action result that can - * be serialized, stored, or transmitted. Useful for logging, caching, - * or API responses. - * - * @return array Array representation of the result - * - * @example Serialization - * ```php - * $result = ActionResult::success(['user_id' => 123]); - * $array = $result->toArray(); - * - * // Store in cache or database - * Cache::put('action_result', json_encode($array)); - * - * // Log for debugging - * Log::info('Action completed', $array); - * ``` - */ - public function toArray(): array - { - return [ - 'success' => $this->success, - 'error_message' => $this->errorMessage, - 'data' => $this->data, - 'metadata' => $this->metadata, - ]; - } - - /** - * Get a specific data value using dot notation. - * - * Convenience method for accessing nested data values without - * manually checking array keys. Returns null if the key doesn't exist. - * - * @param string $key The data key (supports dot notation) - * @param mixed $default The default value if key is not found - * @return mixed The data value or default - * - * @example Dot notation access - * ```php - * $result = ActionResult::success([ - * 'user' => ['id' => 123, 'email' => 'user@example.com'], - * 'metadata' => ['timestamp' => '2024-01-01T12:00:00Z'] - * ]); - * - * $userId = $result->get('user.id'); // 123 - * $email = $result->get('user.email'); // 'user@example.com' - * $timestamp = $result->get('metadata.timestamp'); - * $missing = $result->get('user.phone', 'N/A'); // 'N/A' - * ``` - */ - public function get(string $key, $default = null) - { - return data_get($this->data, $key, $default); - } - - /** - * Create a new successful result by merging data with this result. - * - * Convenience method for creating a new success result that combines - * the current result's data with additional data. Only works with - * successful results. - * - * @param array $additionalData Data to merge - * @return static A new successful ActionResult with merged data - * - * @throws \LogicException If called on a failed result - * - * @example Merging results - * ```php - * $result1 = ActionResult::success(['user_id' => 123]); - * $result2 = $result1->mergeData(['email' => 'user@example.com']); - * - * // result2 now contains: ['user_id' => 123, 'email' => 'user@example.com'] - * ``` - */ - public function mergeData(array $additionalData): self - { - if ($this->isFailure()) { - throw new \LogicException('Cannot merge data on a failed ActionResult'); - } - - return self::success( - array_merge($this->data, $additionalData), - $this->metadata - ); - } -} diff --git a/packages/workflow-engine-core/src/Core/DefinitionParser.php b/packages/workflow-engine-core/src/Core/DefinitionParser.php deleted file mode 100644 index 39ad9cf..0000000 --- a/packages/workflow-engine-core/src/Core/DefinitionParser.php +++ /dev/null @@ -1,556 +0,0 @@ -parse([ - * 'name' => 'user-onboarding', - * 'version' => '1.0', - * 'steps' => [ - * ['id' => 'welcome', 'action' => 'log', 'parameters' => ['message' => 'Welcome!']], - * ['id' => 'profile', 'action' => 'SaveProfileAction', 'timeout' => '30s'] - * ], - * 'transitions' => [ - * ['from' => 'welcome', 'to' => 'profile'] - * ] - * ]); - * ``` - * - * ### Parse JSON Definition - * ```php - * $json = file_get_contents('workflow.json'); - * $definition = $parser->parse($json); - * ``` - * - * ### With Error Handling - * ```php - * try { - * $definition = $parser->parse($rawDefinition); - * // Use the validated definition... - * } catch (InvalidWorkflowDefinitionException $e) { - * // Handle validation errors with detailed context - * echo "Validation failed: " . $e->getMessage(); - * echo "Context: " . json_encode($e->getContext()); - * } - * ``` - * - * ## Validation Rules - * - * ### Required Fields - * - `name`: Non-empty string workflow identifier - * - `steps`: Array of step definitions with at least one step - * - * ### Step Validation - * - Each step must have a unique ID - * - Action class (if specified) must be a valid string - * - Timeout format: `/^\d+[smhd]$/` (e.g., "30s", "5m", "2h", "1d") - * - Retry attempts: Non-negative integer - * - * ### Transition Validation - * - Must have both `from` and `to` fields - * - Referenced steps must exist in the workflow - * - No circular dependencies (future enhancement) - * - * @see WorkflowDefinition For the resulting validated definition object - * @see WorkflowBuilder For fluent definition creation - * @see InvalidWorkflowDefinitionException For validation error details - */ -class DefinitionParser -{ - /** - * Parse a workflow definition from array or JSON string format. - * - * Converts raw workflow definition data into a validated WorkflowDefinition - * object. Supports both array and JSON string input formats with comprehensive - * validation and normalization. - * - * @param array|string $definition Raw workflow definition data - * @return WorkflowDefinition Validated and normalized workflow definition - * - * @throws InvalidWorkflowDefinitionException If definition structure is invalid - * - * @example Parse from array - * ```php - * $definition = $parser->parse([ - * 'name' => 'order-processing', - * 'version' => '2.0', - * 'steps' => [ - * ['id' => 'validate', 'action' => 'ValidateOrderAction'], - * ['id' => 'charge', 'action' => 'ChargePaymentAction', 'timeout' => '30s'], - * ['id' => 'fulfill', 'action' => 'FulfillOrderAction'] - * ], - * 'transitions' => [ - * ['from' => 'validate', 'to' => 'charge'], - * ['from' => 'charge', 'to' => 'fulfill'] - * ], - * 'metadata' => ['department' => 'orders', 'priority' => 'high'] - * ]); - * ``` - * @example Parse from JSON - * ```php - * $json = '{"name":"user-signup","steps":[{"id":"welcome","action":"log"}]}'; - * $definition = $parser->parse($json); - * ``` - */ - public function parse(array|string $definition): WorkflowDefinition - { - // Handle JSON string input - if (is_string($definition)) { - $decoded = json_decode($definition, true); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new InvalidWorkflowDefinitionException( - 'Invalid JSON workflow definition: '.json_last_error_msg(), - ['json_error_code' => json_last_error(), 'raw_definition' => substr($definition, 0, 200)], - ['JSON parsing failed with error code: '.json_last_error()] - ); - } - $definition = $decoded; - } - - // Validate the complete definition structure - $this->validateDefinition($definition); - - // Normalize steps before creating WorkflowDefinition - $definition['steps'] = $this->normalizeSteps($definition['steps']); - - return WorkflowDefinition::fromArray($definition); - } - - /** - * Validate the complete workflow definition structure. - * - * Performs comprehensive validation of the workflow definition including - * required fields, data types, step validation, and transition validation. - * Throws detailed exceptions with context for any validation failures. - * - * @param array $definition The workflow definition to validate - * - * @throws InvalidWorkflowDefinitionException If any validation rules fail - * - * @internal Called during definition parsing - */ - private function validateDefinition(array $definition): void - { - // Validate required name field - if (! isset($definition['name'])) { - throw InvalidWorkflowDefinitionException::missingRequiredField('name', $definition); - } - - if (! is_string($definition['name']) || empty(trim($definition['name']))) { - throw InvalidWorkflowDefinitionException::invalidName($definition['name']); - } - - // Validate required steps field - if (! isset($definition['steps'])) { - throw InvalidWorkflowDefinitionException::missingRequiredField('steps', $definition); - } - - if (! is_array($definition['steps'])) { - throw new InvalidWorkflowDefinitionException( - 'Workflow definition steps must be an array', - $definition, - ['Expected array for steps field, got: '.gettype($definition['steps'])] - ); - } - - if (empty($definition['steps'])) { - throw InvalidWorkflowDefinitionException::emptyWorkflow($definition['name']); - } - - // Normalize steps format - convert sequential array to associative if needed - $steps = $this->normalizeSteps($definition['steps']); - - // Validate each step - foreach ($steps as $stepId => $stepData) { - $this->validateStep($stepId, $stepData, $definition); - } - - // Validate transitions if present - if (isset($definition['transitions']) && is_array($definition['transitions'])) { - foreach ($definition['transitions'] as $transitionIndex => $transition) { - $this->validateTransition($transition, $steps, $transitionIndex); - } - } - - // Validate optional version field - if (isset($definition['version']) && ! is_string($definition['version'])) { - throw new InvalidWorkflowDefinitionException( - 'Workflow version must be a string', - $definition, - ['Expected string for version field, got: '.gettype($definition['version'])] - ); - } - - // Validate optional metadata field - if (isset($definition['metadata']) && ! is_array($definition['metadata'])) { - throw new InvalidWorkflowDefinitionException( - 'Workflow metadata must be an array', - $definition, - ['Expected array for metadata field, got: '.gettype($definition['metadata'])] - ); - } - } - - /** - * Normalize step definitions to consistent associative array format. - * - * Converts various step input formats to a standardized associative array - * where step IDs are keys and step configurations are values. Handles both - * sequential arrays with 'id' properties and pre-normalized associative arrays. - * - * @param array $steps Raw steps array in various formats - * @return array> Normalized steps indexed by ID - * - * @throws InvalidWorkflowDefinitionException If step structure is invalid - * - * @example Input formats - * ```php - * // Sequential array with ID properties (will be normalized) - * $steps = [ - * ['id' => 'step1', 'action' => 'LogAction', 'timeout' => '30s'], - * ['id' => 'step2', 'action' => 'EmailAction'] - * ]; - * - * // Already associative (returned as-is) - * $steps = [ - * 'step1' => ['action' => 'LogAction', 'timeout' => '30s'], - * 'step2' => ['action' => 'EmailAction'] - * ]; - * ``` - * - * @internal Used during definition validation and parsing - */ - private function normalizeSteps(array $steps): array - { - // If steps is already associative, return as-is - if (! array_is_list($steps)) { - // Type hint: already associative array with string keys - /** @var array> $steps */ - return $steps; - } - - // Convert sequential array with 'id' property to associative - $normalizedSteps = []; - foreach ($steps as $index => $step) { - if (! is_array($step)) { - throw new InvalidWorkflowDefinitionException( - "Step at index {$index} must be an array", - ['invalid_step' => $step, 'step_index' => $index], - ["Expected array for step {$index}, got: ".gettype($step)] - ); - } - - if (! isset($step['id'])) { - throw new InvalidWorkflowDefinitionException( - "Step at index {$index} must have an 'id' property when using sequential array format", - ['step_data' => $step, 'step_index' => $index], - ['Missing required id field in step array'] - ); - } - - $stepId = $step['id']; - if (! is_string($stepId) || empty(trim($stepId))) { - throw InvalidWorkflowDefinitionException::invalidStepId($stepId); - } - - // Check for duplicate step IDs - if (isset($normalizedSteps[$stepId])) { - throw InvalidWorkflowDefinitionException::duplicateStepId($stepId); - } - - // Remove id from step data since it's now the key - unset($step['id']); - $normalizedSteps[$stepId] = $step; - } - - return $normalizedSteps; - } - - /** - * Validate an individual step configuration. - * - * Performs detailed validation of a single step including ID format, - * action class validity, timeout format, retry configuration, and - * other step-specific properties. - * - * @param string $stepId The step identifier - * @param array $stepData The step configuration data - * @param array $fullDefinition Complete workflow definition for context - * - * @throws InvalidWorkflowDefinitionException If step configuration is invalid - * - * @example Valid step configurations - * ```php - * // Basic step - * $step = ['action' => 'LogAction', 'parameters' => ['message' => 'Hello']]; - * - * // Step with timeout and retry - * $step = [ - * 'action' => 'EmailAction', - * 'timeout' => '30s', - * 'retry_attempts' => 3, - * 'parameters' => ['to' => 'user@example.com'] - * ]; - * - * // Conditional step - * $step = [ - * 'action' => 'PaymentAction', - * 'conditions' => ['payment.method === "credit_card"'] - * ]; - * ``` - * - * @internal Called during definition validation - */ - private function validateStep(string $stepId, array $stepData, array $fullDefinition): void - { - if (empty($stepId)) { - throw InvalidWorkflowDefinitionException::invalidStepId($stepId); - } - - // Validate step ID format (alphanumeric, hyphens, underscores only) - if (! preg_match('/^[a-zA-Z][a-zA-Z0-9_-]*$/', $stepId)) { - throw InvalidWorkflowDefinitionException::invalidStepId($stepId); - } - - // Action is optional for some step types (like manual steps or conditions) - if (isset($stepData['action'])) { - if (! is_string($stepData['action'])) { - throw new InvalidWorkflowDefinitionException( - "Step '{$stepId}' action must be a string", - $fullDefinition, - ["Expected string for action in step {$stepId}, got: ".gettype($stepData['action'])] - ); - } - - if (empty(trim($stepData['action']))) { - throw new InvalidWorkflowDefinitionException( - "Step '{$stepId}' action cannot be empty", - $fullDefinition, - ['Action field is empty or whitespace only'] - ); - } - } - - // Validate timeout format if present - if (isset($stepData['timeout'])) { - if (! is_string($stepData['timeout']) || ! $this->isValidTimeout($stepData['timeout'])) { - throw new InvalidWorkflowDefinitionException( - "Step '{$stepId}' has invalid timeout format. Expected format: number followed by s/m/h/d (e.g., '30s', '5m')", - $fullDefinition, - [ - "Invalid timeout: {$stepData['timeout']}", - 'Valid formats: "30s", "5m", "2h", "1d"', - ] - ); - } - } - - // Validate retry attempts - if (isset($stepData['retry_attempts'])) { - if (! is_int($stepData['retry_attempts']) || $stepData['retry_attempts'] < 0) { - throw InvalidWorkflowDefinitionException::invalidRetryAttempts($stepData['retry_attempts']); - } - } - - // Validate parameters field if present - if (isset($stepData['parameters']) && ! is_array($stepData['parameters'])) { - throw new InvalidWorkflowDefinitionException( - "Step '{$stepId}' parameters must be an array", - $fullDefinition, - ["Expected array for parameters in step {$stepId}, got: ".gettype($stepData['parameters'])] - ); - } - - // Validate conditions field if present - if (isset($stepData['conditions'])) { - if (! is_array($stepData['conditions'])) { - throw new InvalidWorkflowDefinitionException( - "Step '{$stepId}' conditions must be an array", - $fullDefinition, - ["Expected array for conditions in step {$stepId}, got: ".gettype($stepData['conditions'])] - ); - } - - foreach ($stepData['conditions'] as $conditionIndex => $condition) { - if (! is_string($condition) || empty(trim($condition))) { - throw InvalidWorkflowDefinitionException::invalidCondition($condition); - } - } - } - } - - /** - * Validate a workflow transition definition. - * - * Ensures transition definitions have required fields and reference - * valid steps that exist in the workflow. Validates transition structure - * and provides detailed error context for debugging. - * - * @param array $transition The transition definition to validate - * @param array> $steps Available workflow steps - * @param int $transitionIndex Index of transition for error context - * - * @throws InvalidWorkflowDefinitionException If transition is invalid - * - * @example Valid transition formats - * ```php - * // Basic transition - * $transition = ['from' => 'step1', 'to' => 'step2']; - * - * // Conditional transition - * $transition = [ - * 'from' => 'payment', - * 'to' => 'fulfillment', - * 'condition' => 'payment.status === "success"' - * ]; - * - * // Transition with metadata - * $transition = [ - * 'from' => 'review', - * 'to' => 'approved', - * 'metadata' => ['requires_manager_approval' => true] - * ]; - * ``` - * - * @internal Called during definition validation - */ - private function validateTransition(array $transition, array $steps, int $transitionIndex): void - { - // Validate required 'from' field - if (! isset($transition['from'])) { - throw new InvalidWorkflowDefinitionException( - "Transition at index {$transitionIndex} must have a 'from' field", - ['transition' => $transition, 'available_steps' => array_keys($steps)], - ['Missing required from field in transition'] - ); - } - - // Validate required 'to' field - if (! isset($transition['to'])) { - throw new InvalidWorkflowDefinitionException( - "Transition at index {$transitionIndex} must have a 'to' field", - ['transition' => $transition, 'available_steps' => array_keys($steps)], - ['Missing required to field in transition'] - ); - } - - $fromStep = $transition['from']; - $toStep = $transition['to']; - - // Validate 'from' field type and value - if (! is_string($fromStep) || empty(trim($fromStep))) { - throw new InvalidWorkflowDefinitionException( - "Transition 'from' field must be a non-empty string", - ['transition' => $transition, 'from_value' => $fromStep], - ['Invalid from field: '.var_export($fromStep, true)] - ); - } - - // Validate 'to' field type and value - if (! is_string($toStep) || empty(trim($toStep))) { - throw new InvalidWorkflowDefinitionException( - "Transition 'to' field must be a non-empty string", - ['transition' => $transition, 'to_value' => $toStep], - ['Invalid to field: '.var_export($toStep, true)] - ); - } - - // Validate that referenced steps exist - if (! isset($steps[$fromStep])) { - throw new InvalidWorkflowDefinitionException( - "Transition references unknown source step: '{$fromStep}'", - [ - 'transition' => $transition, - 'missing_step' => $fromStep, - 'available_steps' => array_keys($steps), - ], - [ - "Source step '{$fromStep}' does not exist in workflow", - 'Available steps: '.implode(', ', array_keys($steps)), - ] - ); - } - - if (! isset($steps[$toStep])) { - throw new InvalidWorkflowDefinitionException( - "Transition references unknown target step: '{$toStep}'", - [ - 'transition' => $transition, - 'missing_step' => $toStep, - 'available_steps' => array_keys($steps), - ], - [ - "Target step '{$toStep}' does not exist in workflow", - 'Available steps: '.implode(', ', array_keys($steps)), - ] - ); - } - - // Validate optional condition field - if (isset($transition['condition'])) { - if (! is_string($transition['condition']) || empty(trim($transition['condition']))) { - throw InvalidWorkflowDefinitionException::invalidCondition($transition['condition']); - } - } - - // Validate optional metadata field - if (isset($transition['metadata']) && ! is_array($transition['metadata'])) { - throw new InvalidWorkflowDefinitionException( - 'Transition metadata must be an array', - ['transition' => $transition], - ['Expected array for metadata, got: '.gettype($transition['metadata'])] - ); - } - } - - /** - * Validate timeout string format. - * - * Checks if a timeout string follows the expected format of a number - * followed by a time unit (s=seconds, m=minutes, h=hours, d=days). - * - * @param string $timeout The timeout string to validate - * @return bool True if format is valid, false otherwise - * - * @example Valid timeout formats - * ```php - * $parser->isValidTimeout('30s'); // true - 30 seconds - * $parser->isValidTimeout('5m'); // true - 5 minutes - * $parser->isValidTimeout('2h'); // true - 2 hours - * $parser->isValidTimeout('1d'); // true - 1 day - * $parser->isValidTimeout('10'); // false - missing unit - * $parser->isValidTimeout('abc'); // false - invalid format - * ``` - * - * @internal Used for step timeout validation - */ - private function isValidTimeout(string $timeout): bool - { - // Valid formats: "30s", "5m", "2h", "1d" - // Must be: one or more digits followed by exactly one time unit - return preg_match('/^\d+[smhd]$/', $timeout) === 1; - } -} diff --git a/packages/workflow-engine-core/src/Core/Executor.php b/packages/workflow-engine-core/src/Core/Executor.php deleted file mode 100644 index c87a626..0000000 --- a/packages/workflow-engine-core/src/Core/Executor.php +++ /dev/null @@ -1,327 +0,0 @@ -execute($workflowInstance); - * - * // The executor will: - * // 1. Process all pending steps - * // 2. Execute actions in sequence - * // 3. Handle errors and retries - * // 4. Update workflow state - * // 5. Dispatch appropriate events - * ``` - * @example Error handling during execution - * ```php - * try { - * $executor->execute($instance); - * } catch (StepExecutionException $e) { - * // Handle step-specific errors - * echo "Step failed: " . $e->getStep()->getId(); - * echo "Context: " . json_encode($e->getContext()); - * } catch (ActionNotFoundException $e) { - * // Handle missing action classes - * echo "Missing action: " . $e->getActionClass(); - * } - * ``` - */ -class Executor -{ - /** - * State manager for persisting workflow state changes. - */ - private readonly StateManager $stateManager; - - /** - * Event dispatcher for workflow and step events. - */ - private readonly EventDispatcher $eventDispatcher; - - /** - * Create a new workflow executor. - * - * @param StateManager $stateManager The state manager for workflow persistence - * @param EventDispatcher|null $eventDispatcher Optional event dispatcher for workflow events - * - * @example Basic setup - * ```php - * $executor = new Executor( - * new StateManager($storageAdapter), - * app(EventDispatcher::class) - * ); - * ``` - */ - public function __construct( - StateManager $stateManager, - ?EventDispatcher $eventDispatcher = null - ) { - $this->stateManager = $stateManager; - $this->eventDispatcher = $eventDispatcher ?? app(EventDispatcher::class); - } - - /** - * Execute a workflow instance by processing all pending steps. - * - * This method orchestrates the complete workflow execution, handling state transitions, - * step execution, error handling, and event dispatching. It processes steps in sequence - * and manages the workflow lifecycle from start to completion. - * - * @param WorkflowInstance $instance The workflow instance to execute - * - * @throws StepExecutionException If a step fails during execution - * @throws ActionNotFoundException If a required action class is not found - * - * @example Executing a workflow - * ```php - * $instance = $stateManager->load('workflow-123'); - * $executor->execute($instance); - * - * // The instance state will be updated automatically - * echo $instance->getState()->value; // 'completed' or 'failed' - * ``` - */ - public function execute(WorkflowInstance $instance): void - { - try { - $this->processWorkflow($instance); - } catch (Exception $e) { - Log::error('Workflow execution failed', [ - 'workflow_id' => $instance->getId(), - 'workflow_name' => $instance->getDefinition()->getName(), - 'current_step' => $instance->getCurrentStepId(), - 'error' => $e->getMessage(), - 'exception_type' => get_class($e), - 'trace' => $e->getTraceAsString(), - ]); - - $this->stateManager->setError($instance, $e->getMessage()); - $this->eventDispatcher->dispatch(new WorkflowFailedEvent($instance, $e)); - - // Re-throw the original exception to maintain the error context - throw $e; - } - } - - /** - * Process workflow execution by managing state transitions and step execution. - * - * This private method handles the core workflow processing logic, including - * state management, step scheduling, and completion detection. - * - * @param WorkflowInstance $instance The workflow instance to process - * - * @throws StepExecutionException If step execution fails - * @throws ActionNotFoundException If required action classes are missing - */ - private function processWorkflow(WorkflowInstance $instance): void - { - // If workflow is not running, start it - if ($instance->getState() === WorkflowState::PENDING) { - $instance->setState(WorkflowState::RUNNING); - $this->stateManager->save($instance); - } - - // Get next steps to execute - $nextSteps = $instance->getNextSteps(); - - if (empty($nextSteps)) { - // Workflow completed successfully - $instance->setState(WorkflowState::COMPLETED); - $this->stateManager->save($instance); - $this->eventDispatcher->dispatch(new WorkflowCompletedEvent($instance)); - - Log::info('Workflow completed successfully', [ - 'workflow_id' => $instance->getId(), - 'workflow_name' => $instance->getDefinition()->getName(), - 'completed_steps' => count($instance->getCompletedSteps()), - 'execution_time' => $instance->getCreatedAt()->diffInSeconds($instance->getUpdatedAt()).'s', - ]); - - return; - } - - // Execute each next step - foreach ($nextSteps as $step) { - if ($instance->isStepCompleted($step->getId())) { - continue; // Skip already completed steps - } - - if (! $instance->canExecuteStep($step->getId())) { - continue; // Skip steps that can't be executed yet - } - - $this->executeStep($instance, $step); - } - } - - /** - * Execute a single workflow step. - * - * Handles the complete lifecycle of step execution including action execution, - * error handling, state updates, and event dispatching. Provides detailed - * error context for debugging and monitoring. - * - * @param WorkflowInstance $instance The workflow instance - * @param Step $step The step to execute - * - * @throws StepExecutionException If the step fails to execute - * @throws ActionNotFoundException If the action class doesn't exist - */ - private function executeStep(WorkflowInstance $instance, Step $step): void - { - Log::info('Executing workflow step', [ - 'workflow_id' => $instance->getId(), - 'workflow_name' => $instance->getDefinition()->getName(), - 'step_id' => $step->getId(), - 'action_class' => $step->getActionClass(), - 'step_config' => $step->getConfig(), - ]); - - $instance->setCurrentStepId($step->getId()); - $this->stateManager->save($instance); - - try { - if ($step->hasAction()) { - $this->executeAction($instance, $step); - } - - // Mark step as completed - $this->stateManager->markStepCompleted($instance, $step->getId()); - $this->eventDispatcher->dispatch(new StepCompletedEvent($instance, $step)); - - Log::info('Workflow step completed successfully', [ - 'workflow_id' => $instance->getId(), - 'step_id' => $step->getId(), - 'step_duration' => 'calculated_in_future_version', // TODO: Add timing - ]); - - // Continue execution recursively - $this->processWorkflow($instance); - - } catch (Exception $e) { - $context = new WorkflowContext( - workflowId: $instance->getId(), - stepId: $step->getId(), - data: $instance->getData(), - config: $step->getConfig(), - instance: $instance - ); - - // Create detailed step execution exception - $stepException = match (true) { - $e instanceof ActionNotFoundException => $e, - str_contains($e->getMessage(), 'does not exist') => ActionNotFoundException::classNotFound($step->getActionClass(), $step, $context), - str_contains($e->getMessage(), 'must implement') => ActionNotFoundException::invalidInterface($step->getActionClass(), $step, $context), - default => StepExecutionException::fromException($e, $step, $context) - }; - - Log::error('Workflow step execution failed', [ - 'workflow_id' => $instance->getId(), - 'step_id' => $step->getId(), - 'action_class' => $step->getActionClass(), - 'error_type' => get_class($e), - 'error_message' => $e->getMessage(), - 'step_config' => $step->getConfig(), - 'context_data' => $instance->getData(), - ]); - - $this->stateManager->markStepFailed($instance, $step->getId(), $stepException->getMessage()); - $this->eventDispatcher->dispatch(new StepFailedEvent($instance, $step, $stepException)); - - // Propagate the enhanced exception - throw $stepException; - } - } - - /** - * Execute the action associated with a workflow step. - * - * Handles action instantiation, validation, execution, and result processing. - * Provides comprehensive error handling for missing classes, interface compliance, - * and execution failures. - * - * @param WorkflowInstance $instance The workflow instance - * @param Step $step The step containing the action to execute - * - * @throws ActionNotFoundException If the action class doesn't exist or implement the interface - * @throws StepExecutionException If action execution fails - */ - private function executeAction(WorkflowInstance $instance, Step $step): void - { - $actionClass = $step->getActionClass(); - - if (! class_exists($actionClass)) { - $context = new WorkflowContext( - workflowId: $instance->getId(), - stepId: $step->getId(), - data: $instance->getData(), - config: $step->getConfig(), - instance: $instance - ); - - throw ActionNotFoundException::classNotFound($actionClass, $step, $context); - } - - $action = app($actionClass, ['config' => $step->getConfig()]); - - if (! $action instanceof WorkflowAction) { - $context = new WorkflowContext( - workflowId: $instance->getId(), - stepId: $step->getId(), - data: $instance->getData(), - config: $step->getConfig(), - instance: $instance - ); - - throw ActionNotFoundException::invalidInterface($actionClass, $step, $context); - } - - $context = new WorkflowContext( - workflowId: $instance->getId(), - stepId: $step->getId(), - data: $instance->getData(), - config: $step->getConfig(), - instance: $instance - ); - - $result = $action->execute($context); - - if ($result->isSuccess()) { - // Merge any output data from the action - if ($result->hasData()) { - $instance->mergeData($result->getData()); - $this->stateManager->save($instance); - } - } else { - throw StepExecutionException::actionFailed( - $result->getErrorMessage() ?? 'Action execution failed without specific error message', - $step, - $context - ); - } - } -} diff --git a/packages/workflow-engine-core/src/Core/StateManager.php b/packages/workflow-engine-core/src/Core/StateManager.php deleted file mode 100644 index 50aea12..0000000 --- a/packages/workflow-engine-core/src/Core/StateManager.php +++ /dev/null @@ -1,326 +0,0 @@ -save($instance); - * - * // Load workflow instance - * $instance = $stateManager->load('workflow-123'); - * - * // Update workflow state - * $stateManager->updateState($instance, WorkflowState::COMPLETED); - * ``` - * @example Step management - * ```php - * // Mark step as completed - * $stateManager->markStepCompleted($instance, 'send-email'); - * - * // Mark step as failed - * $stateManager->markStepFailed($instance, 'payment', 'Payment gateway timeout'); - * - * // Set current step - * $stateManager->setCurrentStep($instance, 'next-step'); - * ``` - */ -class StateManager -{ - /** - * The storage adapter for workflow persistence. - */ - private readonly StorageAdapter $storage; - - /** - * Create a new state manager. - * - * @param StorageAdapter $storage The storage adapter for workflow persistence - * - * @example Creating with database storage - * ```php - * $stateManager = new StateManager( - * new DatabaseStorageAdapter($connection) - * ); - * ``` - */ - public function __construct(StorageAdapter $storage) - { - $this->storage = $storage; - } - - /** - * Save a workflow instance to storage. - * - * Persists the complete workflow instance state including current step, - * completed steps, data, and metadata to the underlying storage system. - * - * @param WorkflowInstance $instance The workflow instance to save - * - * @throws \Exception If the storage operation fails - * - * @example Saving workflow state - * ```php - * $instance->setState(WorkflowState::RUNNING); - * $instance->setCurrentStepId('process-payment'); - * $stateManager->save($instance); - * ``` - */ - public function save(WorkflowInstance $instance): void - { - $this->storage->save($instance); - } - - /** - * Load a workflow instance from storage by ID. - * - * Retrieves a complete workflow instance including its definition, - * current state, execution history, and context data. - * - * @param string $instanceId The workflow instance ID to load - * @return WorkflowInstance The loaded workflow instance - * - * @throws WorkflowInstanceNotFoundException If the workflow instance doesn't exist - * - * @example Loading a workflow - * ```php - * try { - * $instance = $stateManager->load('workflow-123'); - * echo "Current state: " . $instance->getState()->value; - * } catch (WorkflowInstanceNotFoundException $e) { - * echo "Workflow not found: " . $e->getUserMessage(); - * } - * ``` - */ - public function load(string $instanceId): WorkflowInstance - { - if (! $this->storage->exists($instanceId)) { - throw WorkflowInstanceNotFoundException::notFound($instanceId, $this->storage::class); - } - - return $this->storage->load($instanceId); - } - - /** - * Update the state of a workflow instance. - * - * Changes the workflow state and persists the change to storage. - * This method handles state transition validation and ensures - * the change is properly recorded. - * - * @param WorkflowInstance $instance The workflow instance to update - * @param WorkflowState $newState The new state to set - * - * @throws \Exception If the storage operation fails - * - * @example Completing a workflow - * ```php - * $stateManager->updateState($instance, WorkflowState::COMPLETED); - * ``` - */ - public function updateState(WorkflowInstance $instance, WorkflowState $newState): void - { - $instance->setState($newState); - $this->save($instance); - } - - /** - * Update the data/context of a workflow instance. - * - * Merges new data with existing workflow context and persists - * the updated instance to storage. - * - * @param WorkflowInstance $instance The workflow instance to update - * @param array $data The data to merge with existing context - * - * @throws \Exception If the storage operation fails - * - * @example Adding user context - * ```php - * $stateManager->updateData($instance, [ - * 'user_id' => 123, - * 'preferences' => ['email_notifications' => true] - * ]); - * ``` - */ - public function updateData(WorkflowInstance $instance, array $data): void - { - $instance->mergeData($data); - $this->save($instance); - } - - /** - * Set the current step for a workflow instance. - * - * Updates the workflow's current step pointer and persists the change. - * This is typically called during step transitions. - * - * @param WorkflowInstance $instance The workflow instance to update - * @param string|null $stepId The step ID to set as current (null for no current step) - * - * @throws \Exception If the storage operation fails - * - * @example Moving to next step - * ```php - * $stateManager->setCurrentStep($instance, 'process-payment'); - * ``` - */ - public function setCurrentStep(WorkflowInstance $instance, ?string $stepId): void - { - $instance->setCurrentStepId($stepId); - $this->save($instance); - } - - /** - * Mark a workflow step as completed. - * - * Records step completion in the workflow instance and persists - * the change to storage. This tracks execution progress. - * - * @param WorkflowInstance $instance The workflow instance - * @param string $stepId The ID of the completed step - * - * @throws \Exception If the storage operation fails - * - * @example Marking step completion - * ```php - * $stateManager->markStepCompleted($instance, 'send-welcome-email'); - * ``` - */ - public function markStepCompleted(WorkflowInstance $instance, string $stepId): void - { - $instance->addCompletedStep($stepId); - $this->save($instance); - } - - /** - * Mark a workflow step as failed. - * - * Records step failure with error details in the workflow instance - * and persists the change to storage. This maintains error history. - * - * @param WorkflowInstance $instance The workflow instance - * @param string $stepId The ID of the failed step - * @param string $error The error message describing the failure - * - * @throws \Exception If the storage operation fails - * - * @example Recording step failure - * ```php - * $stateManager->markStepFailed( - * $instance, - * 'payment-processing', - * 'Payment gateway timeout after 30 seconds' - * ); - * ``` - */ - public function markStepFailed(WorkflowInstance $instance, string $stepId, string $error): void - { - $instance->addFailedStep($stepId, $error); - $this->save($instance); - } - - /** - * Set an error message and fail the workflow. - * - * Updates the workflow with an error message and sets the state to FAILED. - * This is typically called when a workflow encounters an unrecoverable error. - * - * @param WorkflowInstance $instance The workflow instance - * @param string $error The error message describing the failure - * - * @throws \Exception If the storage operation fails - * - * @example Failing a workflow - * ```php - * $stateManager->setError($instance, 'Critical dependency service unavailable'); - * ``` - */ - public function setError(WorkflowInstance $instance, string $error): void - { - $instance->setErrorMessage($error); - $instance->setState(WorkflowState::FAILED); - $this->save($instance); - } - - /** - * Find workflow instances matching the given criteria. - * - * Searches for workflow instances based on filtering criteria such as - * state, workflow name, creation date, etc. The exact criteria supported - * depend on the storage adapter implementation. - * - * @param array $criteria Search criteria for filtering instances - * @return WorkflowInstance[] Array of matching workflow instances - * - * @throws \Exception If the search operation fails - * - * @example Finding failed workflows - * ```php - * $failedWorkflows = $stateManager->findInstances([ - * 'state' => WorkflowState::FAILED, - * 'created_after' => '2024-01-01' - * ]); - * ``` - */ - public function findInstances(array $criteria = []): array - { - return $this->storage->findInstances($criteria); - } - - /** - * Delete a workflow instance from storage. - * - * Permanently removes a workflow instance and all its associated data - * from the storage system. This operation cannot be undone. - * - * @param string $instanceId The workflow instance ID to delete - * - * @throws \Exception If the delete operation fails - * - * @example Cleaning up old workflows - * ```php - * $stateManager->delete('old-workflow-123'); - * ``` - */ - public function delete(string $instanceId): void - { - $this->storage->delete($instanceId); - } - - /** - * Check if a workflow instance exists in storage. - * - * Verifies whether a workflow instance with the given ID exists - * without loading the full instance data. - * - * @param string $instanceId The workflow instance ID to check - * @return bool True if the instance exists, false otherwise - * - * @example Checking instance existence - * ```php - * if ($stateManager->exists('workflow-123')) { - * $instance = $stateManager->load('workflow-123'); - * } else { - * echo "Workflow not found"; - * } - * ``` - */ - public function exists(string $instanceId): bool - { - return $this->storage->exists($instanceId); - } -} diff --git a/packages/workflow-engine-core/src/Core/Step.php b/packages/workflow-engine-core/src/Core/Step.php deleted file mode 100644 index 7aca15d..0000000 --- a/packages/workflow-engine-core/src/Core/Step.php +++ /dev/null @@ -1,271 +0,0 @@ - 'welcome', 'to' => 'user@example.com'] - * ); - * ``` - * - * ### Step with Resilience Configuration - * ```php - * $step = new Step( - * id: 'payment_processing', - * actionClass: ProcessPaymentAction::class, - * config: ['gateway' => 'stripe'], - * timeout: '300', // 5 minutes - * retryAttempts: 3 - * ); - * ``` - * - * ### Conditional Step - * ```php - * $step = new Step( - * id: 'premium_features', - * actionClass: EnablePremiumAction::class, - * conditions: ['user.plan === "premium"'] - * ); - * ``` - * - * @see WorkflowAction For action implementation interface - * @see WorkflowDefinition For step orchestration and transitions - */ -class Step -{ - /** - * Create a new workflow step with comprehensive configuration. - * - * @param string $id Unique step identifier within the workflow - * @param string|null $actionClass Fully qualified action class name - * @param array $config Step-specific configuration parameters - * @param string|null $timeout Maximum execution time (in seconds as string) - * @param int $retryAttempts Number of retry attempts on failure (0-10) - * @param string|null $compensationAction Action class for rollback scenarios - * @param array $conditions Array of condition expressions for conditional execution - * @param array $prerequisites Array of prerequisite step IDs that must complete first - */ - public function __construct( - private readonly string $id, - private readonly ?string $actionClass = null, - private readonly array $config = [], - private readonly ?string $timeout = null, - private readonly int $retryAttempts = 0, - private readonly ?string $compensationAction = null, - private readonly array $conditions = [], - private readonly array $prerequisites = [] - ) {} - - /** - * Get the unique step identifier. - * - * @return string The step ID within the workflow - */ - public function getId(): string - { - return $this->id; - } - - /** - * Get the action class name for this step. - * - * @return string|null Fully qualified action class name, or null for no-op steps - */ - public function getActionClass(): ?string - { - return $this->actionClass; - } - - /** - * Get the step-specific configuration parameters. - * - * @return array Configuration array passed to the action - */ - public function getConfig(): array - { - return $this->config; - } - - /** - * Get the maximum execution timeout for this step. - * - * @return string|null Timeout in seconds as string, or null for no timeout - */ - public function getTimeout(): ?string - { - return $this->timeout; - } - - /** - * Get the number of retry attempts on failure. - * - * @return int Number of retries (0 means no retries) - */ - public function getRetryAttempts(): int - { - return $this->retryAttempts; - } - - /** - * Get the compensation action class for rollback scenarios. - * - * @return string|null Compensation action class name, or null if none - */ - public function getCompensationAction(): ?string - { - return $this->compensationAction; - } - - /** - * Get the conditional expressions for this step. - * - * @return array Array of condition expressions that must all be true - */ - public function getConditions(): array - { - return $this->conditions; - } - - /** - * Get the prerequisite step IDs that must complete before this step. - * - * @return array Array of step IDs that are prerequisites - */ - public function getPrerequisites(): array - { - return $this->prerequisites; - } - - /** - * Check if this step has an associated action to execute. - * - * @return bool True if an action class is defined - */ - public function hasAction(): bool - { - return $this->actionClass !== null; - } - - /** - * Check if this step has a compensation action for rollback. - * - * @return bool True if a compensation action is defined - */ - public function hasCompensation(): bool - { - return $this->compensationAction !== null; - } - - /** - * Determine if this step can execute based on its conditions. - * - * Evaluates all condition expressions against the provided data. - * All conditions must be true for the step to be executable. - * - * @param array $data Workflow data to evaluate conditions against - * @return bool True if all conditions pass (or no conditions exist) - * - * @example - * ```php - * $step = new Step( - * id: 'premium_step', - * actionClass: PremiumAction::class, - * conditions: ['user.plan === "premium"', 'user.active === true'] - * ); - * - * $data = ['user' => ['plan' => 'premium', 'active' => true]]; - * $canExecute = $step->canExecute($data); // true - * ``` - */ - public function canExecute(array $data): bool - { - foreach ($this->conditions as $condition) { - if (! $this->evaluateCondition($condition, $data)) { - return false; - } - } - - return true; - } - - /** - * Evaluate a single condition expression against workflow data. - * - * Supports basic comparison operators and nested property access using dot notation. - * - * @param string $condition Condition expression (e.g., "user.age >= 18") - * @param array $data Workflow data to evaluate against - * @return bool True if condition evaluates to true - * - * @internal This method handles condition parsing and evaluation - */ - private function evaluateCondition(string $condition, array $data): bool - { - // Enhanced condition evaluation with support for more operators - if (preg_match('/(\w+(?:\.\w+)*)\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)/', $condition, $matches)) { - $key = $matches[1]; - $operator = $matches[2]; - $value = trim($matches[3], '"\''); - - $dataValue = data_get($data, $key); - - return match ($operator) { - '===' => $dataValue === $value, - '!==' => $dataValue !== $value, - '==' => $dataValue == $value, - '!=' => $dataValue != $value, - '>' => $dataValue > $value, - '<' => $dataValue < $value, - '>=' => $dataValue >= $value, - '<=' => $dataValue <= $value, - default => false, - }; - } - - return true; // Default to true if condition cannot be parsed - } - - /** - * Convert the step to an array representation for serialization. - * - * @return array Array representation suitable for JSON encoding - * - * @example - * ```php - * $stepArray = $step->toArray(); - * $json = json_encode($stepArray); - * ``` - */ - public function toArray(): array - { - return [ - 'id' => $this->id, - 'action' => $this->actionClass, - 'config' => $this->config, - 'timeout' => $this->timeout, - 'retry_attempts' => $this->retryAttempts, - 'compensation' => $this->compensationAction, - 'conditions' => $this->conditions, - 'prerequisites' => $this->prerequisites, - ]; - } -} diff --git a/packages/workflow-engine-core/src/Core/WorkflowBuilder.php b/packages/workflow-engine-core/src/Core/WorkflowBuilder.php deleted file mode 100644 index 5eea252..0000000 --- a/packages/workflow-engine-core/src/Core/WorkflowBuilder.php +++ /dev/null @@ -1,604 +0,0 @@ -description('Complete user onboarding process') - * ->addStep('send_welcome', SendWelcomeEmailAction::class) - * ->addStep('create_profile', CreateUserProfileAction::class, ['template' => 'basic']) - * ->build(); - * ``` - * - * ## Conditional Steps - * - * ```php - * $workflow = WorkflowBuilder::create('order-processing') - * ->addStep('validate_order', ValidateOrderAction::class) - * ->when('order.type === "premium"', function($builder) { - * $builder->addStep('premium_processing', PremiumProcessingAction::class); - * }) - * ->addStep('finalize_order', FinalizeOrderAction::class) - * ->build(); - * ``` - * - * ## Common Patterns - * - * ```php - * $workflow = WorkflowBuilder::create('newsletter') - * ->email('newsletter-template', '{{ user.email }}', 'Weekly Newsletter') - * ->delay(hours: 1) - * ->http('https://api.example.com/track', 'POST', ['user_id' => '{{ user.id }}']) - * ->build(); - * ``` - * - * @see WorkflowDefinition For the resulting workflow definition structure - * @see QuickWorkflowBuilder For pre-built common workflow patterns - */ -final class WorkflowBuilder -{ - /** @var string The unique workflow name/identifier */ - private string $name; - - /** @var string The workflow version for change tracking */ - private string $version = '1.0'; - - /** @var string Human-readable workflow description */ - private string $description = ''; - - /** @var array> Array of step configurations */ - private array $steps = []; - - /** @var array> Array of step transitions */ - private array $transitions = []; - - /** @var array Additional workflow metadata */ - private array $metadata = []; - - /** - * Private constructor to enforce factory pattern usage. - * - * @param string $name The workflow name/identifier - * - * @throws InvalidWorkflowDefinitionException If name is empty or invalid - */ - private function __construct(string $name) - { - if (empty(trim($name))) { - throw InvalidWorkflowDefinitionException::invalidName($name); - } - - if (! preg_match('/^[a-zA-Z][a-zA-Z0-9_-]*$/', $name)) { - throw InvalidWorkflowDefinitionException::invalidName($name, 'Name must start with a letter and contain only letters, numbers, hyphens, and underscores'); - } - - $this->name = $name; - } - - /** - * Create a new workflow builder instance. - * - * @param string $name The unique workflow name/identifier - * @return static New builder instance for method chaining - * - * @throws InvalidWorkflowDefinitionException If name is invalid - * - * @example - * ```php - * $builder = WorkflowBuilder::create('user-registration'); - * ``` - */ - public static function create(string $name): static - { - return new self($name); - } - - /** - * Set the workflow description for documentation and debugging. - * - * @param string $description Human-readable workflow description - * @return $this For method chaining - * - * @example - * ```php - * $builder->description('Handles complete user onboarding process'); - * ``` - */ - public function description(string $description): self - { - $this->description = $description; - - return $this; - } - - /** - * Set the workflow version for change tracking and compatibility. - * - * @param string $version Semantic version string (e.g., "1.0.0", "2.1") - * @return $this For method chaining - * - * @example - * ```php - * $builder->version('2.1.0'); - * ``` - */ - public function version(string $version): self - { - $this->version = $version; - - return $this; - } - - /** - * Add a workflow step with comprehensive configuration options. - * - * @param string $id Unique step identifier within the workflow - * @param string|WorkflowAction $action Action class name or instance - * @param array $config Step-specific configuration parameters - * @param int|null $timeout Maximum execution time in seconds - * @param int $retryAttempts Number of retry attempts on failure (0-10) - * @return $this For method chaining - * - * @throws InvalidWorkflowDefinitionException If step configuration is invalid - * - * @example - * ```php - * $builder->addStep( - * 'send_email', - * SendEmailAction::class, - * ['template' => 'welcome', 'to' => '{{ user.email }}'], - * timeout: 30, - * retryAttempts: 3 - * ); - * ``` - */ - public function addStep( - string $id, - string|WorkflowAction $action, - array $config = [], - ?int $timeout = null, - int $retryAttempts = 0 - ): self { - if (empty(trim($id))) { - throw InvalidWorkflowDefinitionException::invalidStepId($id); - } - - if ($retryAttempts < 0 || $retryAttempts > 10) { - throw InvalidWorkflowDefinitionException::invalidRetryAttempts($retryAttempts); - } - - if ($timeout !== null && $timeout <= 0) { - throw InvalidWorkflowDefinitionException::invalidTimeout($timeout); - } - - // Check for duplicate step IDs - foreach ($this->steps as $existingStep) { - if ($existingStep['id'] === $id) { - throw InvalidWorkflowDefinitionException::duplicateStepId($id); - } - } - - $this->steps[] = [ - 'id' => $id, - 'action' => is_string($action) ? $action : $action::class, - 'config' => $config, - 'timeout' => $timeout, - 'retry_attempts' => $retryAttempts, - ]; - - return $this; - } - - /** - * Add the first step in a workflow (syntactic sugar for better readability). - * - * @param string|WorkflowAction $action Action class name or instance - * @param array $config Step-specific configuration parameters - * @param int|null $timeout Maximum execution time in seconds - * @param int $retryAttempts Number of retry attempts on failure - * @return $this For method chaining - * - * @example - * ```php - * $builder->startWith(ValidateInputAction::class, ['strict' => true]); - * ``` - */ - public function startWith( - string|WorkflowAction $action, - array $config = [], - ?int $timeout = null, - int $retryAttempts = 0 - ): self { - $stepId = 'step_'.(count($this->steps) + 1); - - return $this->addStep($stepId, $action, $config, $timeout, $retryAttempts); - } - - /** - * Add a sequential step (syntactic sugar for better readability). - * - * @param string|WorkflowAction $action Action class name or instance - * @param array $config Step-specific configuration parameters - * @param int|null $timeout Maximum execution time in seconds - * @param int $retryAttempts Number of retry attempts on failure - * @return $this For method chaining - * - * @example - * ```php - * $builder->then(ProcessDataAction::class)->then(SaveResultAction::class); - * ``` - */ - public function then( - string|WorkflowAction $action, - array $config = [], - ?int $timeout = null, - int $retryAttempts = 0 - ): self { - $stepId = 'step_'.(count($this->steps) + 1); - - return $this->addStep($stepId, $action, $config, $timeout, $retryAttempts); - } - - /** - * Add conditional steps that are only executed when a condition is met. - * - * @param string $condition Condition expression to evaluate (e.g., "user.premium === true") - * @param callable(static): void $callback Callback that receives the builder to add conditional steps - * @return $this For method chaining - * - * @throws InvalidWorkflowDefinitionException If condition is invalid - * - * @example - * ```php - * $builder->when('order.amount > 1000', function($builder) { - * $builder->addStep('fraud_check', FraudCheckAction::class); - * $builder->addStep('manager_approval', ManagerApprovalAction::class); - * }); - * ``` - */ - public function when(string $condition, callable $callback): self - { - if (empty(trim($condition))) { - throw InvalidWorkflowDefinitionException::invalidCondition($condition); - } - - $originalStepsCount = count($this->steps); - $callback($this); - $newStepsCount = count($this->steps); - - // Mark new steps as conditional - for ($i = $originalStepsCount; $i < $newStepsCount; $i++) { - $this->steps[$i]['condition'] = $condition; - } - - return $this; - } - - /** - * Add an email step using pre-configured email action (common pattern). - * - * @param string $template Email template identifier - * @param string $to Recipient email address (supports placeholders like "{{ user.email }}") - * @param string $subject Email subject line (supports placeholders) - * @param array $data Additional template data - * @return $this For method chaining - * - * @example - * ```php - * $builder->email( - * 'welcome-email', - * '{{ user.email }}', - * 'Welcome to {{ app.name }}!', - * ['user_name' => '{{ user.name }}'] - * ); - * ``` - */ - public function email( - string $template, - string $to, - string $subject, - array $data = [] - ): self { - return $this->addStep( - 'email_'.count($this->steps), - 'SolutionForest\\WorkflowMastery\\Actions\\EmailAction', - [ - 'template' => $template, - 'to' => $to, - 'subject' => $subject, - 'data' => $data, - ] - ); - } - - /** - * Add a delay step to pause workflow execution (common pattern). - * - * @param int|null $seconds Delay in seconds - * @param int|null $minutes Delay in minutes (converted to seconds) - * @param int|null $hours Delay in hours (converted to seconds) - * @return $this For method chaining - * - * @throws InvalidWorkflowDefinitionException If no delay value is provided - * - * @example - * ```php - * $builder->delay(seconds: 30); // 30 second delay - * $builder->delay(minutes: 5); // 5 minute delay - * $builder->delay(hours: 1, minutes: 30); // 1.5 hour delay - * ``` - */ - public function delay(?int $seconds = null, ?int $minutes = null, ?int $hours = null): self - { - $totalSeconds = $seconds ?? 0; - $totalSeconds += ($minutes ?? 0) * 60; - $totalSeconds += ($hours ?? 0) * 3600; - - if ($totalSeconds <= 0) { - throw InvalidWorkflowDefinitionException::invalidDelay($seconds, $minutes, $hours); - } - - return $this->addStep( - 'delay_'.count($this->steps), - 'SolutionForest\\WorkflowMastery\\Actions\\DelayAction', - ['seconds' => $totalSeconds] - ); - } - - /** - * Add an HTTP request step for external API calls (common pattern). - * - * @param string $url Target URL for the HTTP request - * @param string $method HTTP method (GET, POST, PUT, DELETE, etc.) - * @param array $data Request payload data - * @param array $headers Additional HTTP headers - * @return $this For method chaining - * - * @example - * ```php - * $builder->http( - * 'https://api.example.com/users', - * 'POST', - * ['name' => '{{ user.name }}', 'email' => '{{ user.email }}'], - * ['Authorization' => 'Bearer {{ api.token }}'] - * ); - * ``` - */ - public function http( - string $url, - string $method = 'GET', - array $data = [], - array $headers = [] - ): self { - return $this->addStep( - 'http_'.count($this->steps), - 'SolutionForest\\WorkflowMastery\\Actions\\HttpAction', - [ - 'url' => $url, - 'method' => $method, - 'data' => $data, - 'headers' => $headers, - ] - ); - } - - /** - * Add a condition check step for workflow branching (common pattern). - * - * @param string $condition Condition expression to evaluate - * @return $this For method chaining - * - * @example - * ```php - * $builder->condition('user.verified === true'); - * ``` - */ - public function condition(string $condition): self - { - return $this->addStep( - 'condition_'.count($this->steps), - 'SolutionForest\\WorkflowMastery\\Actions\\ConditionAction', - ['condition' => $condition] - ); - } - - /** - * Add custom metadata to the workflow definition. - * - * @param array $metadata Additional metadata to merge - * @return $this For method chaining - * - * @example - * ```php - * $builder->withMetadata([ - * 'author' => 'John Doe', - * 'department' => 'Engineering', - * 'priority' => 'high' - * ]); - * ``` - */ - public function withMetadata(array $metadata): self - { - $this->metadata = array_merge($this->metadata, $metadata); - - return $this; - } - - /** - * Build the final workflow definition from the configured steps and settings. - * - * @return WorkflowDefinition The complete workflow definition ready for execution - * - * @throws InvalidWorkflowDefinitionException If the workflow configuration is invalid - * - * @example - * ```php - * $workflow = WorkflowBuilder::create('user-registration') - * ->addStep('validate', ValidateUserAction::class) - * ->addStep('save', SaveUserAction::class) - * ->build(); - * ``` - */ - public function build(): WorkflowDefinition - { - if (empty($this->steps)) { - throw InvalidWorkflowDefinitionException::emptyWorkflow($this->name); - } - - // Convert builder format to Step objects - $steps = []; - foreach ($this->steps as $stepData) { - $steps[] = new Step( - id: $stepData['id'], - actionClass: $stepData['action'], - config: $stepData['config'], - timeout: $stepData['timeout'] ? (string) $stepData['timeout'] : null, - retryAttempts: $stepData['retry_attempts'], - conditions: isset($stepData['condition']) ? [$stepData['condition']] : [] - ); - } - - // Add description to metadata - if ($this->description) { - $this->metadata['description'] = $this->description; - } - - return new WorkflowDefinition( - name: $this->name, - version: $this->version, - steps: $steps, - transitions: $this->transitions, - metadata: $this->metadata - ); - } - - /** - * Get access to quick workflow builder for common workflow patterns. - * - * @return QuickWorkflowBuilder Instance for creating pre-configured workflows - * - * @example - * ```php - * $workflow = WorkflowBuilder::quick()->userOnboarding('new-user-flow'); - * $workflow = WorkflowBuilder::quick()->orderProcessing(); - * ``` - */ - public static function quick(): QuickWorkflowBuilder - { - return new QuickWorkflowBuilder; - } -} - -/** - * Pre-built workflow patterns for common business scenarios. - * - * This class provides ready-to-use workflow templates that can be customized - * and extended for typical business processes. - * - * @see WorkflowBuilder For custom workflow creation - */ -class QuickWorkflowBuilder -{ - /** - * Create a user onboarding workflow with standard steps. - * - * @param string $name Workflow name (defaults to 'user-onboarding') - * @return WorkflowBuilder Configured builder ready for customization - * - * @example - * ```php - * $workflow = WorkflowBuilder::quick() - * ->userOnboarding('premium-user-onboarding') - * ->then(SetupPremiumFeaturesAction::class) - * ->build(); - * ``` - */ - public function userOnboarding(string $name = 'user-onboarding'): WorkflowBuilder - { - return WorkflowBuilder::create($name) - ->description('Standard user onboarding process') - ->email( - template: 'welcome', - to: '{{ user.email }}', - subject: 'Welcome to {{ app.name }}!' - ) - ->delay(minutes: 5) - ->addStep('create_profile', 'App\\Actions\\CreateUserProfileAction') - ->addStep('assign_role', 'App\\Actions\\AssignDefaultRoleAction'); - } - - /** - * Create an order processing workflow for e-commerce scenarios. - * - * @param string $name Workflow name (defaults to 'order-processing') - * @return WorkflowBuilder Configured builder ready for customization - * - * @example - * ```php - * $workflow = WorkflowBuilder::quick() - * ->orderProcessing('premium-order-flow') - * ->when('order.priority === "high"', function($builder) { - * $builder->addStep('priority_handling', PriorityHandlingAction::class); - * }) - * ->build(); - * ``` - */ - public function orderProcessing(string $name = 'order-processing'): WorkflowBuilder - { - return WorkflowBuilder::create($name) - ->description('E-commerce order processing workflow') - ->addStep('validate_order', 'App\\Actions\\ValidateOrderAction') - ->addStep('charge_payment', 'App\\Actions\\ChargePaymentAction') - ->addStep('update_inventory', 'App\\Actions\\UpdateInventoryAction') - ->email( - template: 'order-confirmation', - to: '{{ order.customer.email }}', - subject: 'Order Confirmation #{{ order.id }}' - ); - } - - /** - * Create a document approval workflow for content management. - * - * @param string $name Workflow name (defaults to 'document-approval') - * @return WorkflowBuilder Configured builder ready for customization - * - * @example - * ```php - * $workflow = WorkflowBuilder::quick() - * ->documentApproval('legal-document-review') - * ->addStep('legal_review', LegalReviewAction::class) - * ->build(); - * ``` - */ - public function documentApproval(string $name = 'document-approval'): WorkflowBuilder - { - return WorkflowBuilder::create($name) - ->description('Document approval process') - ->addStep('submit_document', 'App\\Actions\\SubmitDocumentAction') - ->addStep('assign_reviewer', 'App\\Actions\\AssignReviewerAction') - ->email( - template: 'review-request', - to: '{{ reviewer.email }}', - subject: 'Document Review Request' - ) - ->addStep('review_document', 'App\\Actions\\ReviewDocumentAction') - ->when('review.approved', function ($builder) { - $builder->addStep('approve_document', 'App\\Actions\\ApproveDocumentAction'); - }) - ->when('review.rejected', function ($builder) { - $builder->addStep('reject_document', 'App\\Actions\\RejectDocumentAction'); - }); - } -} diff --git a/packages/workflow-engine-core/src/Core/WorkflowContext.php b/packages/workflow-engine-core/src/Core/WorkflowContext.php deleted file mode 100644 index 72424d5..0000000 --- a/packages/workflow-engine-core/src/Core/WorkflowContext.php +++ /dev/null @@ -1,252 +0,0 @@ - ['email' => 'user@example.com', 'name' => 'John']] - * ); - * - * $userEmail = data_get($context->data, 'user.email'); // 'user@example.com' - * $hasUserData = $context->hasData('user.name'); // true - * ``` - * - * ### Immutable Updates - * ```php - * $newContext = $context->with('user.verified', true); - * $mergedContext = $context->withData(['order' => ['id' => 123]]); - * ``` - * - * ### Configuration Access - * ```php - * $timeout = $context->getConfig('timeout', 30); - * $allConfig = $context->getConfig(); - * ``` - * - * @see WorkflowInstance For workflow execution state management - * @see WorkflowEngine For workflow execution coordination - */ -final readonly class WorkflowContext -{ - /** - * Create a new immutable workflow context. - * - * @param string $workflowId Unique identifier for the workflow definition - * @param string $stepId Current step identifier within the workflow - * @param array $data Workflow execution data (JSON-serializable) - * @param array $config Step-specific configuration parameters - * @param WorkflowInstance|null $instance Associated workflow instance (if available) - * @param DateTime $executedAt Timestamp when this context was created - */ - public function __construct( - public string $workflowId, - public string $stepId, - public array $data = [], - public array $config = [], - public ?WorkflowInstance $instance = null, - public DateTime $executedAt = new DateTime - ) {} - - /** - * Get the workflow identifier. - * - * @return string The unique workflow definition identifier - */ - public function getWorkflowId(): string - { - return $this->workflowId; - } - - /** - * Get the current step identifier. - * - * @return string The step identifier within the workflow - */ - public function getStepId(): string - { - return $this->stepId; - } - - /** - * Get all workflow execution data. - * - * @return array Complete data array - */ - public function getData(): array - { - return $this->data; - } - - /** - * Get all workflow execution data (alias for compatibility). - * - * @return array Complete data array - * - * @deprecated Use getData() instead - */ - public function getAllData(): array - { - return $this->data; - } - - /** - * Create a new context with additional data merged in (immutable operation). - * - * The new data is merged with existing data using array_merge, so new keys - * are added and existing keys are overwritten. - * - * @param array $newData Data to merge with existing data - * @return static New context instance with merged data - * - * @example - * ```php - * $newContext = $context->withData([ - * 'order' => ['id' => 123, 'total' => 99.99], - * 'payment' => ['method' => 'credit_card'] - * ]); - * ``` - */ - public function withData(array $newData): static - { - return new self( - workflowId: $this->workflowId, - stepId: $this->stepId, - data: array_merge($this->data, $newData), - config: $this->config, - instance: $this->instance, - executedAt: $this->executedAt - ); - } - - /** - * Create a new context with a single data value set (immutable operation). - * - * Uses Laravel's data_set helper for setting nested values using dot notation. - * - * @param string $key Data key (supports dot notation for nested access) - * @param mixed $value Value to set - * @return static New context instance with updated data - * - * @example - * ```php - * $newContext = $context->with('user.email', 'newemail@example.com'); - * $nestedContext = $context->with('order.items.0.quantity', 2); - * ``` - */ - public function with(string $key, mixed $value): static - { - $newData = $this->data; - data_set($newData, $key, $value); - - return new self( - workflowId: $this->workflowId, - stepId: $this->stepId, - data: $newData, - config: $this->config, - instance: $this->instance, - executedAt: $this->executedAt - ); - } - - /** - * Check if a data key exists in the context. - * - * Uses Laravel's data_get helper for checking nested keys using dot notation. - * - * @param string $key Data key (supports dot notation for nested access) - * @return bool True if the key exists and has a non-null value - * - * @example - * ```php - * if ($context->hasData('user.email')) { - * // User email is available - * } - * - * if ($context->hasData('order.payment.status')) { - * // Payment status is set - * } - * ``` - */ - public function hasData(string $key): bool - { - return data_get($this->data, $key) !== null; - } - - /** - * Get configuration value(s) for the current step. - * - * @param string|null $key Configuration key (supports dot notation), or null for all config - * @param mixed $default Default value to return if key doesn't exist - * @return mixed Configuration value or default - * - * @example - * ```php - * $timeout = $context->getConfig('timeout', 30); - * $retries = $context->getConfig('retry.attempts', 3); - * $allConfig = $context->getConfig(); // Gets all configuration - * ``` - */ - public function getConfig(?string $key = null, mixed $default = null): mixed - { - if ($key === null) { - return $this->config; - } - - return data_get($this->config, $key, $default); - } - - /** - * Get current step ID (alias for compatibility). - * - * @return string The step identifier within the workflow - * - * @deprecated Use getStepId() instead - */ - public function getCurrentStepId(): string - { - return $this->stepId; - } - - /** - * Convert the context to an array representation for serialization. - * - * @return array Array representation suitable for JSON encoding - * - * @example - * ```php - * $contextArray = $context->toArray(); - * $json = json_encode($contextArray); - * ``` - */ - public function toArray(): array - { - return [ - 'workflow_id' => $this->workflowId, - 'step_id' => $this->stepId, - 'data' => $this->data, - 'config' => $this->config, - 'instance_id' => $this->instance?->getId(), - 'executed_at' => $this->executedAt->format('Y-m-d H:i:s'), - ]; - } -} diff --git a/packages/workflow-engine-core/src/Core/WorkflowDefinition.php b/packages/workflow-engine-core/src/Core/WorkflowDefinition.php deleted file mode 100644 index 963ea58..0000000 --- a/packages/workflow-engine-core/src/Core/WorkflowDefinition.php +++ /dev/null @@ -1,397 +0,0 @@ - 'step1', 'to' => 'step2'], - * ['from' => 'step2', 'to' => 'step3'] - * ] - * ); - * ``` - * - * ### Workflow Navigation - * ```php - * $firstStep = $definition->getFirstStep(); - * $nextSteps = $definition->getNextSteps('current_step', $workflowData); - * $isComplete = $definition->isLastStep('final_step'); - * ``` - * - * ### Step Access - * ```php - * $step = $definition->getStep('send_email'); - * $hasStep = $definition->hasStep('validate_input'); - * $allSteps = $definition->getSteps(); - * ``` - * - * @see Step For individual step configuration - * @see WorkflowBuilder For fluent workflow construction - * @see WorkflowEngine For workflow execution - */ -final class WorkflowDefinition -{ - /** @var array Indexed steps for fast lookup by ID */ - private readonly array $steps; - - /** - * Create a new workflow definition with validation. - * - * @param string $name Unique workflow name/identifier - * @param string $version Workflow version for change tracking - * @param array> $steps Array of Step objects or step configurations - * @param array> $transitions Array of step transitions - * @param array $metadata Additional workflow metadata - * - * @throws InvalidWorkflowDefinitionException If the definition is invalid - */ - public function __construct( - private readonly string $name, - private readonly string $version, - array $steps = [], - private readonly array $transitions = [], - private readonly array $metadata = [] - ) { - $this->steps = $this->processSteps($steps); - } - - /** - * Get the workflow name. - * - * @return string Unique workflow identifier - */ - public function getName(): string - { - return $this->name; - } - - /** - * Get the workflow version. - * - * @return string Version string for change tracking - */ - public function getVersion(): string - { - return $this->version; - } - - /** - * Get all workflow steps indexed by their IDs. - * - * @return array Steps indexed by step ID for fast lookup - */ - public function getSteps(): array - { - return $this->steps; - } - - /** - * Get a specific step by its ID. - * - * @param string $id Step identifier - * @return Step|null The step instance, or null if not found - * - * @example - * ```php - * $emailStep = $definition->getStep('send_welcome_email'); - * if ($emailStep) { - * $config = $emailStep->getConfig(); - * } - * ``` - */ - public function getStep(string $id): ?Step - { - return $this->steps[$id] ?? null; - } - - /** - * Get all defined step transitions. - * - * @return array> Array of transition configurations - */ - public function getTransitions(): array - { - return $this->transitions; - } - - /** - * Get workflow metadata. - * - * @return array Additional workflow configuration and documentation - */ - public function getMetadata(): array - { - return $this->metadata; - } - - /** - * Find the first step in the workflow execution sequence. - * - * Attempts to find a step with no incoming transitions. If none exists, - * returns the first step in the steps array. - * - * @return Step|null The first step to execute, or null if no steps exist - * - * @example - * ```php - * $firstStep = $definition->getFirstStep(); - * if ($firstStep) { - * $engine->executeStep($firstStep, $context); - * } - * ``` - */ - public function getFirstStep(): ?Step - { - // Find step with no incoming transitions - $stepsWithIncoming = []; - foreach ($this->transitions as $transition) { - $stepsWithIncoming[] = $transition['to']; - } - - foreach ($this->steps as $step) { - if (! in_array($step->getId(), $stepsWithIncoming)) { - return $step; - } - } - - // If no step found without incoming transitions, return first step - $stepsArray = $this->steps; - - return reset($stepsArray) ?: null; - } - - /** - * Get the next steps to execute after the current step. - * - * Considers transitions and conditional logic to determine which steps - * should be executed next based on the current workflow state. - * - * @param string|null $currentStepId Current step ID, or null to get first steps - * @param array $data Workflow data for condition evaluation - * @return array Array of next steps to execute - * - * @example - * ```php - * $nextSteps = $definition->getNextSteps('validate_order', $workflowData); - * foreach ($nextSteps as $step) { - * if ($step->canExecute($workflowData)) { - * $engine->executeStep($step, $context); - * } - * } - * ``` - */ - public function getNextSteps(?string $currentStepId, array $data = []): array - { - if ($currentStepId === null) { - $firstStep = $this->getFirstStep(); - - return $firstStep ? [$firstStep] : []; - } - - $nextSteps = []; - foreach ($this->transitions as $transition) { - if ($transition['from'] === $currentStepId) { - // Check condition if present - if (isset($transition['condition']) && ! $this->evaluateCondition($transition['condition'], $data)) { - continue; - } - - $nextStep = $this->getStep($transition['to']); - if ($nextStep) { - $nextSteps[] = $nextStep; - } - } - } - - return $nextSteps; - } - - /** - * Check if a step exists in the workflow. - * - * @param string $stepId Step identifier to check - * @return bool True if the step exists - */ - public function hasStep(string $stepId): bool - { - return isset($this->steps[$stepId]); - } - - /** - * Check if a step is the last step in the workflow. - * - * A step is considered the last step if it has no outgoing transitions. - * - * @param string $stepId Step identifier to check - * @return bool True if this is a terminal step - * - * @example - * ```php - * if ($definition->isLastStep($currentStepId)) { - * // Workflow is complete - * $this->markWorkflowComplete($workflowInstance); - * } - * ``` - */ - public function isLastStep(string $stepId): bool - { - // Check if this step has any outgoing transitions - foreach ($this->transitions as $transition) { - if ($transition['from'] === $stepId) { - return false; - } - } - - return true; - } - - /** - * Process and validate step configurations into Step objects. - * - * @param array> $stepsData Array of Step objects or configurations - * @return array Processed steps indexed by ID - * - * @throws InvalidWorkflowDefinitionException If step configuration is invalid - * - * @internal Used during workflow definition construction - */ - private function processSteps(array $stepsData): array - { - $steps = []; - foreach ($stepsData as $index => $stepData) { - // Handle both Step objects and array data - if ($stepData instanceof Step) { - $steps[$stepData->getId()] = $stepData; - - continue; - } - - // Use the 'id' field from step data, or fall back to array index - $stepId = $stepData['id'] ?? $index; - - $actionClass = null; - if (isset($stepData['action'])) { - $actionClass = ActionResolver::resolve($stepData['action']); - } - - $steps[$stepId] = new Step( - id: $stepId, - actionClass: $actionClass, - config: $stepData['parameters'] ?? $stepData['config'] ?? [], - timeout: $stepData['timeout'] ?? null, - retryAttempts: $stepData['retry_attempts'] ?? 0, - compensationAction: $stepData['compensation'] ?? null, - conditions: $stepData['conditions'] ?? [], - prerequisites: $stepData['prerequisites'] ?? [] - ); - } - - return $steps; - } - - /** - * Evaluate a condition expression against workflow data. - * - * @param string $condition Condition expression to evaluate - * @param array $data Workflow data for evaluation - * @return bool True if condition evaluates to true - * - * @internal Used for transition condition evaluation - */ - private function evaluateCondition(string $condition, array $data): bool - { - // Enhanced condition evaluation with comprehensive operator support - if (preg_match('/(\w+(?:\.\w+)*)\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)/', $condition, $matches)) { - $key = $matches[1]; - $operator = $matches[2]; - $value = trim($matches[3], '"\''); - - $dataValue = data_get($data, $key); - - return match ($operator) { - '===' => $dataValue === $value, - '!==' => $dataValue !== $value, - '==' => $dataValue == $value, - '!=' => $dataValue != $value, - '>' => $dataValue > $value, - '<' => $dataValue < $value, - '>=' => $dataValue >= $value, - '<=' => $dataValue <= $value, - default => false, - }; - } - - return false; // Default to false if condition cannot be parsed - } - - /** - * Convert the workflow definition to an array representation. - * - * @return array Array representation suitable for serialization - * - * @example - * ```php - * $definitionArray = $definition->toArray(); - * $json = json_encode($definitionArray); - * file_put_contents('workflow.json', $json); - * ``` - */ - public function toArray(): array - { - return [ - 'name' => $this->name, - 'version' => $this->version, - 'steps' => array_map(fn ($step) => $step->toArray(), $this->steps), - 'transitions' => $this->transitions, - 'metadata' => $this->metadata, - ]; - } - - /** - * Create a workflow definition from an array representation. - * - * @param array $data Array representation of workflow definition - * @return static New workflow definition instance - * - * @throws InvalidWorkflowDefinitionException If data is invalid - * - * @example - * ```php - * $json = file_get_contents('workflow.json'); - * $data = json_decode($json, true); - * $definition = WorkflowDefinition::fromArray($data); - * ``` - */ - public static function fromArray(array $data): static - { - return new self( - name: $data['name'], - version: $data['version'] ?? '1.0', - steps: $data['steps'] ?? [], - transitions: $data['transitions'] ?? [], - metadata: $data['metadata'] ?? [] - ); - } -} diff --git a/packages/workflow-engine-core/src/Core/WorkflowEngine.php b/packages/workflow-engine-core/src/Core/WorkflowEngine.php deleted file mode 100644 index 2aaa0c3..0000000 --- a/packages/workflow-engine-core/src/Core/WorkflowEngine.php +++ /dev/null @@ -1,322 +0,0 @@ -start('user-onboarding', [ - * 'name' => 'User Onboarding', - * 'steps' => [ - * ['id' => 'welcome', 'action' => SendWelcomeEmailAction::class], - * ['id' => 'profile', 'action' => CreateProfileAction::class], - * ] - * ], ['user_id' => 123]); - * - * // Resume execution later - * $instance = $engine->resume($instanceId); - * ``` - * @example With dependency injection - * ```php - * // In a Laravel service provider - * $this->app->singleton(WorkflowEngine::class, function ($app) { - * return new WorkflowEngine( - * $app->make(StorageAdapter::class), - * $app->make(EventDispatcher::class) - * ); - * }); - * ``` - */ -class WorkflowEngine -{ - /** - * The definition parser for processing workflow definitions. - */ - private readonly DefinitionParser $parser; - - /** - * The state manager for persisting workflow state. - */ - private readonly StateManager $stateManager; - - /** - * The executor for running workflow steps. - */ - private readonly Executor $executor; - - /** - * Create a new workflow engine instance. - * - * @param StorageAdapter $storage The storage adapter for persisting workflow data - * @param EventDispatcher|null $eventDispatcher Optional event dispatcher for workflow events - * - * @throws \InvalidArgumentException If the storage adapter is not properly configured - */ - public function __construct( - private readonly StorageAdapter $storage, - private readonly ?EventDispatcher $eventDispatcher = null - ) { - $this->parser = new DefinitionParser; - $this->stateManager = new StateManager($storage); - $this->executor = new Executor($this->stateManager, $eventDispatcher); - - // If no event dispatcher is provided, we'll use a fallback approach - if ($this->eventDispatcher === null) { - // We'll handle this case in the methods that use the event dispatcher - } - } - - /** - * Start a new workflow instance with the given definition and context. - * - * Creates a new workflow instance, saves it to storage, dispatches a start event, - * and begins execution of the first step. - * - * @param string $workflowId Unique identifier for this workflow instance - * @param array $definition The workflow definition containing steps and configuration - * @param array $context Initial context data for the workflow - * @return string The workflow instance ID - * - * @throws InvalidWorkflowDefinitionException If the workflow definition is invalid - * @throws \RuntimeException If the workflow cannot be started due to system issues - * - * @example Starting a simple workflow - * ```php - * $instanceId = $engine->start('order-processing', [ - * 'name' => 'Order Processing', - * 'steps' => [ - * ['id' => 'validate', 'action' => ValidateOrderAction::class], - * ['id' => 'payment', 'action' => ProcessPaymentAction::class], - * ['id' => 'fulfill', 'action' => FulfillOrderAction::class], - * ] - * ], [ - * 'order_id' => 12345, - * 'customer_id' => 67890 - * ]); - * ``` - */ - public function start(string $workflowId, array $definition, array $context = []): string - { - // Parse definition - $workflowDef = $this->parser->parse($definition); - - // Create instance - $instance = new WorkflowInstance( - id: $workflowId, - definition: $workflowDef, - state: WorkflowState::PENDING, - data: $context, - createdAt: now(), - updatedAt: now() - ); - - // Save initial state - $this->stateManager->save($instance); - - // Dispatch start event - $this->dispatchEvent(new WorkflowStarted( - $instance->getId(), - $instance->getDefinition()->getName(), - $context - )); - - // Execute first step - $this->executor->execute($instance); - - return $instance->getId(); - } - - /** - * Resume execution of an existing workflow instance. - * - * Loads the workflow instance from storage and continues execution - * from where it left off. Only works for workflows in PENDING or FAILED state. - * - * @param string $instanceId The workflow instance ID to resume - * @return WorkflowInstance The resumed workflow instance - * - * @throws WorkflowInstanceNotFoundException If the workflow instance doesn't exist - * @throws InvalidWorkflowStateException If the workflow cannot be resumed (e.g., already completed) - * @throws \RuntimeException If the workflow cannot be resumed due to system issues - * - * @example Resuming a workflow - * ```php - * try { - * $instance = $engine->resume('workflow-123'); - * echo "Workflow resumed, current state: " . $instance->getState()->value; - * } catch (InvalidWorkflowStateException $e) { - * echo "Cannot resume: " . $e->getUserMessage(); - * } - * ``` - */ - public function resume(string $instanceId): WorkflowInstance - { - $instance = $this->stateManager->load($instanceId); - - if ($instance->getState() === WorkflowState::COMPLETED) { - throw InvalidWorkflowStateException::cannotResumeCompleted($instanceId); - } - - $this->executor->execute($instance); - - return $instance; - } - - /** - * Get a workflow instance by its ID. - * - * Retrieves the complete workflow instance including its current state, - * execution history, and context data. - * - * @param string $instanceId The workflow instance ID - * @return WorkflowInstance The workflow instance - * - * @throws WorkflowInstanceNotFoundException If the workflow instance doesn't exist - * - * @example Getting workflow instance details - * ```php - * $instance = $engine->getInstance('workflow-123'); - * - * echo "Workflow: " . $instance->getDefinition()->getName(); - * echo "State: " . $instance->getState()->label(); - * echo "Progress: " . $instance->getProgress() . "%"; - * ``` - */ - public function getInstance(string $instanceId): WorkflowInstance - { - return $this->stateManager->load($instanceId); - } - - /** - * Get all workflow instances with optional filtering. - * - * Retrieves workflow instances based on the provided filters. - * Useful for building dashboards, monitoring, and reporting. - * - * @param array $filters Optional filters to apply - * - 'state': Filter by workflow state (e.g., 'running', 'completed') - * - 'definition_name': Filter by workflow definition name - * - 'created_after': Filter by creation date (DateTime or string) - * - 'created_before': Filter by creation date (DateTime or string) - * - 'limit': Maximum number of results to return - * - 'offset': Number of results to skip (for pagination) - * @return WorkflowInstance[] Array of workflow instances matching the filters - * - * @throws \InvalidArgumentException If invalid filters are provided - * - * @example Getting recent failed workflows - * ```php - * $failedWorkflows = $engine->getInstances([ - * 'state' => 'failed', - * 'created_after' => now()->subDays(7), - * 'limit' => 50 - * ]); - * - * foreach ($failedWorkflows as $workflow) { - * echo "Failed: " . $workflow->getId() . "\n"; - * } - * ``` - */ - public function getInstances(array $filters = []): array - { - return $this->storage->findInstances($filters); - } - - /** - * Cancel a workflow instance - */ - public function cancel(string $instanceId, string $reason = ''): WorkflowInstance - { - $instance = $this->stateManager->load($instanceId); - $instance->setState(WorkflowState::CANCELLED); - $this->stateManager->save($instance); - - // Dispatch cancel event - $this->dispatchEvent(new WorkflowCancelled( - $instance->getId(), - $instance->getDefinition()->getName(), - $reason - )); - - return $instance; - } - - /** - * Get workflow instance by ID - */ - public function getWorkflow(string $workflowId): WorkflowInstance - { - return $this->stateManager->load($workflowId); - } - - /** - * Get workflow status - */ - public function getStatus(string $workflowId): array - { - $instance = $this->getWorkflow($workflowId); - - return [ - 'workflow_id' => $instance->getId(), - 'name' => $instance->getDefinition()->getName(), - 'state' => $instance->getState()->value, - 'current_step' => $instance->getCurrentStepId(), - 'progress' => $instance->getProgress(), - 'created_at' => $instance->getCreatedAt(), - 'updated_at' => $instance->getUpdatedAt(), - ]; - } - - /** - * List workflows with optional filters - */ - public function listWorkflows(array $filters = []): array - { - // Convert WorkflowState enum to string value for storage layer - if (isset($filters['state']) && $filters['state'] instanceof WorkflowState) { - $filters['state'] = $filters['state']->value; - } - - $instances = $this->storage->findInstances($filters); - - return array_map(function (WorkflowInstance $instance) { - return [ - 'workflow_id' => $instance->getId(), - 'name' => $instance->getDefinition()->getName(), - 'state' => $instance->getState()->value, - 'current_step' => $instance->getCurrentStepId(), - 'progress' => $instance->getProgress(), - 'created_at' => $instance->getCreatedAt(), - 'updated_at' => $instance->getUpdatedAt(), - ]; - }, $instances); - } - - /** - * Safely dispatch an event if event dispatcher is available - */ - private function dispatchEvent(object $event): void - { - if ($this->eventDispatcher !== null) { - $this->eventDispatcher->dispatch($event); - } - } -} diff --git a/packages/workflow-engine-core/src/Core/WorkflowInstance.php b/packages/workflow-engine-core/src/Core/WorkflowInstance.php deleted file mode 100644 index 1d55242..0000000 --- a/packages/workflow-engine-core/src/Core/WorkflowInstance.php +++ /dev/null @@ -1,527 +0,0 @@ -addStep('send-welcome', SendWelcomeEmailAction::class) - * ->addStep('create-profile', CreateProfileAction::class) - * ->build(); - * - * $instance = new WorkflowInstance( - * id: 'user-123-onboarding', - * definition: $definition, - * state: WorkflowState::Pending, - * data: ['user_id' => 123, 'email' => 'user@example.com'] - * ); - * ``` - * - * ### Tracking Progress - * ```php - * // Check current progress - * $progress = $instance->getProgress(); // 0.0 to 100.0 - * - * // Mark step as completed - * $instance->addCompletedStep('send-welcome'); - * - * // Check what steps can be executed next - * $nextSteps = $instance->getNextSteps(); - * ``` - * - * ### Data Management - * ```php - * // Update workflow data - * $instance->mergeData(['profile_created' => true, 'welcome_sent' => true]); - * - * // Get current data - * $data = $instance->getData(); - * $userId = data_get($data, 'user_id'); - * ``` - * - * ### Error Handling - * ```php - * // Record a failed step - * $instance->addFailedStep('send-welcome', 'SMTP server unavailable'); - * - * // Update workflow state - * $instance->setState(WorkflowState::Failed); - * $instance->setErrorMessage('Email delivery failed'); - * ``` - * - * ### Serialization - * ```php - * // Convert to array for storage - * $data = $instance->toArray(); - * Cache::put("workflow:{$instance->getId()}", $data); - * - * // Restore from array - * $restoredInstance = WorkflowInstance::fromArray($data, $definition); - * ``` - * - * @see WorkflowDefinition For the workflow blueprint - * @see WorkflowState For execution state enumeration - * @see WorkflowContext For step execution context - */ -final class WorkflowInstance -{ - /** @var WorkflowState Current execution state of the workflow */ - private WorkflowState $state; - - /** @var array Workflow data that flows between steps */ - private array $data; - - /** @var string|null ID of the currently executing or next step */ - private ?string $currentStepId = null; - - /** @var array List of step IDs that have been completed */ - private array $completedSteps = []; - - /** @var array List of failed steps with error details */ - private array $failedSteps = []; - - /** @var string|null Overall workflow error message if execution failed */ - private ?string $errorMessage = null; - - /** @var Carbon When this workflow instance was created */ - private readonly Carbon $createdAt; - - /** @var Carbon When this workflow instance was last updated */ - private Carbon $updatedAt; - - /** - * Create a new workflow instance. - * - * @param string $id Unique identifier for this workflow instance - * @param WorkflowDefinition $definition The workflow definition blueprint - * @param WorkflowState $state Initial execution state - * @param array $data Initial workflow data - * @param Carbon|null $createdAt Creation timestamp (defaults to now) - * @param Carbon|null $updatedAt Last update timestamp (defaults to now) - */ - public function __construct( - private readonly string $id, - private readonly WorkflowDefinition $definition, - WorkflowState $state, - array $data = [], - ?Carbon $createdAt = null, - ?Carbon $updatedAt = null - ) { - $this->state = $state; - $this->data = $data; - $this->createdAt = $createdAt ?? now(); - $this->updatedAt = $updatedAt ?? now(); - } - - /** - * Get the unique identifier for this workflow instance. - * - * @return string The instance ID - */ - public function getId(): string - { - return $this->id; - } - - /** - * Get the workflow definition blueprint. - * - * @return WorkflowDefinition The workflow definition - */ - public function getDefinition(): WorkflowDefinition - { - return $this->definition; - } - - /** - * Get the current workflow execution state. - * - * @return WorkflowState The current state - */ - public function getState(): WorkflowState - { - return $this->state; - } - - /** - * Update the workflow execution state. - * - * @param WorkflowState $state The new execution state - */ - public function setState(WorkflowState $state): void - { - $this->state = $state; - $this->updatedAt = now(); - } - - /** - * Get the current workflow data. - * - * @return array The workflow data - */ - public function getData(): array - { - return $this->data; - } - - /** - * Replace the entire workflow data. - * - * @param array $data The new workflow data - */ - public function setData(array $data): void - { - $this->data = $data; - $this->updatedAt = now(); - } - - /** - * Merge new data with existing workflow data. - * - * @param array $data Data to merge - */ - public function mergeData(array $data): void - { - $this->data = array_merge($this->data, $data); - $this->updatedAt = now(); - } - - /** - * Get the ID of the current step. - * - * @return string|null The current step ID, or null if no step is active - */ - public function getCurrentStepId(): ?string - { - return $this->currentStepId; - } - - /** - * Set the current step ID. - * - * @param string|null $stepId The step ID to set as current - */ - public function setCurrentStepId(?string $stepId): void - { - $this->currentStepId = $stepId; - $this->updatedAt = now(); - } - - /** - * Get the list of completed step IDs. - * - * @return array List of completed step IDs - */ - public function getCompletedSteps(): array - { - return $this->completedSteps; - } - - /** - * Mark a step as completed. - * - * @param string $stepId The step ID to mark as completed - */ - public function addCompletedStep(string $stepId): void - { - if (! in_array($stepId, $this->completedSteps)) { - $this->completedSteps[] = $stepId; - $this->updatedAt = now(); - } - } - - /** - * Get the list of failed steps with error details. - * - * @return array List of failed steps - */ - public function getFailedSteps(): array - { - return $this->failedSteps; - } - - /** - * Record a step failure with error details. - * - * @param string $stepId The step ID that failed - * @param string $error The error message or description - */ - public function addFailedStep(string $stepId, string $error): void - { - $this->failedSteps[] = [ - 'step_id' => $stepId, - 'error' => $error, - 'failed_at' => now()->toISOString(), - ]; - $this->updatedAt = now(); - } - - /** - * Get the overall workflow error message. - * - * @return string|null The error message, or null if no error - */ - public function getErrorMessage(): ?string - { - return $this->errorMessage; - } - - /** - * Set the overall workflow error message. - * - * @param string|null $errorMessage The error message - */ - public function setErrorMessage(?string $errorMessage): void - { - $this->errorMessage = $errorMessage; - $this->updatedAt = now(); - } - - /** - * Get the workflow instance creation timestamp. - * - * @return Carbon The creation timestamp - */ - public function getCreatedAt(): Carbon - { - return $this->createdAt; - } - - /** - * Get the workflow instance last update timestamp. - * - * @return Carbon The last update timestamp - */ - public function getUpdatedAt(): Carbon - { - return $this->updatedAt; - } - - /** - * Check if a specific step has been completed. - * - * @param string $stepId The step ID to check - * @return bool True if the step is completed, false otherwise - */ - public function isStepCompleted(string $stepId): bool - { - return in_array($stepId, $this->completedSteps); - } - - /** - * Get the next executable steps based on current state. - * - * @return array List of steps that can be executed next - */ - public function getNextSteps(): array - { - return $this->definition->getNextSteps($this->currentStepId, $this->data); - } - - /** - * Check if a specific step can be executed now. - * - * @param string $stepId The step ID to check - * @return bool True if the step can be executed, false otherwise - */ - public function canExecuteStep(string $stepId): bool - { - $step = $this->definition->getStep($stepId); - if (! $step) { - return false; - } - - // Check if all prerequisites are met - foreach ($step->getPrerequisites() as $prerequisite) { - if (! $this->isStepCompleted($prerequisite)) { - return false; - } - } - - return true; - } - - /** - * Convert the workflow instance to an array representation. - * - * @return array Array representation of the instance - */ - public function toArray(): array - { - return [ - 'id' => $this->id, - 'definition_name' => $this->definition->getName(), - 'definition_version' => $this->definition->getVersion(), - 'state' => $this->state->value, - 'data' => $this->data, - 'current_step_id' => $this->currentStepId, - 'completed_steps' => $this->completedSteps, - 'failed_steps' => $this->failedSteps, - 'error_message' => $this->errorMessage, - 'created_at' => $this->createdAt->toISOString(), - 'updated_at' => $this->updatedAt->toISOString(), - ]; - } - - /** - * Create a workflow instance from an array representation. - * - * @param array $data Array data to restore from - * @param WorkflowDefinition $definition The workflow definition - * @return static Restored workflow instance - */ - public static function fromArray(array $data, WorkflowDefinition $definition): static - { - $instance = new self( - id: $data['id'], - definition: $definition, - state: WorkflowState::from($data['state']), - data: $data['data'] ?? [], - createdAt: Carbon::parse($data['created_at']), - updatedAt: Carbon::parse($data['updated_at']) - ); - - $instance->currentStepId = $data['current_step_id'] ?? null; - $instance->completedSteps = $data['completed_steps'] ?? []; - $instance->failedSteps = $data['failed_steps'] ?? []; - $instance->errorMessage = $data['error_message'] ?? null; - - return $instance; - } - - /** - * Get workflow execution progress as a percentage. - * - * Calculates completion percentage based on the number of completed - * steps versus total steps in the workflow definition. - * - * @return float Progress percentage (0.0 to 100.0) - */ - public function getProgress(): float - { - $totalSteps = count($this->definition->getSteps()); - if ($totalSteps === 0) { - return 100.0; - } - - $completedSteps = count($this->completedSteps); - - return ($completedSteps / $totalSteps) * 100.0; - } - - /** - * Get the workflow execution context for the current step. - * - * Creates a WorkflowContext object that can be used for step execution, - * containing the instance ID, current step ID, and workflow data. - * - * @return WorkflowContext The execution context - */ - public function getContext(): WorkflowContext - { - return new WorkflowContext( - $this->id, - $this->currentStepId ?? '', - $this->data - ); - } - - /** - * Get the workflow name from the definition. - * - * @return string The workflow name - */ - public function getName(): string - { - return $this->definition->getName(); - } - - /** - * Check if the workflow has failed. - * - * @return bool True if the workflow is in failed state - */ - public function isFailed(): bool - { - return $this->state === WorkflowState::FAILED; - } - - /** - * Check if the workflow is completed. - * - * @return bool True if the workflow is in completed state - */ - public function isCompleted(): bool - { - return $this->state === WorkflowState::COMPLETED; - } - - /** - * Check if the workflow is currently running. - * - * @return bool True if the workflow is in running state - */ - public function isRunning(): bool - { - return $this->state === WorkflowState::RUNNING; - } - - /** - * Check if the workflow is pending execution. - * - * @return bool True if the workflow is in pending state - */ - public function isPending(): bool - { - return $this->state === WorkflowState::PENDING; - } - - /** - * Get a summary of the workflow execution status. - * - * @return array Status summary with key metrics - */ - public function getStatusSummary(): array - { - return [ - 'id' => $this->id, - 'name' => $this->getName(), - 'state' => $this->state->value, - 'progress' => $this->getProgress(), - 'current_step' => $this->currentStepId, - 'completed_steps_count' => count($this->completedSteps), - 'failed_steps_count' => count($this->failedSteps), - 'total_steps' => count($this->definition->getSteps()), - 'has_errors' => ! empty($this->failedSteps) || ! empty($this->errorMessage), - 'created_at' => $this->createdAt->toISOString(), - 'updated_at' => $this->updatedAt->toISOString(), - ]; - } -} diff --git a/packages/workflow-engine-core/src/Core/WorkflowState.php b/packages/workflow-engine-core/src/Core/WorkflowState.php deleted file mode 100644 index be86902..0000000 --- a/packages/workflow-engine-core/src/Core/WorkflowState.php +++ /dev/null @@ -1,374 +0,0 @@ -getState()->isActive()) { - * // Workflow can still execute - * $nextSteps = $instance->getNextSteps(); - * } - * - * if ($instance->getState()->isFinished()) { - * // Workflow execution completed - * $result = $instance->getData(); - * } - * ``` - * - * ### State Transitions - * ```php - * $currentState = WorkflowState::PENDING; - * - * if ($currentState->canTransitionTo(WorkflowState::RUNNING)) { - * $instance->setState(WorkflowState::RUNNING); - * } - * ``` - * - * ### UI Representation - * ```php - * $state = $instance->getState(); - * - * echo "Status: {$state->icon()} {$state->label()}"; - * echo "{$state->label()}"; - * ``` - * - * @see WorkflowInstance For workflow state management - * @see WorkflowEngine For state transitions during execution - */ -enum WorkflowState: string -{ - /** Workflow created but not yet started execution */ - case PENDING = 'pending'; - - /** Workflow is actively executing steps */ - case RUNNING = 'running'; - - /** Workflow is waiting for external input or conditions */ - case WAITING = 'waiting'; - - /** Workflow execution temporarily suspended by user */ - case PAUSED = 'paused'; - - /** Workflow finished successfully with all steps completed */ - case COMPLETED = 'completed'; - - /** Workflow terminated due to step failures or errors */ - case FAILED = 'failed'; - - /** Workflow terminated by user action before completion */ - case CANCELLED = 'cancelled'; - - /** - * Check if the workflow is in an active state. - * - * Active states indicate that the workflow can potentially continue - * execution and progress through its steps. This includes pending - * workflows that haven't started yet. - * - * @return bool True if the workflow can potentially execute or continue - * - * @example Active state checking - * ```php - * $state = WorkflowState::RUNNING; - * - * if ($state->isActive()) { - * // Can execute next steps - * $executor->continueExecution($instance); - * } - * ``` - */ - public function isActive(): bool - { - return in_array($this, [ - self::PENDING, - self::RUNNING, - self::WAITING, - self::PAUSED, - ]); - } - - /** - * Check if the workflow is in a finished state. - * - * Finished states indicate that the workflow execution has terminated - * and cannot continue. No further steps will be executed. - * - * @return bool True if the workflow execution has finished - * - * @example Finished state checking - * ```php - * $state = $instance->getState(); - * - * if ($state->isFinished()) { - * // Archive or cleanup workflow - * $archiver->archive($instance); - * } - * ``` - */ - public function isFinished(): bool - { - return in_array($this, [ - self::COMPLETED, - self::FAILED, - self::CANCELLED, - ]); - } - - /** - * Get color code for UI representation. - * - * Returns a semantic color name that can be used in web interfaces - * to visually represent the workflow state. - * - * @return string Color name (gray, blue, yellow, orange, green, red, purple) - * - * @example UI color usage - * ```php - * $state = $instance->getState(); - * $color = $state->color(); - * - * echo "{$state->label()}"; - * ``` - */ - public function color(): string - { - return match ($this) { - self::PENDING => 'gray', // Neutral, waiting to start - self::RUNNING => 'blue', // Active, in progress - self::WAITING => 'yellow', // Attention, waiting for input - self::PAUSED => 'orange', // Warning, temporarily stopped - self::COMPLETED => 'green', // Success, finished successfully - self::FAILED => 'red', // Error, terminated with failure - self::CANCELLED => 'purple', // Info, terminated by user - }; - } - - /** - * Get icon emoji for UI representation. - * - * Returns an emoji icon that visually represents the workflow state - * for use in user interfaces, notifications, or logs. - * - * @return string Emoji icon representing the state - * - * @example Icon usage in notifications - * ```php - * $state = $instance->getState(); - * - * $message = "Workflow {$instance->getName()} is {$state->icon()} {$state->label()}"; - * Notification::send($user, $message); - * ``` - */ - public function icon(): string - { - return match ($this) { - self::PENDING => '⏳', // Hourglass - waiting to start - self::RUNNING => '▶️', // Play button - actively running - self::WAITING => '⏸️', // Pause button - waiting for input - self::PAUSED => '⏸️', // Pause button - user paused - self::COMPLETED => '✅', // Check mark - successfully completed - self::FAILED => '❌', // X mark - failed with errors - self::CANCELLED => '🚫', // Prohibited sign - cancelled by user - }; - } - - /** - * Check if this state can transition to another state. - * - * Validates whether a state transition is allowed according to the - * workflow state machine rules. This helps prevent invalid state - * changes and ensures workflow integrity. - * - * @param self $state The target state to transition to - * @return bool True if the transition is valid, false otherwise - * - * @example State transition validation - * ```php - * $currentState = WorkflowState::PENDING; - * $targetState = WorkflowState::RUNNING; - * - * if ($currentState->canTransitionTo($targetState)) { - * $instance->setState($targetState); - * } else { - * throw new InvalidStateTransitionException($currentState, $targetState); - * } - * ``` - * @example Checking multiple transitions - * ```php - * $currentState = WorkflowState::RUNNING; - * - * $validTransitions = []; - * foreach (WorkflowState::cases() as $state) { - * if ($currentState->canTransitionTo($state)) { - * $validTransitions[] = $state; - * } - * } - * ``` - */ - public function canTransitionTo(self $state): bool - { - return match ($this) { - // From PENDING: can start running or be cancelled - self::PENDING => in_array($state, [self::RUNNING, self::CANCELLED]), - - // From RUNNING: can wait, pause, complete, fail, or be cancelled - self::RUNNING => in_array($state, [ - self::WAITING, - self::PAUSED, - self::COMPLETED, - self::FAILED, - self::CANCELLED, - ]), - - // From WAITING: can resume running, fail, or be cancelled - self::WAITING => in_array($state, [self::RUNNING, self::FAILED, self::CANCELLED]), - - // From PAUSED: can resume running or be cancelled - self::PAUSED => in_array($state, [self::RUNNING, self::CANCELLED]), - - // Terminal states cannot transition to other states - default => false, - }; - } - - /** - * Get human-readable label for the state. - * - * Returns a capitalized, user-friendly name for the state that can - * be displayed in user interfaces, reports, or logs. - * - * @return string Human-readable state label - * - * @example Display in status page - * ```php - * $instances = WorkflowInstance::all(); - * - * foreach ($instances as $instance) { - * echo "Workflow: {$instance->getName()} - Status: {$instance->getState()->label()}\n"; - * } - * ``` - */ - public function label(): string - { - return match ($this) { - self::PENDING => 'Pending', - self::RUNNING => 'Running', - self::WAITING => 'Waiting', - self::PAUSED => 'Paused', - self::COMPLETED => 'Completed', - self::FAILED => 'Failed', - self::CANCELLED => 'Cancelled', - }; - } - - /** - * Get a detailed description of what this state means. - * - * Returns a comprehensive explanation of the state, useful for - * tooltips, help documentation, or detailed status reports. - * - * @return string Detailed state description - * - * @example Tooltip or help text - * ```php - * $state = $instance->getState(); - * - * echo "{$state->label()}"; - * ``` - */ - public function description(): string - { - return match ($this) { - self::PENDING => 'The workflow has been created but execution has not yet started. It is waiting to be triggered or scheduled.', - self::RUNNING => 'The workflow is actively executing steps. One or more actions are currently being processed.', - self::WAITING => 'The workflow is paused waiting for external input, conditions to be met, or scheduled delays to complete.', - self::PAUSED => 'The workflow execution has been temporarily suspended by a user and can be resumed at any time.', - self::COMPLETED => 'The workflow has finished successfully. All steps have been executed and the final state has been reached.', - self::FAILED => 'The workflow execution has terminated due to errors, step failures, or unrecoverable conditions.', - self::CANCELLED => 'The workflow execution was terminated by user action before it could complete naturally.', - }; - } - - /** - * Check if this is a successful terminal state. - * - * @return bool True if the workflow completed successfully - */ - public function isSuccessful(): bool - { - return $this === self::COMPLETED; - } - - /** - * Check if this is an error terminal state. - * - * @return bool True if the workflow ended with an error - */ - public function isError(): bool - { - return $this === self::FAILED; - } - - /** - * Get all possible states that can be transitioned to from this state. - * - * @return array Array of valid target states - * - * @example Get available transitions - * ```php - * $currentState = WorkflowState::RUNNING; - * $availableStates = $currentState->getValidTransitions(); - * - * // Show available actions to user - * foreach ($availableStates as $state) { - * echo "Can transition to: {$state->label()}\n"; - * } - * ``` - */ - public function getValidTransitions(): array - { - $validStates = []; - - foreach (self::cases() as $state) { - if ($this->canTransitionTo($state)) { - $validStates[] = $state; - } - } - - return $validStates; - } -} diff --git a/packages/workflow-engine-core/src/Events/WorkflowCancelled.php b/packages/workflow-engine-core/src/Events/WorkflowCancelled.php deleted file mode 100644 index ff90cb4..0000000 --- a/packages/workflow-engine-core/src/Events/WorkflowCancelled.php +++ /dev/null @@ -1,14 +0,0 @@ -instance = $instance; - } -} diff --git a/packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php b/packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php deleted file mode 100644 index 0c26cee..0000000 --- a/packages/workflow-engine-core/src/Events/WorkflowFailedEvent.php +++ /dev/null @@ -1,19 +0,0 @@ -instance = $instance; - $this->exception = $exception; - } -} diff --git a/packages/workflow-engine-core/src/Events/WorkflowStarted.php b/packages/workflow-engine-core/src/Events/WorkflowStarted.php deleted file mode 100644 index f23cb08..0000000 --- a/packages/workflow-engine-core/src/Events/WorkflowStarted.php +++ /dev/null @@ -1,12 +0,0 @@ - $actionClass, - 'step_id' => $stepId, - 'error_type' => $errorType, - 'class_exists' => class_exists($actionClass), - 'suggested_namespace' => $this->suggestNamespace($actionClass), - ]; - - parent::__construct($message, $context, 0, $previous); - } - - /** - * Get the action class that was not found. - * - * @return string The action class name - */ - public function getActionClass(): string - { - return $this->actionClass; - } - - /** - * Get the step ID that references this action. - * - * @return string The step ID - */ - public function getStepId(): string - { - return $this->stepId; - } - - /** - * Get the error type. - * - * @return string|null The error type - */ - public function getErrorType(): ?string - { - return $this->errorType; - } - - /** - * Suggest a proper namespace for the action class. - * - * @param string $className The class name to analyze - * @return string|null Suggested namespace - */ - private function suggestNamespace(string $className): ?string - { - // Common Laravel action patterns - $suggestions = [ - 'App\\Actions\\', - 'App\\Workflow\\Actions\\', - 'App\\Services\\', - 'App\\Jobs\\', - ]; - - foreach ($suggestions as $namespace) { - if (class_exists($namespace.$className)) { - return $namespace.$className; - } - } - - return null; - } - - /** - * Get helpful suggestions for resolving this error. - * - * @return array Array of suggestion messages - */ - public function getSuggestions(): array - { - $suggestions = []; - - // Check if class exists anywhere - if (! class_exists($this->actionClass)) { - $suggestions[] = "Ensure the action class '{$this->actionClass}' exists and is properly autoloaded"; - - $suggested = $this->suggestNamespace(class_basename($this->actionClass)); - if ($suggested) { - $suggestions[] = "Did you mean '{$suggested}'?"; - } - - $suggestions[] = 'Run "composer dump-autoload" to refresh the autoloader'; - $suggestions[] = 'Check that the namespace declaration in the action class file is correct'; - } - - // Interface compliance check - if (class_exists($this->actionClass)) { - $reflection = new \ReflectionClass($this->actionClass); - if (! $reflection->implementsInterface('SolutionForest\\WorkflowMastery\\Contracts\\WorkflowAction')) { - $suggestions[] = 'Action class must implement the WorkflowAction interface'; - $suggestions[] = "Add 'implements WorkflowAction' to your class declaration"; - } - } - - $suggestions[] = 'Verify the action class is registered in your service container if using dependency injection'; - - return $suggestions; - } - - /** - * Get user-friendly error message for display. - * - * @return string User-friendly error message - */ - public function getUserMessage(): string - { - return "The action '{$this->actionClass}' for step '{$this->stepId}' could not be found or loaded."; - } - - /** - * Create exception for action not found errors. - * - * @param string $actionClass The action class that was not found - * @param Step $step The step configuration - * @param WorkflowContext $context The execution context - */ - public static function actionNotFound( - string $actionClass, - Step $step, - WorkflowContext $context - ): static { - return new self($actionClass, $step->getId(), 'action_not_found'); - } - - /** - * Create exception for invalid action class. - * - * @param string $actionClass The invalid action class name - * @param Step $step The step configuration - * @param WorkflowContext $context The execution context - */ - public static function invalidActionClass( - string $actionClass, - Step $step, - WorkflowContext $context - ): static { - return new self($actionClass, $step->getId(), 'invalid_action_class'); - } - - /** - * Create an exception for class not found errors. - * - * @param string $actionClass The missing action class name - * @param Step $step The step configuration - * @param WorkflowContext $context The execution context - */ - public static function classNotFound( - string $actionClass, - Step $step, - WorkflowContext $context - ): static { - return new self($actionClass, $step->getId(), 'class_not_found'); - } - - /** - * Create an exception for an action class with invalid interface. - * - * @param string $actionClass The invalid action class name - * @param Step $step The step configuration - * @param WorkflowContext $context The execution context - */ - public static function invalidInterface( - string $actionClass, - Step $step, - WorkflowContext $context - ): static { - return new self($actionClass, $step->getId(), 'invalid_interface'); - } -} diff --git a/packages/workflow-engine-core/src/Exceptions/InvalidWorkflowDefinitionException.php b/packages/workflow-engine-core/src/Exceptions/InvalidWorkflowDefinitionException.php deleted file mode 100644 index 273d429..0000000 --- a/packages/workflow-engine-core/src/Exceptions/InvalidWorkflowDefinitionException.php +++ /dev/null @@ -1,341 +0,0 @@ - $definition The invalid definition that caused the error - * @param string[] $validationErrors Specific validation error messages - * @param \Throwable|null $previous Previous exception - */ - public function __construct( - string $message, - array $definition = [], - protected readonly array $validationErrors = [], - ?\Throwable $previous = null - ) { - $context = [ - 'definition' => $definition, - 'validation_errors' => $validationErrors, - 'definition_keys' => array_keys($definition), - ]; - - parent::__construct($message, $context, 0, $previous); - } - - /** - * Get the validation errors that caused this exception. - * - * @return string[] Array of validation error messages - */ - public function getValidationErrors(): array - { - return $this->validationErrors; - } - - /** - * {@inheritdoc} - */ - public function getUserMessage(): string - { - return 'The workflow definition contains errors and cannot be processed. Please check the definition structure and ensure all required fields are present.'; - } - - /** - * {@inheritdoc} - */ - public function getSuggestions(): array - { - $suggestions = [ - 'Verify the workflow definition follows the correct schema', - 'Check that all required fields (name, steps) are present', - 'Ensure step IDs are unique within the workflow', - 'Validate that all action classes exist and implement WorkflowAction', - ]; - - if (! empty($this->validationErrors)) { - $suggestions[] = 'Review the specific validation errors: '.implode(', ', $this->validationErrors); - } - - $definition = $this->getContextValue('definition', []); - if (empty($definition['name'])) { - $suggestions[] = 'Add a "name" field to identify your workflow'; - } - if (empty($definition['steps'])) { - $suggestions[] = 'Add at least one step to the "steps" array'; - } - - return $suggestions; - } - - /** - * Create an exception for a missing required field. - * - * @param string $fieldName The name of the missing field - * @param array $definition The workflow definition - */ - public static function missingRequiredField(string $fieldName, array $definition): static - { - return new self( - "Required field '{$fieldName}' is missing from workflow definition", - $definition, - ["Missing required field: {$fieldName}"] - ); - } - - /** - * Create an exception for invalid step configuration. - * - * @param string $stepId The step ID with invalid configuration - * @param string $reason The reason why the step is invalid - * @param array $definition The workflow definition - */ - public static function invalidStep(string $stepId, string $reason, array $definition): static - { - return new self( - "Step '{$stepId}' has invalid configuration: {$reason}", - $definition, - ["Invalid step '{$stepId}': {$reason}"] - ); - } - - /** - * Create exception for invalid step ID. - * - * @param string $stepId The invalid step ID - */ - public static function invalidStepId(string $stepId): static - { - return new self( - message: "Invalid step ID: '{$stepId}'. Step ID cannot be empty.", - definition: ['provided_step_id' => $stepId], - validationErrors: [ - 'Use a descriptive step identifier', - 'Examples: "send_email", "validate_input", "process_payment"', - 'Ensure the step ID is not empty or whitespace-only', - ] - ); - } - - /** - * Create exception for invalid retry attempts. - * - * @param int $attempts The invalid retry attempts value - */ - public static function invalidRetryAttempts(int $attempts): static - { - return new self( - message: "Invalid retry attempts: {$attempts}. Must be between 0 and 10.", - definition: ['provided_attempts' => $attempts, 'valid_range' => '0-10'], - validationErrors: [ - 'Use a value between 0 and 10 for retry attempts', - 'Consider 0 for no retries, 3 for moderate resilience, or 5+ for critical operations', - 'Too many retries can delay workflow completion significantly', - ] - ); - } - - /** - * Create exception for invalid timeout. - * - * @param int|null $timeout The invalid timeout value - */ - public static function invalidTimeout(?int $timeout): static - { - return new self( - message: "Invalid timeout: {$timeout}. Timeout must be a positive integer or null.", - definition: ['provided_timeout' => $timeout], - validationErrors: [ - 'Use a positive integer for timeout in seconds', - 'Use null for no timeout limit', - 'Consider reasonable timeouts: 30s for quick operations, 300s for complex tasks', - ] - ); - } - - /** - * Create an exception for duplicate step ID. - * - * @param string $stepId The duplicate step ID - */ - public static function duplicateStepId(string $stepId): static - { - return new self( - message: "Duplicate step ID: '{$stepId}'. Step IDs must be unique within a workflow.", - definition: ['duplicate_step_id' => $stepId], - validationErrors: [ - 'Use unique step identifiers within each workflow', - 'Consider adding prefixes or suffixes to make IDs unique', - 'Examples: "send_email_1", "send_email_welcome", "send_email_reminder"', - ] - ); - } - - /** - * Create exception for invalid workflow name. - * - * @param string $name The invalid workflow name - * @param string|null $reason Additional reason for the error - */ - public static function invalidName(string $name, ?string $reason = null): static - { - $message = "Invalid workflow name: '{$name}'"; - if ($reason) { - $message .= ". {$reason}"; - } - - $suggestions = [ - 'Use a descriptive name that starts with a letter', - 'Only use letters, numbers, hyphens, and underscores', - 'Avoid special characters and spaces', - 'Examples: "user-onboarding", "order_processing", "documentApproval"', - ]; - - return new self( - message: $message, - definition: [ - 'provided_name' => $name, - 'validation_rule' => '/^[a-zA-Z][a-zA-Z0-9_-]*$/', - 'reason' => $reason, - ], - validationErrors: $suggestions - ); - } - - /** - * Create exception for invalid condition expression. - * - * @param string $condition The invalid condition expression - */ - public static function invalidCondition(string $condition): static - { - return new self( - message: "Invalid condition expression: '{$condition}'. Condition cannot be empty.", - definition: ['provided_condition' => $condition], - validationErrors: [ - 'Use valid condition expressions with comparison operators', - 'Examples: "user.premium === true", "order.amount > 1000", "status !== \'cancelled\'"', - 'Supported operators: ===, !==, ==, !=, >, <, >=, <=', - 'Use dot notation to access nested properties: "user.profile.type"', - ] - ); - } - - /** - * Create exception for invalid delay configuration. - * - * @param int|null $seconds Provided seconds value - * @param int|null $minutes Provided minutes value - * @param int|null $hours Provided hours value - */ - public static function invalidDelay(?int $seconds, ?int $minutes, ?int $hours): static - { - return new self( - message: 'Invalid delay configuration. At least one positive time value must be provided.', - definition: [ - 'provided_seconds' => $seconds, - 'provided_minutes' => $minutes, - 'provided_hours' => $hours, - ], - validationErrors: [ - 'Provide at least one positive time value', - 'Examples: delay(seconds: 30), delay(minutes: 5), delay(hours: 1)', - 'You can combine multiple time units: delay(hours: 1, minutes: 30)', - 'All time values must be positive integers', - ] - ); - } - - /** - * Create exception for empty workflow (no steps defined). - * - * @param string $workflowName The name of the empty workflow - */ - public static function emptyWorkflow(string $workflowName): static - { - return new self( - message: "Workflow '{$workflowName}' cannot be built with no steps defined.", - definition: ['workflow_name' => $workflowName], - validationErrors: [ - 'Add at least one step using addStep(), startWith(), or then() methods', - 'Example: $builder->addStep("validate", ValidateAction::class)', - 'Consider using common patterns: email(), delay(), or http()', - 'Use WorkflowBuilder::quick() for pre-built common workflows', - ] - ); - } - - /** - * Create exception for action class not found. - * - * @param string $actionName The missing action name or class - * @param array $context Additional context information - */ - public static function actionNotFound(string $actionName, array $context = []): static - { - $message = "Action '{$actionName}' could not be found or resolved to a valid class."; - - $suggestions = [ - 'Check if the action class exists and is properly autoloaded', - 'Verify the action name spelling and capitalization', - 'Ensure the action class implements the WorkflowAction interface', - ]; - - if (isset($context['tried_classes'])) { - $suggestions[] = 'Tried resolving to classes: '.implode(', ', $context['tried_classes']); - } - - if (isset($context['predefined_actions'])) { - $suggestions[] = 'Available predefined actions: '.implode(', ', $context['predefined_actions']); - } - - if (isset($context['custom_actions']) && ! empty($context['custom_actions'])) { - $suggestions[] = 'Available custom actions: '.implode(', ', $context['custom_actions']); - } - - if (isset($context['suggestion'])) { - $suggestions[] = $context['suggestion']; - } - - return new self( - message: $message, - definition: array_merge(['action_name' => $actionName], $context), - validationErrors: $suggestions - ); - } - - /** - * Create exception for invalid action class (doesn't implement required interface). - * - * @param string $className The invalid action class name - * @param string $requiredInterface The required interface - */ - public static function invalidActionClass(string $className, string $requiredInterface): static - { - return new self( - message: "Class '{$className}' does not implement the required '{$requiredInterface}' interface.", - definition: [ - 'class_name' => $className, - 'required_interface' => $requiredInterface, - 'class_exists' => class_exists($className), - 'implemented_interfaces' => class_exists($className) ? class_implements($className) : [], - ], - validationErrors: [ - "Make sure '{$className}' implements the '{$requiredInterface}' interface", - 'Check that the class has the required methods: execute(), canExecute(), getName(), getDescription()', - 'Verify the class is properly imported and autoloaded', - "Example: class MyAction implements {$requiredInterface} { ... }", - ] - ); - } -} diff --git a/packages/workflow-engine-core/src/Exceptions/InvalidWorkflowStateException.php b/packages/workflow-engine-core/src/Exceptions/InvalidWorkflowStateException.php deleted file mode 100644 index 8695efe..0000000 --- a/packages/workflow-engine-core/src/Exceptions/InvalidWorkflowStateException.php +++ /dev/null @@ -1,214 +0,0 @@ - $instanceId, - 'current_state' => $currentState->value, - 'attempted_state' => $attemptedState->value, - 'current_state_label' => $currentState->label(), - 'attempted_state_label' => $attemptedState->label(), - 'valid_transitions' => $this->getValidTransitions($currentState), - ]; - - parent::__construct($message, $context, 0, $previous); - } - - /** - * Get the current workflow state. - * - * @return WorkflowState The current state - */ - public function getCurrentState(): WorkflowState - { - return $this->currentState; - } - - /** - * Get the attempted workflow state. - * - * @return WorkflowState The attempted state - */ - public function getAttemptedState(): WorkflowState - { - return $this->attemptedState; - } - - /** - * Get the workflow instance ID. - * - * @return string The instance ID - */ - public function getInstanceId(): string - { - return $this->instanceId; - } - - /** - * Get valid state transitions from the current state. - * - * @param WorkflowState $state The state to get transitions for - * @return string[] Array of valid state values - */ - private function getValidTransitions(WorkflowState $state): array - { - $validTransitions = []; - - foreach (WorkflowState::cases() as $targetState) { - if ($state->canTransitionTo($targetState)) { - $validTransitions[] = $targetState->value; - } - } - - return $validTransitions; - } - - /** - * {@inheritdoc} - */ - public function getUserMessage(): string - { - $currentLabel = $this->currentState->label(); - $attemptedLabel = $this->attemptedState->label(); - - return "Cannot transition workflow from '{$currentLabel}' state to '{$attemptedLabel}' state. ". - 'This transition is not allowed by the workflow lifecycle rules.'; - } - - /** - * {@inheritdoc} - */ - public function getSuggestions(): array - { - $validTransitions = $this->getContextValue('valid_transitions', []); - $suggestions = []; - - if (! empty($validTransitions)) { - $validStates = implode(', ', $validTransitions); - $suggestions[] = "Valid transitions from '{$this->currentState->value}' are: {$validStates}"; - } else { - $suggestions[] = "No valid transitions are available from the current state '{$this->currentState->value}'"; - } - - // State-specific suggestions - switch ($this->currentState) { - case WorkflowState::COMPLETED: - $suggestions[] = 'Completed workflows cannot be modified - create a new workflow instance if needed'; - $suggestions[] = 'Consider using workflow versioning for updates to completed workflows'; - break; - - case WorkflowState::FAILED: - $suggestions[] = 'Failed workflows can only be retried or cancelled'; - $suggestions[] = 'Review and fix the underlying issue before retrying'; - break; - - case WorkflowState::CANCELLED: - $suggestions[] = 'Cancelled workflows cannot be resumed - create a new instance if needed'; - break; - - case WorkflowState::PENDING: - $suggestions[] = 'Start the workflow execution to transition from pending state'; - break; - - case WorkflowState::RUNNING: - $suggestions[] = 'Allow the workflow to complete naturally, or cancel if needed'; - break; - } - - return $suggestions; - } - - /** - * Create an exception for attempting to resume a completed workflow. - * - * @param string $instanceId The workflow instance ID - */ - public static function cannotResumeCompleted(string $instanceId): static - { - return new self( - "Cannot resume workflow '{$instanceId}' because it is already completed", - WorkflowState::COMPLETED, - WorkflowState::RUNNING, - $instanceId - ); - } - - /** - * Create an exception for attempting to cancel a failed workflow. - * - * @param string $instanceId The workflow instance ID - */ - public static function cannotCancelFailed(string $instanceId): static - { - return new self( - "Cannot cancel workflow '{$instanceId}' because it has already failed", - WorkflowState::FAILED, - WorkflowState::CANCELLED, - $instanceId - ); - } - - /** - * Create an exception for attempting to start an already running workflow. - * - * @param string $instanceId The workflow instance ID - */ - public static function alreadyRunning(string $instanceId): static - { - return new self( - "Cannot start workflow '{$instanceId}' because it is already running", - WorkflowState::RUNNING, - WorkflowState::RUNNING, - $instanceId - ); - } - - /** - * Create an exception from a workflow instance with state transition details. - * - * @param WorkflowInstance $instance The workflow instance - * @param WorkflowState $attemptedState The attempted state - * @param string $operation The operation that was attempted - */ - public static function fromInstanceTransition( - WorkflowInstance $instance, - WorkflowState $attemptedState, - string $operation - ): static { - $message = "Cannot {$operation} workflow '{$instance->getId()}' - invalid state transition"; - - return new self( - $message, - $instance->getState(), - $attemptedState, - $instance->getId() - ); - } -} diff --git a/packages/workflow-engine-core/src/Exceptions/StepExecutionException.php b/packages/workflow-engine-core/src/Exceptions/StepExecutionException.php deleted file mode 100644 index bf76fc1..0000000 --- a/packages/workflow-engine-core/src/Exceptions/StepExecutionException.php +++ /dev/null @@ -1,244 +0,0 @@ - $step->getId(), - 'step_action' => $step->getActionClass(), - 'step_config' => $step->getConfig(), - 'attempt_number' => $attemptNumber, - 'workflow_id' => $context->getWorkflowId(), - 'context_data' => $context->getData(), - 'execution_time' => $context->executedAt->format('Y-m-d H:i:s'), - ]; - - parent::__construct($message, $contextData, 0, $previous); - } - - /** - * Get the step that failed. - * - * @return Step The failed step - */ - public function getStep(): Step - { - return $this->step; - } - - /** - * Get the current attempt number. - * - * @return int The attempt number (1-based) - */ - public function getAttemptNumber(): int - { - return $this->attemptNumber; - } - - /** - * Check if this step can be retried. - * - * @return bool True if the step supports retries and hasn't exceeded the limit - */ - public function canRetry(): bool - { - $maxRetries = $this->step->getConfig()['retry_attempts'] ?? 0; - - return $this->attemptNumber <= $maxRetries; - } - - /** - * {@inheritdoc} - */ - public function getUserMessage(): string - { - $stepName = $this->step->getId(); - $actionClass = class_basename($this->step->getActionClass()); - - return "The workflow step '{$stepName}' (using {$actionClass}) failed to execute. ". - 'This may be due to invalid input data, external service issues, or configuration problems.'; - } - - /** - * {@inheritdoc} - */ - public function getSuggestions(): array - { - $suggestions = []; - $actionClass = $this->step->getActionClass(); - - // Basic suggestions - $suggestions[] = "Verify the action class '{$actionClass}' is properly implemented"; - $suggestions[] = 'Check the step configuration and input data for correctness'; - $suggestions[] = 'Review the logs for more detailed error information'; - - // Retry-specific suggestions - if ($this->canRetry()) { - $maxRetries = $this->step->getConfig()['retry_attempts'] ?? 0; - $remaining = $maxRetries - $this->attemptNumber + 1; - $suggestions[] = "This step will be retried automatically ({$remaining} attempts remaining)"; - } elseif (isset($this->step->getConfig()['retry_attempts'])) { - $suggestions[] = 'All retry attempts have been exhausted'; - $suggestions[] = 'Consider increasing the retry_attempts configuration if this is a transient issue'; - } else { - $suggestions[] = "Consider adding retry logic by setting 'retry_attempts' in the step configuration"; - } - - // Action-specific suggestions - if (str_contains($actionClass, 'Http') || str_contains($actionClass, 'Api')) { - $suggestions[] = 'If this is an HTTP/API action, check network connectivity and service availability'; - $suggestions[] = 'Verify API credentials and endpoint URLs are correct'; - } - - if (str_contains($actionClass, 'Database') || str_contains($actionClass, 'Sql')) { - $suggestions[] = 'If this is a database action, check database connectivity and permissions'; - $suggestions[] = 'Verify the SQL queries and table/column names are correct'; - } - - if (str_contains($actionClass, 'Email') || str_contains($actionClass, 'Mail')) { - $suggestions[] = 'If this is an email action, check SMTP configuration and recipient addresses'; - } - - // Context-specific suggestions - $contextData = $this->getContextValue('context_data', []); - if (empty($contextData)) { - $suggestions[] = 'The workflow context is empty - ensure previous steps are setting data correctly'; - } - - return $suggestions; - } - - /** - * Create an exception for action class not found. - * - * @param string $actionClass The missing action class - * @param Step $step The step configuration - * @param WorkflowContext $context The execution context - */ - public static function actionClassNotFound( - string $actionClass, - Step $step, - WorkflowContext $context - ): static { - $message = "Action class '{$actionClass}' does not exist or could not be loaded"; - $exception = new self($message, $step, $context); - $exception->context['error_type'] = 'class_not_found'; - $exception->context['missing_class'] = $actionClass; - - return $exception; - } - - /** - * Create an exception for invalid action class. - * - * @param string $actionClass The invalid action class - * @param Step $step The step configuration - * @param WorkflowContext $context The execution context - */ - public static function invalidActionClass( - string $actionClass, - Step $step, - WorkflowContext $context - ): static { - $message = "Class '{$actionClass}' does not implement the WorkflowAction interface"; - $exception = new self($message, $step, $context); - $exception->context['error_type'] = 'invalid_interface'; - $exception->context['required_interface'] = 'WorkflowAction'; - - return $exception; - } - - /** - * Create an exception for timeout. - * - * @param int $timeout The timeout duration in seconds - * @param Step $step The step configuration - * @param WorkflowContext $context The execution context - */ - public static function timeout( - int $timeout, - Step $step, - WorkflowContext $context - ): static { - $message = "Step '{$step->getId()}' timed out after {$timeout} seconds"; - $exception = new self($message, $step, $context); - $exception->context['error_type'] = 'timeout'; - $exception->context['timeout_seconds'] = $timeout; - - return $exception; - } - - /** - * Create a StepExecutionException from any other exception. - * - * @param \Exception $exception The original exception - * @param Step $step The step that failed - * @param WorkflowContext $context The execution context - */ - public static function fromException( - \Exception $exception, - Step $step, - WorkflowContext $context - ): static { - $message = "Step '{$step->getId()}' failed: {$exception->getMessage()}"; - $stepException = new self($message, $step, $context, 0, $exception); - - $stepException->context['original_exception'] = get_class($exception); - $stepException->context['original_message'] = $exception->getMessage(); - $stepException->context['original_code'] = $exception->getCode(); - $stepException->context['original_file'] = $exception->getFile(); - $stepException->context['original_line'] = $exception->getLine(); - - return $stepException; - } - - /** - * Create an exception for action execution failure. - * - * @param string $errorMessage The action error message - * @param Step $step The step that failed - * @param WorkflowContext $context The execution context - */ - public static function actionFailed( - string $errorMessage, - Step $step, - WorkflowContext $context - ): static { - $message = "Action execution failed in step '{$step->getId()}': {$errorMessage}"; - $exception = new self($message, $step, $context); - - $exception->context['error_type'] = 'action_execution_failed'; - $exception->context['action_class'] = $step->getActionClass(); - $exception->context['action_error'] = $errorMessage; - - return $exception; - } -} diff --git a/packages/workflow-engine-core/src/Exceptions/WorkflowException.php b/packages/workflow-engine-core/src/Exceptions/WorkflowException.php deleted file mode 100644 index 2158913..0000000 --- a/packages/workflow-engine-core/src/Exceptions/WorkflowException.php +++ /dev/null @@ -1,153 +0,0 @@ - $context Additional context data for debugging - * @param int $code The error code (default: 0) - * @param Throwable|null $previous The previous throwable used for chaining - */ - public function __construct( - string $message, - protected array $context = [], - int $code = 0, - ?Throwable $previous = null - ) { - parent::__construct($message, $code, $previous); - } - - /** - * Get the context data for this exception. - * - * Contains debugging information such as workflow instance details, - * step information, configuration, and execution state. - * - * @return array The context data - */ - public function getContext(): array - { - return $this->context; - } - - /** - * Get a specific context value. - * - * @param string $key The context key to retrieve - * @param mixed $default The default value if key doesn't exist - * @return mixed The context value or default - */ - public function getContextValue(string $key, mixed $default = null): mixed - { - return $this->context[$key] ?? $default; - } - - /** - * Get formatted debug information for logging and error reporting. - * - * @return array Structured debug information - */ - public function getDebugInfo(): array - { - return [ - 'exception_type' => static::class, - 'message' => $this->getMessage(), - 'code' => $this->getCode(), - 'file' => $this->getFile(), - 'line' => $this->getLine(), - 'context' => $this->getContext(), - 'suggestions' => $this->getSuggestions(), - 'trace' => $this->getTraceAsString(), - ]; - } - - /** - * Get helpful suggestions for resolving this error. - * - * Override this method in specific exception classes to provide - * contextual suggestions based on the error type. - * - * @return string[] Array of suggestion strings - */ - public function getSuggestions(): array - { - return [ - 'Check the workflow definition for syntax errors', - 'Verify all required action classes exist and are accessible', - 'Review the execution logs for additional context', - ]; - } - - /** - * Get a user-friendly error summary. - * - * Provides a concise explanation of what went wrong without - * exposing internal implementation details. - * - * @return string User-friendly error description - */ - abstract public function getUserMessage(): string; - - /** - * Create an exception from a workflow context. - * - * @param string $message The error message - * @param WorkflowContext $context The workflow context - * @param Throwable|null $previous Previous exception - * @return static The created exception instance - */ - public static function fromContext( - string $message, - WorkflowContext $context, - ?Throwable $previous = null - ): static { - // @phpstan-ignore-next-line new.static - return new static($message, [ - 'workflow_id' => $context->getWorkflowId(), - 'step_id' => $context->getStepId(), - 'context_data' => $context->getData(), - 'config' => $context->getConfig(), - 'executed_at' => $context->executedAt->format('Y-m-d H:i:s'), - ], 0, $previous); - } - - /** - * Create an exception from a workflow instance. - * - * @param string $message The error message - * @param WorkflowInstance $instance The workflow instance - * @param Throwable|null $previous Previous exception - * @return static The created exception instance - */ - public static function fromInstance( - string $message, - WorkflowInstance $instance, - ?Throwable $previous = null - ): static { - // @phpstan-ignore-next-line new.static - return new static($message, [ - 'instance_id' => $instance->getId(), - 'workflow_name' => $instance->getDefinition()->getName(), - 'current_state' => $instance->getState()->value, - 'current_step' => $instance->getCurrentStepId(), - 'instance_data' => $instance->getData(), - 'created_at' => $instance->getCreatedAt()->format('Y-m-d H:i:s'), - 'updated_at' => $instance->getUpdatedAt()->format('Y-m-d H:i:s'), - ], 0, $previous); - } -} diff --git a/packages/workflow-engine-core/src/Exceptions/WorkflowInstanceNotFoundException.php b/packages/workflow-engine-core/src/Exceptions/WorkflowInstanceNotFoundException.php deleted file mode 100644 index 0cffec6..0000000 --- a/packages/workflow-engine-core/src/Exceptions/WorkflowInstanceNotFoundException.php +++ /dev/null @@ -1,168 +0,0 @@ - $searchFilters Any filters that were applied during search - * @param \Throwable|null $previous Previous exception - */ - public function __construct( - protected readonly string $instanceId, - protected readonly ?string $storageType = null, - array $searchFilters = [], - ?\Throwable $previous = null - ) { - $message = "Workflow instance '{$instanceId}' was not found"; - - $context = [ - 'instance_id' => $instanceId, - 'storage_type' => $storageType, - 'search_filters' => $searchFilters, - ]; - - parent::__construct($message, $context, 0, $previous); - } - - /** - * Get the workflow instance ID that was not found. - * - * @return string The instance ID - */ - public function getInstanceId(): string - { - return $this->instanceId; - } - - /** - * Get the storage type being used. - * - * @return string|null The storage adapter type - */ - public function getStorageType(): ?string - { - return $this->storageType; - } - - /** - * {@inheritdoc} - */ - public function getUserMessage(): string - { - return "The requested workflow instance '{$this->instanceId}' could not be found. ". - 'It may have been deleted, or the instance ID may be incorrect.'; - } - - /** - * {@inheritdoc} - */ - public function getSuggestions(): array - { - $suggestions = [ - "Verify the workflow instance ID '{$this->instanceId}' is correct", - 'Check if the workflow instance was created successfully', - 'Ensure the storage configuration is working properly', - ]; - - // Storage-specific suggestions - if ($this->storageType) { - switch (strtolower($this->storageType)) { - case 'database': - $suggestions[] = 'Check database connectivity and table structure'; - $suggestions[] = 'Verify the workflow_instances table exists and is accessible'; - $suggestions[] = 'Look for any database migration issues'; - break; - - case 'file': - $suggestions[] = 'Check file system permissions for the storage directory'; - $suggestions[] = 'Verify the storage directory exists and is writable'; - $suggestions[] = 'Look for any file corruption or disk space issues'; - break; - - case 'redis': - $suggestions[] = 'Check Redis connectivity and configuration'; - $suggestions[] = 'Verify Redis is running and accessible'; - $suggestions[] = 'Check if the Redis key may have expired'; - break; - } - } - - // ID format suggestions - if (strlen($this->instanceId) < 10) { - $suggestions[] = "The instance ID seems unusually short - verify it's a complete ID"; - } - - if (preg_match('/[^a-zA-Z0-9\-_]/', $this->instanceId)) { - $suggestions[] = "The instance ID contains unusual characters - ensure it's properly formatted"; - } - - $suggestions[] = "Use the workflow engine's getInstance() method to check if the instance exists"; - $suggestions[] = 'Review recent logs for any deletion or cleanup operations'; - - return $suggestions; - } - - /** - * Create an exception for a malformed instance ID. - * - * @param string $instanceId The malformed instance ID - * @param string $expectedFormat Description of the expected format - * @param string|null $storageType The storage type being used - */ - public static function malformedId( - string $instanceId, - string $expectedFormat, - ?string $storageType = null - ): static { - $exception = new self($instanceId, $storageType); - $exception->context['error_type'] = 'malformed_id'; - $exception->context['expected_format'] = $expectedFormat; - - return $exception; - } - - /** - * Create an exception for storage connectivity issues. - * - * @param string $instanceId The instance ID that was being searched for - * @param string $storageType The storage type that failed - * @param string $connectionError The connection error message - */ - public static function storageConnectionError( - string $instanceId, - string $storageType, - string $connectionError - ): static { - $exception = new self($instanceId, $storageType); - $exception->context['error_type'] = 'storage_connection'; - $exception->context['connection_error'] = $connectionError; - - return $exception; - } - - /** - * Create an exception for a workflow instance that was not found. - * - * @param string $instanceId The workflow instance ID that was not found - * @param string|null $storageType The storage adapter type - */ - public static function notFound(string $instanceId, ?string $storageType = null): static - { - return new self( - $instanceId, - $storageType, - [] // empty search filters - ); - } -} diff --git a/packages/workflow-engine-core/src/Support/SimpleWorkflow.php b/packages/workflow-engine-core/src/Support/SimpleWorkflow.php deleted file mode 100644 index 1a4a9a5..0000000 --- a/packages/workflow-engine-core/src/Support/SimpleWorkflow.php +++ /dev/null @@ -1,206 +0,0 @@ -sequential('user-onboarding', [ - * SendWelcomeEmailAction::class, - * CreateUserProfileAction::class, - * AssignDefaultRoleAction::class, - * ], ['user_id' => 123]); - * ``` - * @example Using static methods - * ```php - * // Quick single action execution - * SimpleWorkflow::runAction(SendEmailAction::class, [ - * 'to' => 'user@example.com', - * 'subject' => 'Welcome!' - * ]); - * ``` - */ -class SimpleWorkflow -{ - /** - * The underlying workflow engine for execution. - */ - private WorkflowEngine $engine; - - /** - * Create a new SimpleWorkflow instance. - * - * @param StorageAdapter $storage Storage adapter for workflow persistence - * @param Dispatcher|null $eventDispatcher Optional event dispatcher for workflow events - */ - public function __construct(StorageAdapter $storage, ?Dispatcher $eventDispatcher = null) - { - $this->engine = new WorkflowEngine($storage, $eventDispatcher); - } - - /** - * Create and execute a simple sequential workflow. - * - * Creates a workflow where actions execute one after another - * in the order they are provided. - * - * @param string $name Workflow name/identifier - * @param array $actions Array of action class names - * @param array $context Initial workflow context data - * @return string The workflow instance ID - * - * @example Sequential workflow - * ```php - * $instanceId = $simple->sequential('user-onboarding', [ - * SendWelcomeEmailAction::class, - * CreateUserProfileAction::class, - * AssignDefaultRoleAction::class, - * ], ['user_id' => 123]); - * ``` - */ - public function sequential(string $name, array $actions, array $context = []): string - { - $builder = WorkflowBuilder::create($name); - - // Use the fluent 'then' method for sequential execution - foreach ($actions as $action) { - $builder->then($action); - } - - $workflow = $builder->build(); - - return $this->engine->start($name.'_'.uniqid(), $workflow->toArray(), $context); - } - - /** - * Run a single action as a complete workflow. - * - * Convenience method for executing a single action with - * the full workflow infrastructure. - * - * @param string $actionClass The action class to execute - * @param array $context Context data for the action - * @return string The workflow instance ID - * - * @example Single action execution - * ```php - * $instanceId = $simple->runAction(SendEmailAction::class, [ - * 'to' => 'user@example.com', - * 'subject' => 'Welcome to our platform!' - * ]); - * ``` - */ - public function runAction(string $actionClass, array $context = []): string - { - return $this->sequential( - 'single_action_'.class_basename($actionClass), - [$actionClass], - $context - ); - } - - /** - * Get the underlying workflow engine instance. - * - * Provides access to the full WorkflowEngine API when - * the simplified methods are insufficient. - * - * @return WorkflowEngine The workflow engine instance - * - * @example Advanced usage - * ```php - * $simple = new SimpleWorkflow($storage); - * - * // Use the engine directly for advanced operations - * $engine = $simple->getEngine(); - * $instances = $engine->listInstances(['state' => 'running']); - * ``` - */ - public function getEngine(): WorkflowEngine - { - return $this->engine; - } - - /** - * Create and execute a workflow from a builder. - * - * Allows using the full WorkflowBuilder API while still - * benefiting from the simple execution interface. - * - * @param WorkflowBuilder $builder Configured workflow builder - * @param array $context Initial workflow context - * @return string The workflow instance ID - * - * @example Custom workflow with builder - * ```php - * $builder = WorkflowBuilder::create('complex-workflow') - * ->addStep('validate', ValidateAction::class) - * ->addStep('process', ProcessAction::class) - * ->addConditionalStep('notify', NotifyAction::class, 'success === true') - * ->addTransition('validate', 'process') - * ->addTransition('process', 'notify'); - * - * $instanceId = $simple->executeBuilder($builder, $context); - * ``` - */ - public function executeBuilder(WorkflowBuilder $builder, array $context = []): string - { - $workflow = $builder->build(); - - return $this->engine->start( - $workflow->getName().'_'.uniqid(), - $workflow->toArray(), - $context - ); - } - - /** - * Resume a paused workflow instance. - * - * Convenience method for resuming workflow execution. - * - * @param string $instanceId The workflow instance ID to resume - */ - public function resume(string $instanceId): void - { - $this->engine->resume($instanceId); - } - - /** - * Get the status of a workflow instance. - * - * @param string $instanceId The workflow instance ID - * @return array Workflow instance status information - */ - public function getStatus(string $instanceId): array - { - $instance = $this->engine->getInstance($instanceId); - - return [ - 'id' => $instance->getId(), - 'state' => $instance->getState()->value, - 'current_step' => $instance->getCurrentStepId(), - 'progress' => $instance->getProgress(), - 'completed_steps' => $instance->getCompletedSteps(), - 'failed_steps' => $instance->getFailedSteps(), - 'error_message' => $instance->getErrorMessage(), - 'created_at' => $instance->getCreatedAt(), - 'updated_at' => $instance->getUpdatedAt(), - ]; - } -} diff --git a/packages/workflow-engine-core/src/Support/Uuid.php b/packages/workflow-engine-core/src/Support/Uuid.php deleted file mode 100644 index d4105ff..0000000 --- a/packages/workflow-engine-core/src/Support/Uuid.php +++ /dev/null @@ -1,105 +0,0 @@ -start($workflowId, $definition, $context); - * ``` - * - * ### Generate Unique Step Execution ID - * ```php - * $executionId = Uuid::v4(); - * $context = $context->withMetadata('execution_id', $executionId); - * ``` - * - * ### Generate Correlation IDs - * ```php - * $correlationId = Uuid::v4(); - * Log::info('Starting workflow execution', [ - * 'workflow_id' => $workflowId, - * 'correlation_id' => $correlationId - * ]); - * ``` - * - * ## Format Specification - * - * Generated UUIDs follow the standard format: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` - * - **Version**: 4 (random) - * - **Variant**: DCE 1.1 (RFC 4122) - * - **Length**: 36 characters including hyphens - * - **Character Set**: Hexadecimal (0-9, a-f) - * - * @see https://tools.ietf.org/html/rfc4122 RFC 4122 UUID specification - */ -class Uuid -{ - /** - * Generate a version 4 (random) UUID string. - * - * Creates a new UUID v4 using pseudo-random number generation. - * The generated UUID is compliant with RFC 4122 and suitable for - * use as unique identifiers in distributed systems. - * - * @return string A 36-character UUID v4 string in canonical format - * - * @example Basic usage - * ```php - * $uuid = Uuid::v4(); - * echo $uuid; // "550e8400-e29b-41d4-a716-446655440000" - * - * // Validate format - * $isValid = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $uuid); - * ``` - * @example Use in workflow context - * ```php - * $workflowInstance = new WorkflowInstance( - * id: Uuid::v4(), - * definition: $definition, - * state: WorkflowState::PENDING - * ); - * ``` - */ - public static function v4(): string - { - return sprintf( - '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - // 32 bits for "time_low" - mt_rand(0, 0xFFFF), mt_rand(0, 0xFFFF), - - // 16 bits for "time_mid" - mt_rand(0, 0xFFFF), - - // 16 bits for "time_hi_and_version", - // four most significant bits holds version number 4 - mt_rand(0, 0x0FFF) | 0x4000, - - // 16 bits, 8 bits for "clk_seq_hi_res", - // 8 bits for "clk_seq_low", - // two most significant bits holds zero and one for variant DCE1.1 - mt_rand(0, 0x3FFF) | 0x8000, - - // 48 bits for "node" - mt_rand(0, 0xFFFF), mt_rand(0, 0xFFFF), mt_rand(0, 0xFFFF) - ); - } -} diff --git a/packages/workflow-engine-core/src/helpers.php b/packages/workflow-engine-core/src/helpers.php deleted file mode 100644 index 5e1312a..0000000 --- a/packages/workflow-engine-core/src/helpers.php +++ /dev/null @@ -1,65 +0,0 @@ -start($workflowId, $definition, $context); - } -} - -if (! function_exists('resume_workflow')) { - /** - * Resume an existing workflow - */ - function resume_workflow(string $instanceId): WorkflowInstance - { - return workflow()->resume($instanceId); - } -} - -if (! function_exists('get_workflow')) { - /** - * Get a workflow instance - */ - function get_workflow(string $instanceId): WorkflowInstance - { - return workflow()->getInstance($instanceId); - } -} - -if (! function_exists('cancel_workflow')) { - /** - * Cancel a workflow - */ - function cancel_workflow(string $instanceId, string $reason = ''): WorkflowInstance - { - return workflow()->cancel($instanceId, $reason); - } -} - -if (! function_exists('workflow_definition')) { - /** - * Create a workflow definition from array - */ - function workflow_definition(array $definition): WorkflowDefinition - { - return WorkflowDefinition::fromArray($definition); - } -} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/CreateShipmentAction.php b/packages/workflow-engine-core/tests/Actions/ECommerce/CreateShipmentAction.php deleted file mode 100644 index 5b3a86b..0000000 --- a/packages/workflow-engine-core/tests/Actions/ECommerce/CreateShipmentAction.php +++ /dev/null @@ -1,48 +0,0 @@ -getData('order'); - - // Mock shipment creation - $shipmentId = 'ship_'.uniqid(); - $trackingNumber = 'TRK'.mt_rand(100000, 999999); - - $context->setData('shipment.id', $shipmentId); - $context->setData('shipment.tracking_number', $trackingNumber); - $context->setData('shipment.created', true); - - return new ActionResult( - success: true, - data: [ - 'shipment_id' => $shipmentId, - 'tracking_number' => $trackingNumber, - 'status' => 'created', - ] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order') && - $context->getData('payment.success') === true; - } - - public function getName(): string - { - return 'Create Shipment'; - } - - public function getDescription(): string - { - return 'Creates shipment and generates tracking number'; - } -} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/FraudCheckAction.php b/packages/workflow-engine-core/tests/Actions/ECommerce/FraudCheckAction.php deleted file mode 100644 index 0dddf40..0000000 --- a/packages/workflow-engine-core/tests/Actions/ECommerce/FraudCheckAction.php +++ /dev/null @@ -1,55 +0,0 @@ -getData('order'); - - // Mock fraud detection logic - $riskScore = $this->calculateRiskScore($order); - $context->setData('fraud.risk', $riskScore); - - return new ActionResult( - success: true, - data: ['risk_score' => $riskScore, 'status' => $riskScore < 0.7 ? 'safe' : 'flagged'] - ); - } - - private function calculateRiskScore(array $order): float - { - // Simple mock risk calculation - $baseRisk = 0.1; - - if ($order['total'] > 10000) { - $baseRisk += 0.3; - } - - if ($order['total'] > 50000) { - $baseRisk += 0.4; - } - - return min($baseRisk, 1.0); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order') && $context->getData('order.valid') === true; - } - - public function getName(): string - { - return 'Fraud Check'; - } - - public function getDescription(): string - { - return 'Analyzes order for potential fraud indicators'; - } -} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/NotificationAndCompensationActions.php b/packages/workflow-engine-core/tests/Actions/ECommerce/NotificationAndCompensationActions.php deleted file mode 100644 index c4ca6c3..0000000 --- a/packages/workflow-engine-core/tests/Actions/ECommerce/NotificationAndCompensationActions.php +++ /dev/null @@ -1,123 +0,0 @@ -getData('order'); - $shipment = $context->getData('shipment'); - - // Mock notification sending - $notificationId = 'notif_'.uniqid(); - - $context->setData('notification.id', $notificationId); - $context->setData('notification.sent', true); - $context->setData('notification.type', 'order_confirmation'); - - return new ActionResult( - success: true, - data: [ - 'notification_id' => $notificationId, - 'recipient' => $order['customer_email'] ?? 'customer@example.com', - 'tracking_number' => $shipment['tracking_number'] ?? null, - 'status' => 'sent', - ] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order') && - $context->getData('shipment.created') === true; - } - - public function getName(): string - { - return 'Send Order Confirmation'; - } - - public function getDescription(): string - { - return 'Sends order confirmation email to customer'; - } -} - -// Compensation Actions -class ReleaseInventoryAction implements WorkflowAction -{ - public function execute(WorkflowContext $context): ActionResult - { - $reservationId = $context->getData('inventory.reservation_id'); - - if ($reservationId) { - $context->setData('inventory.reserved', false); - $context->setData('inventory.released', true); - } - - return new ActionResult( - success: true, - data: ['reservation_id' => $reservationId, 'status' => 'released'] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('inventory.reservation_id'); - } - - public function getName(): string - { - return 'Release Inventory'; - } - - public function getDescription(): string - { - return 'Releases previously reserved inventory'; - } -} - -class RefundPaymentAction implements WorkflowAction -{ - public function execute(WorkflowContext $context): ActionResult - { - $paymentId = $context->getData('payment.id'); - $amount = $context->getData('payment.amount'); - - if ($paymentId) { - $refundId = 'ref_'.uniqid(); - $context->setData('refund.id', $refundId); - $context->setData('refund.amount', $amount); - $context->setData('refund.processed', true); - } - - return new ActionResult( - success: true, - data: [ - 'refund_id' => $refundId ?? null, - 'amount' => $amount, - 'status' => 'processed', - ] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('payment.id'); - } - - public function getName(): string - { - return 'Refund Payment'; - } - - public function getDescription(): string - { - return 'Processes payment refund'; - } -} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/ProcessPaymentAction.php b/packages/workflow-engine-core/tests/Actions/ECommerce/ProcessPaymentAction.php deleted file mode 100644 index 7f34495..0000000 --- a/packages/workflow-engine-core/tests/Actions/ECommerce/ProcessPaymentAction.php +++ /dev/null @@ -1,54 +0,0 @@ -getData('order'); - - // Mock payment processing - $paymentId = 'pay_'.uniqid(); - $success = $order['total'] < 100000; // Simulate payment failure for very large orders - - if ($success) { - $context->setData('payment.id', $paymentId); - $context->setData('payment.success', true); - $context->setData('payment.amount', $order['total']); - } else { - $context->setData('payment.success', false); - $context->setData('payment.error', 'Payment declined'); - } - - return new ActionResult( - success: $success, - data: [ - 'payment_id' => $success ? $paymentId : null, - 'amount' => $order['total'], - 'status' => $success ? 'completed' : 'failed', - ], - errorMessage: $success ? null : 'Payment processing failed' - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order') && - $context->getData('inventory.reserved') === true; - } - - public function getName(): string - { - return 'Process Payment'; - } - - public function getDescription(): string - { - return 'Processes payment for the order'; - } -} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/ReserveInventoryAction.php b/packages/workflow-engine-core/tests/Actions/ECommerce/ReserveInventoryAction.php deleted file mode 100644 index d133f4a..0000000 --- a/packages/workflow-engine-core/tests/Actions/ECommerce/ReserveInventoryAction.php +++ /dev/null @@ -1,42 +0,0 @@ -getData('order'); - - // Mock inventory reservation - $reservationId = 'res_'.uniqid(); - $context->setData('inventory.reservation_id', $reservationId); - $context->setData('inventory.reserved', true); - - return new ActionResult( - success: true, - data: ['reservation_id' => $reservationId, 'status' => 'reserved'] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order') && - $context->getData('order.valid') === true && - ($context->getData('fraud.risk') ?? 0) < 0.7; - } - - public function getName(): string - { - return 'Reserve Inventory'; - } - - public function getDescription(): string - { - return 'Reserves inventory items for the order'; - } -} diff --git a/packages/workflow-engine-core/tests/Actions/ECommerce/ValidateOrderAction.php b/packages/workflow-engine-core/tests/Actions/ECommerce/ValidateOrderAction.php deleted file mode 100644 index a3322f8..0000000 --- a/packages/workflow-engine-core/tests/Actions/ECommerce/ValidateOrderAction.php +++ /dev/null @@ -1,43 +0,0 @@ -getData('order'); - - // Mock validation logic - $isValid = isset($order['items']) && - count($order['items']) > 0 && - isset($order['total']) && - $order['total'] > 0; - - $context->setData('order.valid', $isValid); - - return new ActionResult( - success: $isValid, - data: ['validation_result' => $isValid ? 'passed' : 'failed'] - ); - } - - public function canExecute(WorkflowContext $context): bool - { - return $context->hasData('order'); - } - - public function getName(): string - { - return 'Validate Order'; - } - - public function getDescription(): string - { - return 'Validates order data including items and total amount'; - } -} diff --git a/packages/workflow-engine-core/tests/ArchTest.php b/packages/workflow-engine-core/tests/ArchTest.php deleted file mode 100644 index 87fb64c..0000000 --- a/packages/workflow-engine-core/tests/ArchTest.php +++ /dev/null @@ -1,5 +0,0 @@ -expect(['dd', 'dump', 'ray']) - ->each->not->toBeUsed(); diff --git a/packages/workflow-engine-core/tests/ExampleTest.php b/packages/workflow-engine-core/tests/ExampleTest.php deleted file mode 100644 index 5d36321..0000000 --- a/packages/workflow-engine-core/tests/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/packages/workflow-engine-core/tests/Integration/WorkflowIntegrationTest.php b/packages/workflow-engine-core/tests/Integration/WorkflowIntegrationTest.php deleted file mode 100644 index 2e79f93..0000000 --- a/packages/workflow-engine-core/tests/Integration/WorkflowIntegrationTest.php +++ /dev/null @@ -1,255 +0,0 @@ - 'User Onboarding Workflow', - 'version' => '1.0', - 'steps' => [ - [ - 'id' => 'welcome', - 'name' => 'Welcome User', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Welcome {{name}} to our platform!', - 'level' => 'info', - ], - ], - [ - 'id' => 'setup_profile', - 'name' => 'Setup User Profile', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Setting up profile for {{name}} with email {{email}}', - 'level' => 'info', - ], - ], - [ - 'id' => 'send_confirmation', - 'name' => 'Send Confirmation', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Sending confirmation email to {{email}}', - 'level' => 'info', - ], - ], - ], - 'transitions' => [ - [ - 'from' => 'welcome', - 'to' => 'setup_profile', - ], - [ - 'from' => 'setup_profile', - 'to' => 'send_confirmation', - ], - ], - ]; - - $context = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', - 'userId' => 123, - ]; - - // Start the workflow using helper function - $workflowId = start_workflow('user-onboarding-123', $definition, $context); - - // Verify workflow was created and completed - expect($workflowId)->not->toBeEmpty(); - expect($workflowId)->toBe('user-onboarding-123'); - - // Get workflow instance using helper - $instance = get_workflow($workflowId); - - // Verify the workflow completed successfully - expect($instance->getState())->toBe(WorkflowState::COMPLETED); - expect($instance->getName())->toBe('User Onboarding Workflow'); - - // Verify the context contains original data plus step outputs - $workflowData = $instance->getContext()->getData(); - expect($workflowData['name'])->toBe('John Doe'); - expect($workflowData['email'])->toBe('john@example.com'); - expect($workflowData['userId'])->toBe(123); - - // Verify that all steps completed successfully - expect($instance->getCompletedSteps())->toHaveCount(3); - expect($instance->getCompletedSteps())->toContain('welcome'); - expect($instance->getCompletedSteps())->toContain('setup_profile'); - expect($instance->getCompletedSteps())->toContain('send_confirmation'); - - // Verify no failed steps - expect($instance->getFailedSteps())->toBeEmpty(); - - // Check workflow status - $status = workflow()->getStatus($workflowId); - expect($status['state'])->toBe('completed'); - expect($status['name'])->toBe('User Onboarding Workflow'); - expect($status['progress'])->toBe(100.0); // 100% complete -}); - -test('it can handle workflow cancellation', function () { - $definition = [ - 'name' => 'Cancellable Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Starting process'], - ], - ], - ]; - - // Start workflow - $workflowId = start_workflow('cancellable-workflow', $definition); - - // Cancel workflow - cancel_workflow($workflowId, 'User requested cancellation'); - - // Verify cancellation - $instance = get_workflow($workflowId); - expect($instance->getState())->toBe(WorkflowState::CANCELLED); -}); - -test('it can list and filter workflows', function () { - $definition1 = [ - 'name' => 'Workflow 1', - 'steps' => [ - ['id' => 'step1', 'action' => 'log', 'parameters' => ['message' => 'Test']], - ], - ]; - - $definition2 = [ - 'name' => 'Workflow 2', - 'steps' => [ - ['id' => 'step1', 'action' => 'log', 'parameters' => ['message' => 'Test']], - ], - ]; - - // Start two workflows - $workflow1Id = start_workflow('list-test-1', $definition1); - $workflow2Id = start_workflow('list-test-2', $definition2); - - // Cancel one - cancel_workflow($workflow2Id); - - // List all workflows - $allWorkflows = workflow()->listWorkflows(); - expect(count($allWorkflows))->toBeGreaterThanOrEqual(2); - - // Filter by state - $completedWorkflows = workflow()->listWorkflows(['state' => WorkflowState::COMPLETED]); - $cancelledWorkflows = workflow()->listWorkflows(['state' => WorkflowState::CANCELLED]); - - expect(count($completedWorkflows))->toBeGreaterThanOrEqual(1); - expect(count($cancelledWorkflows))->toBeGreaterThanOrEqual(1); - - // Verify specific workflows exist in filtered results - $completedIds = array_column($completedWorkflows, 'workflow_id'); - $cancelledIds = array_column($cancelledWorkflows, 'workflow_id'); - - expect($completedIds)->toContain($workflow1Id); - expect($cancelledIds)->toContain($workflow2Id); -}); - -test('it can execute conditional workflows', function () { - $definition = [ - 'name' => 'Conditional Approval Workflow', - 'version' => '1.0', - 'steps' => [ - [ - 'id' => 'validate_request', - 'name' => 'Validate Request', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Validating request from {{user}}', - 'level' => 'info', - ], - ], - [ - 'id' => 'auto_approve', - 'name' => 'Auto Approve', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Auto-approving request for premium user {{user}}', - 'level' => 'info', - ], - ], - [ - 'id' => 'manual_review', - 'name' => 'Manual Review Required', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Manual review required for user {{user}}', - 'level' => 'warning', - ], - ], - [ - 'id' => 'notify_completion', - 'name' => 'Notify Completion', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Process completed for {{user}}', - 'level' => 'info', - ], - ], - ], - 'transitions' => [ - [ - 'from' => 'validate_request', - 'to' => 'auto_approve', - 'condition' => 'tier === premium', - ], - [ - 'from' => 'validate_request', - 'to' => 'manual_review', - 'condition' => 'tier !== premium', - ], - [ - 'from' => 'auto_approve', - 'to' => 'notify_completion', - ], - [ - 'from' => 'manual_review', - 'to' => 'notify_completion', - ], - ], - ]; - - // Test premium user path (should auto-approve) - $premiumContext = [ - 'user' => 'Alice Premium', - 'tier' => 'premium', - 'amount' => 1000, - ]; - - $premiumWorkflowId = start_workflow('premium-approval-123', $definition, $premiumContext); - $premiumInstance = get_workflow($premiumWorkflowId); - - // Verify premium workflow took auto-approval path - expect($premiumInstance->getState())->toBe(WorkflowState::COMPLETED); - expect($premiumInstance->getCompletedSteps())->toContain('validate_request'); - expect($premiumInstance->getCompletedSteps())->toContain('auto_approve'); - expect($premiumInstance->getCompletedSteps())->toContain('notify_completion'); - expect($premiumInstance->getCompletedSteps())->not->toContain('manual_review'); - - // Test regular user path (should require manual review) - $regularContext = [ - 'user' => 'Bob Regular', - 'tier' => 'basic', - 'amount' => 5000, - ]; - - $regularWorkflowId = start_workflow('regular-approval-456', $definition, $regularContext); - $regularInstance = get_workflow($regularWorkflowId); - - // Verify regular workflow took manual review path - expect($regularInstance->getState())->toBe(WorkflowState::COMPLETED); - expect($regularInstance->getCompletedSteps())->toContain('validate_request'); - expect($regularInstance->getCompletedSteps())->toContain('manual_review'); - expect($regularInstance->getCompletedSteps())->toContain('notify_completion'); - expect($regularInstance->getCompletedSteps())->not->toContain('auto_approve'); -}); diff --git a/packages/workflow-engine-core/tests/Pest.php b/packages/workflow-engine-core/tests/Pest.php deleted file mode 100644 index 8b80a2d..0000000 --- a/packages/workflow-engine-core/tests/Pest.php +++ /dev/null @@ -1,5 +0,0 @@ -in(__DIR__); diff --git a/packages/workflow-engine-core/tests/RealWorld/CICDPipelineWorkflowTest.php b/packages/workflow-engine-core/tests/RealWorld/CICDPipelineWorkflowTest.php deleted file mode 100644 index 93956f4..0000000 --- a/packages/workflow-engine-core/tests/RealWorld/CICDPipelineWorkflowTest.php +++ /dev/null @@ -1,427 +0,0 @@ -engine = app(WorkflowEngine::class); -}); - -test('cicd pipeline workflow - successful deployment flow', function () { - $definition = [ - 'name' => 'CI/CD Pipeline Workflow', - 'version' => '3.0', - 'steps' => [ - [ - 'id' => 'checkout_code', - 'name' => 'Code Checkout', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Checking out code from {{pipeline.repository}} branch {{pipeline.branch}}', - 'level' => 'info', - ], - ], - [ - 'id' => 'run_unit_tests', - 'name' => 'Unit Tests', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running unit tests for {{pipeline.project_name}}', - 'timeout' => '10m', - 'parallel_group' => 'tests', - ], - ], - [ - 'id' => 'security_scan', - 'name' => 'Security Scan', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running security vulnerability scan', - 'timeout' => '15m', - 'parallel_group' => 'tests', - ], - ], - [ - 'id' => 'run_integration_tests', - 'name' => 'Integration Tests', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running integration tests for {{pipeline.project_name}}', - 'timeout' => '20m', - ], - ], - [ - 'id' => 'build_artifacts', - 'name' => 'Build Artifacts', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Building deployment artifacts for {{pipeline.project_name}}', - 'timeout' => '30m', - ], - ], - [ - 'id' => 'deploy_staging', - 'name' => 'Deploy to Staging', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Deploying {{pipeline.project_name}} to staging environment', - 'compensation' => 'rollback_staging', - ], - ], - [ - 'id' => 'run_e2e_tests', - 'name' => 'End-to-End Tests', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running E2E tests on staging environment', - 'timeout' => '45m', - ], - ], - [ - 'id' => 'approval_gate', - 'name' => 'Production Approval Gate', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Awaiting production deployment approval for {{pipeline.project_name}}', - 'timeout' => '24h', - 'assigned_to' => 'release_manager', - ], - ], - [ - 'id' => 'deploy_production', - 'name' => 'Deploy to Production', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Deploying {{pipeline.project_name}} to production environment', - 'compensation' => 'rollback_production', - ], - ], - [ - 'id' => 'smoke_tests', - 'name' => 'Production Smoke Tests', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running smoke tests on production deployment', - 'timeout' => '5m', - ], - ], - ], - 'transitions' => [ - ['from' => 'checkout_code', 'to' => 'run_unit_tests'], - ['from' => 'checkout_code', 'to' => 'security_scan'], - ['from' => 'run_unit_tests', 'to' => 'run_integration_tests'], - ['from' => 'security_scan', 'to' => 'run_integration_tests'], - ['from' => 'run_integration_tests', 'to' => 'build_artifacts'], - ['from' => 'build_artifacts', 'to' => 'deploy_staging'], - ['from' => 'deploy_staging', 'to' => 'run_e2e_tests'], - ['from' => 'run_e2e_tests', 'to' => 'approval_gate'], - ['from' => 'approval_gate', 'to' => 'deploy_production'], - ['from' => 'deploy_production', 'to' => 'smoke_tests'], - ], - ]; - - $pipelineContext = [ - 'pipeline' => [ - 'id' => 'PIPE-001', - 'project_name' => 'workflow-engine-api', - 'repository' => 'https://github.com/company/workflow-engine-api', - 'branch' => 'main', - 'commit_sha' => 'abc123def456', - 'triggered_by' => 'developer@company.com', - 'environment' => 'production', - ], - ]; - - $workflowId = $this->engine->start('cicd-pipeline', $definition, $pipelineContext); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance)->not()->toBeNull(); - expect($instance->getState())->toBe(WorkflowState::COMPLETED); - expect($instance->getContext()->getData()['pipeline']['project_name'])->toBe('workflow-engine-api'); -}); - -test('cicd pipeline workflow - parallel testing stages', function () { - $definition = [ - 'name' => 'CI/CD Pipeline with Parallel Testing', - 'version' => '3.0', - 'steps' => [ - [ - 'id' => 'checkout_code', - 'name' => 'Source Code Checkout', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Checking out source code for parallel testing pipeline', - ], - ], - [ - 'id' => 'unit_tests_parallel', - 'name' => 'Unit Tests (Parallel)', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running unit tests in parallel - estimated 8 minutes', - 'test_suite' => 'unit', - 'parallel_workers' => 4, - ], - ], - [ - 'id' => 'security_scan_parallel', - 'name' => 'Security Scan (Parallel)', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running security vulnerability scan in parallel - estimated 12 minutes', - 'scan_type' => 'dependency_check', - 'tools' => ['snyk', 'owasp-dependency-check'], - ], - ], - [ - 'id' => 'code_quality_parallel', - 'name' => 'Code Quality (Parallel)', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running code quality analysis in parallel - estimated 5 minutes', - 'tools' => ['sonarqube', 'phpstan'], - 'quality_gates' => ['coverage', 'complexity', 'duplication'], - ], - ], - [ - 'id' => 'parallel_tests_complete', - 'name' => 'Parallel Tests Completion', - 'action' => 'log', - 'parameters' => [ - 'message' => 'All parallel test stages completed successfully', - 'join_condition' => 'all_tests_passed', - ], - ], - [ - 'id' => 'integration_tests', - 'name' => 'Integration Test Suite', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running comprehensive integration tests', - ], - ], - ], - 'parallel_groups' => [ - [ - 'name' => 'testing_phase', - 'steps' => ['unit_tests_parallel', 'security_scan_parallel', 'code_quality_parallel'], - 'join_type' => 'all_success', - ], - ], - 'transitions' => [ - ['from' => 'checkout_code', 'to' => 'unit_tests_parallel'], - ['from' => 'checkout_code', 'to' => 'security_scan_parallel'], - ['from' => 'checkout_code', 'to' => 'code_quality_parallel'], - ['from' => 'unit_tests_parallel', 'to' => 'parallel_tests_complete'], - ['from' => 'security_scan_parallel', 'to' => 'parallel_tests_complete'], - ['from' => 'code_quality_parallel', 'to' => 'parallel_tests_complete'], - ['from' => 'parallel_tests_complete', 'to' => 'integration_tests'], - ], - ]; - - $parallelPipelineContext = [ - 'pipeline' => [ - 'id' => 'PIPE-PARALLEL-001', - 'project_name' => 'microservice-api', - 'type' => 'parallel_testing', - 'test_configuration' => [ - 'parallel_workers' => 4, - 'test_timeout' => '15m', - 'quality_threshold' => 80, - ], - 'optimization' => 'parallel_execution', - ], - ]; - - $workflowId = $this->engine->start('parallel-cicd', $definition, $parallelPipelineContext); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance->getContext()->getData()['pipeline']['type'])->toBe('parallel_testing'); - expect($instance->getContext()->getData()['pipeline']['optimization'])->toBe('parallel_execution'); -}); - -test('cicd pipeline workflow - deployment failure and rollback', function () { - $definition = [ - 'name' => 'CI/CD Pipeline with Rollback', - 'version' => '3.0', - 'steps' => [ - [ - 'id' => 'build_and_test', - 'name' => 'Build and Test', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Building and testing application before deployment', - ], - ], - [ - 'id' => 'deploy_staging', - 'name' => 'Deploy to Staging', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Deploying to staging environment', - 'environment' => 'staging', - ], - ], - [ - 'id' => 'staging_tests', - 'name' => 'Staging Validation Tests', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running validation tests on staging deployment', - ], - ], - [ - 'id' => 'production_deployment', - 'name' => 'Production Deployment', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Deploying to production environment - simulating failure', - 'environment' => 'production', - 'failure_simulation' => true, - ], - ], - [ - 'id' => 'rollback_production', - 'name' => 'Production Rollback', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Rolling back production deployment due to failure', - 'rollback_type' => 'automatic', - 'target_version' => 'previous_stable', - ], - ], - [ - 'id' => 'incident_notification', - 'name' => 'Incident Notification', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Notifying teams about deployment failure and rollback', - 'notification_channels' => ['slack', 'pagerduty', 'email'], - ], - ], - ], - 'transitions' => [ - ['from' => 'build_and_test', 'to' => 'deploy_staging'], - ['from' => 'deploy_staging', 'to' => 'staging_tests'], - ['from' => 'staging_tests', 'to' => 'production_deployment'], - ['from' => 'production_deployment', 'to' => 'rollback_production', 'condition' => 'deployment.failed === true'], - ['from' => 'rollback_production', 'to' => 'incident_notification'], - ], - 'error_handling' => [ - 'strategy' => 'rollback_and_notify', - 'rollback_triggers' => ['deployment_failure', 'health_check_failure'], - 'notification_channels' => ['slack', 'pagerduty'], - ], - ]; - - $rollbackContext = [ - 'pipeline' => [ - 'id' => 'PIPE-ROLLBACK-001', - 'project_name' => 'critical-service', - 'deployment_strategy' => 'blue_green', - 'rollback_enabled' => true, - 'failure_scenario' => 'deployment_failure', - 'previous_version' => 'v2.1.0', - 'target_version' => 'v2.2.0', - ], - ]; - - $workflowId = $this->engine->start('rollback-pipeline', $definition, $rollbackContext); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance->getContext()->getData()['pipeline']['rollback_enabled'])->toBe(true); - expect($instance->getContext()->getData()['pipeline']['failure_scenario'])->toBe('deployment_failure'); -}); - -test('cicd pipeline workflow - feature branch deployment', function () { - $definition = [ - 'name' => 'Feature Branch CI/CD Pipeline', - 'version' => '3.0', - 'steps' => [ - [ - 'id' => 'feature_validation', - 'name' => 'Feature Branch Validation', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Validating feature branch {{pipeline.feature_branch}}', - ], - ], - [ - 'id' => 'run_tests', - 'name' => 'Feature Tests', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running comprehensive tests for feature {{pipeline.feature_name}}', - ], - ], - [ - 'id' => 'deploy_preview', - 'name' => 'Deploy Preview Environment', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Creating preview environment for feature {{pipeline.feature_name}}', - 'environment_type' => 'ephemeral', - 'auto_cleanup' => '7d', - ], - ], - [ - 'id' => 'qa_validation', - 'name' => 'QA Validation', - 'action' => 'log', - 'parameters' => [ - 'message' => 'QA team validating feature in preview environment', - 'assigned_to' => 'qa_team', - 'validation_checklist' => 'feature_requirements', - ], - ], - [ - 'id' => 'merge_approval', - 'name' => 'Merge Approval', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Requesting approval to merge feature {{pipeline.feature_name}} to main', - 'reviewers' => ['tech_lead', 'product_owner'], - ], - ], - ], - 'transitions' => [ - ['from' => 'feature_validation', 'to' => 'run_tests'], - ['from' => 'run_tests', 'to' => 'deploy_preview'], - ['from' => 'deploy_preview', 'to' => 'qa_validation'], - ['from' => 'qa_validation', 'to' => 'merge_approval'], - ], - 'cleanup_rules' => [ - 'preview_environment_ttl' => '7d', - 'auto_cleanup_on_merge' => true, - 'cleanup_on_branch_delete' => true, - ], - ]; - - $featureBranchContext = [ - 'pipeline' => [ - 'id' => 'PIPE-FEATURE-001', - 'type' => 'feature_branch', - 'feature_name' => 'advanced-search-functionality', - 'feature_branch' => 'feature/advanced-search', - 'base_branch' => 'develop', - 'developer' => 'developer@company.com', - 'jira_ticket' => 'PROJ-1234', - 'preview_url' => 'https://feature-advanced-search.preview.company.com', - ], - ]; - - $workflowId = $this->engine->start('feature-pipeline', $definition, $featureBranchContext); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance->getContext()->getData()['pipeline']['type'])->toBe('feature_branch'); - expect($instance->getContext()->getData()['pipeline']['feature_name'])->toBe('advanced-search-functionality'); - expect($instance->getContext()->getData()['pipeline']['jira_ticket'])->toBe('PROJ-1234'); -}); diff --git a/packages/workflow-engine-core/tests/RealWorld/DocumentApprovalWorkflowTest.php b/packages/workflow-engine-core/tests/RealWorld/DocumentApprovalWorkflowTest.php deleted file mode 100644 index 47df8e7..0000000 --- a/packages/workflow-engine-core/tests/RealWorld/DocumentApprovalWorkflowTest.php +++ /dev/null @@ -1,373 +0,0 @@ -engine = app(WorkflowEngine::class); -}); - -test('document approval workflow - standard approval flow', function () { - $definition = [ - 'name' => 'Document Approval Workflow', - 'version' => '1.5', - 'steps' => [ - [ - 'id' => 'submit_document', - 'name' => 'Submit Document', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Document {{document.title}} submitted for approval by {{document.author}}', - 'level' => 'info', - ], - ], - [ - 'id' => 'initial_review', - 'name' => 'Manager Review', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Initial review by manager for document {{document.title}}', - 'assigned_to' => 'manager_role', - 'timeout' => '2d', - ], - ], - [ - 'id' => 'legal_review', - 'name' => 'Legal Review', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Legal team reviewing contract document {{document.title}}', - 'assigned_to' => 'legal_team', - 'timeout' => '5d', - 'conditions' => 'document.type === "contract"', - ], - ], - [ - 'id' => 'compliance_review', - 'name' => 'Compliance Review', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Compliance review for high-value document {{document.title}}', - 'assigned_to' => 'compliance_team', - 'timeout' => '3d', - 'conditions' => 'document.value > 100000', - ], - ], - [ - 'id' => 'final_approval', - 'name' => 'Executive Approval', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Executive final approval for document {{document.title}}', - 'assigned_to' => 'executive_role', - 'timeout' => '1d', - ], - ], - [ - 'id' => 'archive_document', - 'name' => 'Archive Document', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Archiving approved document {{document.title}} to secure repository', - ], - ], - ], - 'transitions' => [ - ['from' => 'submit_document', 'to' => 'initial_review'], - ['from' => 'initial_review', 'to' => 'legal_review'], - ['from' => 'legal_review', 'to' => 'compliance_review'], - ['from' => 'compliance_review', 'to' => 'final_approval'], - ['from' => 'final_approval', 'to' => 'archive_document'], - ], - ]; - - $documentContext = [ - 'document' => [ - 'id' => 'DOC-001', - 'title' => 'Software License Agreement', - 'author' => 'legal@company.com', - 'type' => 'contract', - 'value' => 250000, - 'submitted_date' => '2024-01-15', - 'priority' => 'high', - ], - ]; - - $workflowId = $this->engine->start('document-approval', $definition, $documentContext); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance)->not()->toBeNull(); - expect($instance->getState())->toBe(WorkflowState::COMPLETED); - expect($instance->getContext()->getData()['document']['type'])->toBe('contract'); -}); - -test('document approval workflow - parallel review process', function () { - $definition = [ - 'name' => 'Parallel Document Approval', - 'version' => '1.5', - 'steps' => [ - [ - 'id' => 'submit_document', - 'name' => 'Document Submission', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Document submitted: {{document.title}} - initiating parallel reviews', - ], - ], - [ - 'id' => 'legal_review_parallel', - 'name' => 'Legal Review (Parallel)', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Legal team parallel review for {{document.title}}', - 'parallel_group' => 'reviews', - 'estimated_duration' => '3-5 days', - ], - ], - [ - 'id' => 'compliance_review_parallel', - 'name' => 'Compliance Review (Parallel)', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Compliance team parallel review for {{document.title}}', - 'parallel_group' => 'reviews', - 'estimated_duration' => '2-4 days', - ], - ], - [ - 'id' => 'technical_review_parallel', - 'name' => 'Technical Review (Parallel)', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Technical team parallel review for {{document.title}}', - 'parallel_group' => 'reviews', - 'estimated_duration' => '1-2 days', - ], - ], - [ - 'id' => 'consolidate_reviews', - 'name' => 'Consolidate Reviews', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Consolidating all parallel reviews for {{document.title}}', - 'join_condition' => 'all_reviews_complete', - ], - ], - [ - 'id' => 'final_decision', - 'name' => 'Final Decision', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Making final approval decision for {{document.title}}', - ], - ], - ], - 'parallel_groups' => [ - [ - 'name' => 'reviews', - 'steps' => ['legal_review_parallel', 'compliance_review_parallel', 'technical_review_parallel'], - 'join_type' => 'all_complete', - ], - ], - 'transitions' => [ - ['from' => 'submit_document', 'to' => 'legal_review_parallel'], - ['from' => 'submit_document', 'to' => 'compliance_review_parallel'], - ['from' => 'submit_document', 'to' => 'technical_review_parallel'], - ['from' => 'legal_review_parallel', 'to' => 'consolidate_reviews'], - ['from' => 'compliance_review_parallel', 'to' => 'consolidate_reviews'], - ['from' => 'technical_review_parallel', 'to' => 'consolidate_reviews'], - ['from' => 'consolidate_reviews', 'to' => 'final_decision'], - ], - ]; - - $complexDocumentContext = [ - 'document' => [ - 'id' => 'DOC-COMPLEX-001', - 'title' => 'Multi-Million Dollar Partnership Agreement', - 'author' => 'partnerships@company.com', - 'type' => 'partnership_agreement', - 'value' => 5000000, - 'complexity' => 'high', - 'requires_parallel_review' => true, - 'review_teams' => ['legal', 'compliance', 'technical'], - ], - ]; - - $workflowId = $this->engine->start('complex-document-approval', $definition, $complexDocumentContext); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance->getContext()->getData()['document']['requires_parallel_review'])->toBe(true); - expect($instance->getContext()->getData()['document']['value'])->toBe(5000000); -}); - -test('document approval workflow - rejection and resubmission flow', function () { - $definition = [ - 'name' => 'Document Approval with Rejection', - 'version' => '1.5', - 'steps' => [ - [ - 'id' => 'submit_document', - 'name' => 'Initial Submission', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Document {{document.title}} submitted for approval', - ], - ], - [ - 'id' => 'initial_review', - 'name' => 'Initial Review', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Reviewing document {{document.title}} for initial compliance', - ], - ], - [ - 'id' => 'rejection_notification', - 'name' => 'Rejection Notification', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Document {{document.title}} rejected - notifying author of required changes', - 'rejection_reasons' => 'compliance_issues', - 'action_required' => 'revision_needed', - ], - ], - [ - 'id' => 'revision_period', - 'name' => 'Revision Period', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Author has 7 days to revise and resubmit {{document.title}}', - 'timeout' => '7d', - 'auto_escalate' => true, - ], - ], - [ - 'id' => 'resubmission_review', - 'name' => 'Resubmission Review', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Reviewing resubmitted document {{document.title}}', - ], - ], - ], - 'transitions' => [ - ['from' => 'submit_document', 'to' => 'initial_review'], - ['from' => 'initial_review', 'to' => 'rejection_notification', 'condition' => 'review.approved === false'], - ['from' => 'rejection_notification', 'to' => 'revision_period'], - ['from' => 'revision_period', 'to' => 'resubmission_review', 'condition' => 'document.resubmitted === true'], - ], - 'error_handling' => [ - 'rejection_workflow' => 'revision_and_resubmit', - 'max_revisions' => 3, - 'escalation_after_rejections' => 2, - ], - ]; - - $rejectedDocumentContext = [ - 'document' => [ - 'id' => 'DOC-REVISION-001', - 'title' => 'Non-Compliant Service Agreement', - 'author' => 'sales@company.com', - 'type' => 'service_agreement', - 'value' => 50000, - 'initial_submission' => true, - 'compliance_issues' => [ - 'missing_liability_clauses', - 'incorrect_termination_terms', - 'data_protection_gaps', - ], - ], - ]; - - $workflowId = $this->engine->start('rejection-flow', $definition, $rejectedDocumentContext); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance->getContext()->getData()['document']['initial_submission'])->toBe(true); - expect(count($instance->getContext()->getData()['document']['compliance_issues']))->toBe(3); -}); - -test('document approval workflow - escalation and timeout handling', function () { - $definition = [ - 'name' => 'Document Approval with Escalation', - 'version' => '1.5', - 'steps' => [ - [ - 'id' => 'submit_document', - 'name' => 'Document Submission', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Critical document {{document.title}} submitted with escalation rules', - ], - ], - [ - 'id' => 'manager_review', - 'name' => 'Manager Review (Timed)', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Manager reviewing {{document.title}} - must complete within 24 hours', - 'timeout' => '24h', - 'escalation_target' => 'director_level', - ], - ], - [ - 'id' => 'director_escalation', - 'name' => 'Director Escalation', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Document {{document.title}} escalated to director due to timeout', - 'escalation_reason' => 'manager_timeout', - 'priority' => 'urgent', - ], - ], - [ - 'id' => 'executive_override', - 'name' => 'Executive Override', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Executive override for time-critical document {{document.title}}', - 'override_reason' => 'business_critical', - 'expedited_approval' => true, - ], - ], - ], - 'transitions' => [ - ['from' => 'submit_document', 'to' => 'manager_review'], - ['from' => 'manager_review', 'to' => 'director_escalation', 'condition' => 'timeout_occurred'], - ['from' => 'director_escalation', 'to' => 'executive_override', 'condition' => 'escalation_required'], - ], - 'escalation_rules' => [ - 'timeout_escalation' => true, - 'escalation_levels' => ['manager', 'director', 'executive'], - 'notification_channels' => ['email', 'slack', 'sms'], - ], - ]; - - $urgentDocumentContext = [ - 'document' => [ - 'id' => 'DOC-URGENT-001', - 'title' => 'Emergency Vendor Contract', - 'author' => 'procurement@company.com', - 'type' => 'emergency_contract', - 'value' => 2000000, - 'priority' => 'critical', - 'business_impact' => 'production_blocker', - 'deadline' => '2024-01-20T23:59:59Z', - 'escalation_enabled' => true, - ], - ]; - - $workflowId = $this->engine->start('escalation-flow', $definition, $urgentDocumentContext); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance->getContext()->getData()['document']['priority'])->toBe('critical'); - expect($instance->getContext()->getData()['document']['escalation_enabled'])->toBe(true); - expect($instance->getContext()->getData()['document']['business_impact'])->toBe('production_blocker'); -}); diff --git a/packages/workflow-engine-core/tests/RealWorld/ECommerceWorkflowTest.php b/packages/workflow-engine-core/tests/RealWorld/ECommerceWorkflowTest.php deleted file mode 100644 index 64becb6..0000000 --- a/packages/workflow-engine-core/tests/RealWorld/ECommerceWorkflowTest.php +++ /dev/null @@ -1,328 +0,0 @@ -engine = app(WorkflowEngine::class); -}); - -test('e-commerce order processing workflow - successful order flow', function () { - // Create workflow definition based on ARCHITECTURE.md example - $definition = [ - 'name' => 'E-Commerce Order Processing', - 'version' => '2.0', - 'steps' => [ - [ - 'id' => 'validate_order', - 'name' => 'Validate Order', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Validating order {{order.id}} with total {{order.total}}', - 'level' => 'info', - ], - ], - [ - 'id' => 'check_fraud', - 'name' => 'Fraud Check', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running fraud check for order {{order.id}}', - 'level' => 'info', - ], - ], - [ - 'id' => 'reserve_inventory', - 'name' => 'Reserve Inventory', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Reserving inventory for order {{order.id}}', - 'level' => 'info', - ], - ], - [ - 'id' => 'process_payment', - 'name' => 'Process Payment', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Processing payment for order {{order.id}} amount {{order.total}}', - 'level' => 'info', - ], - ], - [ - 'id' => 'create_shipment', - 'name' => 'Create Shipment', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Creating shipment for order {{order.id}}', - 'level' => 'info', - ], - ], - [ - 'id' => 'send_notification', - 'name' => 'Send Order Confirmation', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Sending order confirmation for {{order.id}} to {{order.customer_email}}', - 'level' => 'info', - ], - ], - ], - 'transitions' => [ - ['from' => 'validate_order', 'to' => 'check_fraud'], - ['from' => 'check_fraud', 'to' => 'reserve_inventory'], - ['from' => 'reserve_inventory', 'to' => 'process_payment'], - ['from' => 'process_payment', 'to' => 'create_shipment'], - ['from' => 'create_shipment', 'to' => 'send_notification'], - ], - ]; - - // Valid order data - $orderContext = [ - 'order' => [ - 'id' => 'ORD-12345', - 'customer_email' => 'customer@example.com', - 'items' => [ - ['sku' => 'ITEM-001', 'quantity' => 2, 'price' => 50.00], - ['sku' => 'ITEM-002', 'quantity' => 1, 'price' => 100.00], - ], - 'total' => 200.00, - 'currency' => 'USD', - ], - ]; - - // Start workflow - $workflowId = $this->engine->start('ecommerce-order', $definition, $orderContext); - - expect($workflowId)->not()->toBeEmpty(); - - // Get workflow instance to check state - $instance = $this->engine->getInstance($workflowId); - expect($instance)->not()->toBeNull(); - expect($instance->getState())->toBe(WorkflowState::COMPLETED); -}); - -test('e-commerce order processing workflow - workflow definition structure', function () { - $definition = [ - 'name' => 'E-Commerce Order Processing', - 'version' => '2.0', - 'steps' => [ - [ - 'id' => 'validate_order', - 'name' => 'Validate Order', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Order validation: checking items, total, and customer data', - 'conditions' => [ - 'order.items.count > 0', - 'order.total > 0', - 'order.customer_email is set', - ], - ], - ], - [ - 'id' => 'fraud_check', - 'name' => 'Fraud Detection', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Fraud check: analyzing order patterns and risk factors', - 'timeout' => '2m', - 'risk_threshold' => 0.7, - ], - ], - [ - 'id' => 'inventory_reservation', - 'name' => 'Inventory Management', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Inventory: reserving items for order fulfillment', - 'compensation' => 'release_inventory_action', - ], - ], - [ - 'id' => 'payment_processing', - 'name' => 'Payment Gateway', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Payment: processing order payment through secure gateway', - 'retry_attempts' => 3, - 'retry_delay' => '30s', - 'compensation' => 'refund_payment_action', - ], - ], - [ - 'id' => 'shipment_creation', - 'name' => 'Shipping Coordination', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Shipping: creating shipment and generating tracking info', - ], - ], - [ - 'id' => 'customer_notification', - 'name' => 'Customer Communication', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Notification: sending order confirmation and tracking details', - 'async' => true, - 'channels' => ['email', 'sms'], - ], - ], - ], - 'transitions' => [ - ['from' => 'validate_order', 'to' => 'fraud_check', 'condition' => 'order.valid === true'], - ['from' => 'fraud_check', 'to' => 'inventory_reservation', 'condition' => 'fraud.risk < 0.7'], - ['from' => 'inventory_reservation', 'to' => 'payment_processing'], - ['from' => 'payment_processing', 'to' => 'shipment_creation', 'condition' => 'payment.success === true'], - ['from' => 'shipment_creation', 'to' => 'customer_notification'], - ], - 'error_handling' => [ - 'on_failure' => 'compensate_and_notify', - 'notification_channels' => ['email', 'slack', 'webhook'], - ], - ]; - - $context = [ - 'order' => [ - 'id' => 'ORD-COMPLEX-001', - 'customer_email' => 'test@ecommerce.com', - 'total' => 1500.00, - 'items' => [ - ['sku' => 'PREMIUM-ITEM', 'quantity' => 1, 'price' => 1500.00], - ], - ], - ]; - - $workflowId = $this->engine->start('ecommerce-complex', $definition, $context); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance)->not()->toBeNull(); - expect($instance->getContext()->getData()['order']['id'])->toBe('ORD-COMPLEX-001'); -}); - -test('e-commerce order processing workflow - high value order scenarios', function () { - $definition = [ - 'name' => 'High Value Order Processing', - 'version' => '2.0', - 'steps' => [ - [ - 'id' => 'order_validation', - 'name' => 'Enhanced Order Validation', - 'action' => 'log', - 'parameters' => [ - 'message' => 'High-value order validation with enhanced security checks', - 'security_level' => 'enhanced', - ], - ], - [ - 'id' => 'fraud_analysis', - 'name' => 'Advanced Fraud Analysis', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Running advanced fraud detection for high-value transaction', - 'analysis_level' => 'advanced', - 'manual_review_threshold' => 50000, - ], - ], - [ - 'id' => 'executive_approval', - 'name' => 'Executive Approval Gate', - 'action' => 'log', - 'parameters' => [ - 'message' => 'High-value order requiring executive approval', - 'approval_required' => true, - 'timeout' => '24h', - ], - ], - ], - 'transitions' => [ - ['from' => 'order_validation', 'to' => 'fraud_analysis'], - ['from' => 'fraud_analysis', 'to' => 'executive_approval', 'condition' => 'order.total > 50000'], - ], - ]; - - $highValueContext = [ - 'order' => [ - 'id' => 'ORD-HIGH-VALUE-001', - 'customer_email' => 'enterprise@bigcompany.com', - 'total' => 75000.00, - 'items' => [ - ['sku' => 'ENTERPRISE-LICENSE', 'quantity' => 1, 'price' => 75000.00], - ], - 'approval_level' => 'executive', - ], - ]; - - $workflowId = $this->engine->start('high-value-order', $definition, $highValueContext); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance->getContext()->getData()['order']['total'])->toBe(75000); -}); - -test('e-commerce order processing workflow - error handling scenarios', function () { - $definition = [ - 'name' => 'Order Processing with Error Handling', - 'version' => '2.0', - 'steps' => [ - [ - 'id' => 'validate_order', - 'name' => 'Order Validation', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Validating order - simulating validation failure scenario', - ], - ], - [ - 'id' => 'error_notification', - 'name' => 'Error Notification', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Sending error notification to customer and operations team', - ], - ], - [ - 'id' => 'cleanup_resources', - 'name' => 'Resource Cleanup', - 'action' => 'log', - 'parameters' => [ - 'message' => 'Cleaning up any allocated resources due to order failure', - ], - ], - ], - 'transitions' => [ - ['from' => 'validate_order', 'to' => 'error_notification'], - ['from' => 'error_notification', 'to' => 'cleanup_resources'], - ], - 'error_handling' => [ - 'strategy' => 'compensate_and_retry', - 'max_retries' => 3, - 'retry_delay' => '5m', - 'compensation_actions' => [ - 'release_inventory', - 'cancel_payment_authorization', - 'notify_customer', - ], - ], - ]; - - $failedOrderContext = [ - 'order' => [ - 'id' => 'ORD-FAILED-001', - 'customer_email' => 'test@failed-order.com', - 'total' => 0, // Invalid total to trigger failure - 'items' => [], - 'error_scenario' => true, - ], - ]; - - $workflowId = $this->engine->start('failed-order', $definition, $failedOrderContext); - - expect($workflowId)->not()->toBeEmpty(); - - $instance = $this->engine->getInstance($workflowId); - expect($instance->getContext()->getData()['order']['error_scenario'])->toBe(true); -}); diff --git a/packages/workflow-engine-core/tests/Support/InMemoryStorage.php b/packages/workflow-engine-core/tests/Support/InMemoryStorage.php deleted file mode 100644 index 9dc3cbf..0000000 --- a/packages/workflow-engine-core/tests/Support/InMemoryStorage.php +++ /dev/null @@ -1,65 +0,0 @@ -instances[$instance->getId()] = $instance; - } - - public function load(string $id): WorkflowInstance - { - if (! isset($this->instances[$id])) { - throw new \InvalidArgumentException("Workflow instance not found: {$id}"); - } - - return $this->instances[$id]; - } - - public function findInstances(array $criteria = []): array - { - if (empty($criteria)) { - return array_values($this->instances); - } - - return array_filter($this->instances, function ($instance) use ($criteria) { - foreach ($criteria as $key => $value) { - // Simple implementation for basic filtering - if ($key === 'state' && $instance->getState()->value !== $value) { - return false; - } - } - - return true; - }); - } - - public function delete(string $id): void - { - unset($this->instances[$id]); - } - - public function exists(string $id): bool - { - return isset($this->instances[$id]); - } - - public function updateState(string $id, array $updates): void - { - if (! isset($this->instances[$id])) { - throw new \InvalidArgumentException("Workflow instance not found: {$id}"); - } - - // Simple update implementation - // In a real implementation, this would update specific fields - $instance = $this->instances[$id]; - $this->instances[$id] = $instance; - } -} diff --git a/packages/workflow-engine-core/tests/TestCase.php b/packages/workflow-engine-core/tests/TestCase.php deleted file mode 100644 index 6583592..0000000 --- a/packages/workflow-engine-core/tests/TestCase.php +++ /dev/null @@ -1,60 +0,0 @@ - 'SolutionForest\\WorkflowMastery\\Database\\Factories\\'.class_basename($modelName).'Factory' - ); - - $this->setUpDatabase(); - } - - protected function getPackageProviders($app): array - { - return [ - LaravelWorkflowEngineServiceProvider::class, - ]; - } - - public function getEnvironmentSetUp($app): void - { - config()->set('database.default', 'testing'); - config()->set('database.connections.testing', [ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'prefix' => '', - ]); - } - - protected function setUpDatabase(): void - { - Schema::create('workflow_instances', function (Blueprint $table) { - $table->string('id')->primary(); - $table->string('definition_name'); - $table->string('definition_version'); - $table->json('definition_data'); - $table->string('state'); - $table->json('data'); - $table->string('current_step_id')->nullable(); - $table->json('completed_steps'); - $table->json('failed_steps'); - $table->text('error_message')->nullable(); - $table->timestamps(); - - $table->index('state'); - $table->index('definition_name'); - }); - } -} diff --git a/packages/workflow-engine-core/tests/Unit/ActionTest.php b/packages/workflow-engine-core/tests/Unit/ActionTest.php deleted file mode 100644 index ee6ae9a..0000000 --- a/packages/workflow-engine-core/tests/Unit/ActionTest.php +++ /dev/null @@ -1,52 +0,0 @@ - 'Hello {{name}}']); - $context = new WorkflowContext('test-workflow', 'test-step', ['name' => 'John']); - - $result = $action->execute($context); - - expect($result)->toBeInstanceOf(ActionResult::class); - expect($result->isSuccess())->toBeTrue(); - expect($result->getErrorMessage())->toBeNull(); -}); - -test('delay action can execute', function () { - $action = new DelayAction(['seconds' => 1]); - $context = new WorkflowContext('test-workflow', 'test-step'); - - $start = microtime(true); - $result = $action->execute($context); - $end = microtime(true); - - expect($result)->toBeInstanceOf(ActionResult::class); - expect($result->isSuccess())->toBeTrue(); - expect($result->getErrorMessage())->toBeNull(); - - // Check that at least 1 second passed (with some tolerance) - expect($end - $start)->toBeGreaterThanOrEqual(0.9); -}); - -test('log action handles invalid template', function () { - $action = new LogAction(['message' => 'Hello {{invalid_variable}}']); - $context = new WorkflowContext('test-workflow', 'test-step', ['name' => 'John']); - - $result = $action->execute($context); - - expect($result->isSuccess())->toBeTrue(); -}); - -test('delay action handles invalid seconds', function () { - $action = new DelayAction(['seconds' => 'invalid']); - $context = new WorkflowContext('test-workflow', 'test-step'); - - $result = $action->execute($context); - - expect($result->isSuccess())->toBeFalse(); - expect($result->getErrorMessage())->toContain('Invalid delay seconds'); -}); diff --git a/packages/workflow-engine-core/tests/Unit/HelpersTest.php b/packages/workflow-engine-core/tests/Unit/HelpersTest.php deleted file mode 100644 index 0891254..0000000 --- a/packages/workflow-engine-core/tests/Unit/HelpersTest.php +++ /dev/null @@ -1,68 +0,0 @@ -toBeInstanceOf(WorkflowEngine::class); -}); - -test('start workflow helper works', function () { - $definition = [ - 'name' => 'Helper Test Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Hello from helper'], - ], - ], - ]; - - $workflowId = start_workflow('helper-test', $definition); - - expect($workflowId)->not->toBeEmpty(); - expect($workflowId)->toBe('helper-test'); -}); - -test('get workflow helper works', function () { - $definition = [ - 'name' => 'Helper Test Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Hello from helper'], - ], - ], - ]; - - $workflowId = start_workflow('helper-test-get', $definition); - $instance = get_workflow($workflowId); - - expect($instance->getId())->toBe($workflowId); - expect($instance->getName())->toBe('Helper Test Workflow'); -}); - -test('cancel workflow helper works', function () { - $definition = [ - 'name' => 'Helper Test Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Hello from helper'], - ], - ], - ]; - - $workflowId = start_workflow('helper-test-cancel', $definition); - cancel_workflow($workflowId, 'Test cancellation'); - - $instance = get_workflow($workflowId); - expect($instance->getState()->value)->toBe('cancelled'); -}); diff --git a/packages/workflow-engine-core/tests/Unit/PHP83FeaturesTest.php b/packages/workflow-engine-core/tests/Unit/PHP83FeaturesTest.php deleted file mode 100644 index c6749bc..0000000 --- a/packages/workflow-engine-core/tests/Unit/PHP83FeaturesTest.php +++ /dev/null @@ -1,158 +0,0 @@ -color())->toBe('blue'); - expect($state->icon())->toBe('▶️'); - expect($state->label())->toBe('Running'); - expect($state->canTransitionTo(WorkflowState::COMPLETED))->toBeTrue(); - expect($state->canTransitionTo(WorkflowState::PENDING))->toBeFalse(); - }); - - it('can create workflow with fluent builder API', function () { - $workflow = WorkflowBuilder::create('test-workflow') - ->description('Test workflow with fluent API') - ->version('2.0') - ->startWith(LogAction::class, ['message' => 'Starting workflow']) - ->then(DelayAction::class, ['seconds' => 1]) - ->email( - template: 'test', - to: '{{ user.email }}', - subject: 'Test Email' - ) - ->withMetadata(['created_by' => 'test']) - ->build(); - - expect($workflow->getName())->toBe('test-workflow'); - expect($workflow->getVersion())->toBe('2.0'); - expect($workflow->getMetadata())->toHaveKey('description'); - expect($workflow->getMetadata())->toHaveKey('created_by'); - expect($workflow->getSteps())->toHaveCount(3); - }); - - it('can use conditional workflow building', function () { - $workflow = WorkflowBuilder::create('conditional-test') - ->startWith(LogAction::class, ['message' => 'Start']) - ->when('user.premium', function ($builder) { - $builder->then(LogAction::class, ['message' => 'Premium user step']); - }) - ->then(LogAction::class, ['message' => 'Final step']) - ->build(); - - expect($workflow->getSteps())->toHaveCount(3); - }); - - it('can create quick template workflows', function () { - $builder = WorkflowBuilder::quick()->userOnboarding(); - $workflow = $builder->build(); - - expect($workflow->getName())->toBe('user-onboarding'); - expect($workflow->getSteps())->not()->toBeEmpty(); - }); - - it('validates state transitions correctly', function () { - // Test valid transitions - expect(WorkflowState::PENDING->canTransitionTo(WorkflowState::RUNNING))->toBeTrue(); - expect(WorkflowState::RUNNING->canTransitionTo(WorkflowState::COMPLETED))->toBeTrue(); - expect(WorkflowState::RUNNING->canTransitionTo(WorkflowState::FAILED))->toBeTrue(); - - // Test invalid transitions - expect(WorkflowState::COMPLETED->canTransitionTo(WorkflowState::RUNNING))->toBeFalse(); - expect(WorkflowState::FAILED->canTransitionTo(WorkflowState::RUNNING))->toBeFalse(); - expect(WorkflowState::CANCELLED->canTransitionTo(WorkflowState::RUNNING))->toBeFalse(); - }); - - it('provides UI-friendly state information', function () { - $testCases = [ - [WorkflowState::PENDING, 'gray', '⏳', 'Pending'], - [WorkflowState::RUNNING, 'blue', '▶️', 'Running'], - [WorkflowState::COMPLETED, 'green', '✅', 'Completed'], - [WorkflowState::FAILED, 'red', '❌', 'Failed'], - ]; - - foreach ($testCases as [$state, $expectedColor, $expectedIcon, $expectedLabel]) { - expect($state->color())->toBe($expectedColor); - expect($state->icon())->toBe($expectedIcon); - expect($state->label())->toBe($expectedLabel); - } - }); - -}); - -describe('Simplified Learning Curve', function () { - - it('can create workflow with common patterns using helper methods', function () { - $workflow = WorkflowBuilder::create('helper-test') - ->email( - template: 'welcome', - to: 'user@example.com', - subject: 'Welcome!' - ) - ->delay(minutes: 5) - ->http( - url: 'https://api.example.com/webhook', - method: 'POST', - data: ['event' => 'user_registered'] - ) - ->condition('user.verified') - ->build(); - - $steps = array_values($workflow->getSteps()); // Convert to numeric array - expect($steps)->toHaveCount(4); - - // Check email step - expect($steps[0]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\EmailAction'); - expect($steps[0]->getConfig()['template'])->toBe('welcome'); - - // Check delay step - expect($steps[1]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\DelayAction'); - - // Check HTTP step - expect($steps[2]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\HttpAction'); - expect($steps[2]->getConfig()['method'])->toBe('POST'); - - // Check condition step - expect($steps[3]->getActionClass())->toBe('SolutionForest\\WorkflowMastery\\Actions\\ConditionAction'); - expect($steps[3]->getConfig()['condition'])->toBe('user.verified'); - }); - - it('provides quick workflow templates', function () { - $templates = ['userOnboarding', 'orderProcessing', 'documentApproval']; - - foreach ($templates as $template) { - $workflow = WorkflowBuilder::quick()->$template()->build(); - expect($workflow->getSteps())->not()->toBeEmpty(); - expect($workflow->getMetadata()['description'])->not()->toBeEmpty(); - } - }); - - it('can use named arguments for better readability', function () { - // This test demonstrates the improved API readability - // The actual testing is implicit in the successful execution - $workflow = WorkflowBuilder::create(name: 'named-args-test') - ->description(description: 'Testing named arguments') - ->version(version: '1.0') - ->email( - template: 'test', - to: 'test@example.com', - subject: 'Test Subject', - data: ['key' => 'value'] - ) - ->delay( - minutes: 5 - ) - ->build(); - - expect($workflow->getName())->toBe('named-args-test'); - expect($workflow->getVersion())->toBe('1.0'); - }); - -}); diff --git a/packages/workflow-engine-core/tests/Unit/WorkflowEngineTest.php b/packages/workflow-engine-core/tests/Unit/WorkflowEngineTest.php deleted file mode 100644 index b8fc9bb..0000000 --- a/packages/workflow-engine-core/tests/Unit/WorkflowEngineTest.php +++ /dev/null @@ -1,207 +0,0 @@ -engine = app(WorkflowEngine::class); -}); - -test('it can start a workflow', function () { - $definition = [ - 'name' => 'Test Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Hello World'], - ], - ], - ]; - - $workflowId = $this->engine->start('test-workflow', $definition); - - expect($workflowId)->not->toBeEmpty(); - - // Verify the workflow instance was created - $instance = $this->engine->getWorkflow($workflowId); - expect($instance)->toBeInstanceOf(WorkflowInstance::class); - expect($instance->getState())->toBe(WorkflowState::COMPLETED); // Log action completes immediately - expect($instance->getName())->toBe('Test Workflow'); -}); - -test('it can start a workflow with context', function () { - Event::fake(); - - $definition = [ - 'name' => 'Test Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Hello {{name}}'], - ], - ], - ]; - - $context = ['name' => 'John']; - $workflowId = $this->engine->start('test-workflow', $definition, $context); - - $instance = $this->engine->getWorkflow($workflowId); - $workflowData = $instance->getContext()->getData(); - - // Should contain original context plus any data added by actions - expect($workflowData['name'])->toBe('John'); - expect($workflowData)->toHaveKey('logged_message'); // Added by LogAction - expect($workflowData)->toHaveKey('logged_at'); // Added by LogAction -}); - -test('it can resume a paused workflow', function () { - Event::fake(); - - // Create a workflow with multiple steps - $definition = [ - 'name' => 'Test Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Hello World'], - ], - [ - 'id' => 'step2', - 'name' => 'Second Step', - 'action' => 'log', - 'parameters' => ['message' => 'Second step'], - ], - ], - ]; - - $workflowId = $this->engine->start('test-workflow', $definition); - - // Manually pause it - $storage = app(StorageAdapter::class); - $instance = $storage->load($workflowId); - $instance->setState(WorkflowState::PAUSED); - $storage->save($instance); - - // Resume it - $this->engine->resume($workflowId); - - $instance = $this->engine->getWorkflow($workflowId); - // After resume, it should be completed since we have simple log actions - expect($instance->getState())->toBe(WorkflowState::COMPLETED); -}); - -test('it can cancel a workflow', function () { - $definition = [ - 'name' => 'Test Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Hello World'], - ], - ], - ]; - - $workflowId = $this->engine->start('test-workflow', $definition); - $this->engine->cancel($workflowId, 'User cancelled'); - - $instance = $this->engine->getWorkflow($workflowId); - expect($instance->getState())->toBe(WorkflowState::CANCELLED); -}); - -test('it can get workflow status', function () { - $definition = [ - 'name' => 'Test Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Hello World'], - ], - ], - ]; - - $workflowId = $this->engine->start('test-workflow', $definition); - $status = $this->engine->getStatus($workflowId); - - expect($status)->toBeArray(); - expect($status['state'])->toBe(WorkflowState::COMPLETED->value); - expect($status['name'])->toBe('Test Workflow'); - expect($status)->toHaveKey('current_step'); - expect($status)->toHaveKey('progress'); -}); - -test('it throws exception for invalid workflow definition', function () { - $invalidDefinition = [ - 'steps' => [], - ]; - - $this->engine->start('test-workflow', $invalidDefinition); -})->throws(InvalidWorkflowDefinitionException::class, 'Required field \'name\' is missing from workflow definition'); - -test('it throws exception for nonexistent workflow', function () { - $this->engine->getWorkflow('nonexistent'); -})->throws(WorkflowInstanceNotFoundException::class, 'Workflow instance \'nonexistent\' was not found'); - -test('it can list workflows', function () { - $definition = [ - 'name' => 'Test Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Hello World'], - ], - ], - ]; - - $workflowId1 = $this->engine->start('test-workflow-1', $definition); - $workflowId2 = $this->engine->start('test-workflow-2', $definition); - - $workflows = $this->engine->listWorkflows(); - - expect($workflows)->toHaveCount(2); - expect(array_column($workflows, 'workflow_id'))->toContain($workflowId1); - expect(array_column($workflows, 'workflow_id'))->toContain($workflowId2); -}); - -test('it can filter workflows by state', function () { - $definition = [ - 'name' => 'Test Workflow', - 'steps' => [ - [ - 'id' => 'step1', - 'name' => 'First Step', - 'action' => 'log', - 'parameters' => ['message' => 'Hello World'], - ], - ], - ]; - - $completedId = $this->engine->start('completed-workflow', $definition); - $cancelledId = $this->engine->start('cancelled-workflow', $definition); - - $this->engine->cancel($cancelledId); - - $completedWorkflows = $this->engine->listWorkflows(['state' => WorkflowState::COMPLETED]); - $cancelledWorkflows = $this->engine->listWorkflows(['state' => WorkflowState::CANCELLED]); - - expect($completedWorkflows)->toHaveCount(1); - expect($cancelledWorkflows)->toHaveCount(1); - expect($completedWorkflows[0]['workflow_id'])->toBe($completedId); - expect($cancelledWorkflows[0]['workflow_id'])->toBe($cancelledId); -}); diff --git a/src/Events/StepCompletedEvent.php b/src/Events/StepCompletedEvent.php deleted file mode 100644 index 5c59dfe..0000000 --- a/src/Events/StepCompletedEvent.php +++ /dev/null @@ -1,23 +0,0 @@ -instance = $instance; - $this->step = $step; - } -} diff --git a/src/Events/StepFailedEvent.php b/src/Events/StepFailedEvent.php deleted file mode 100644 index 7586ed4..0000000 --- a/src/Events/StepFailedEvent.php +++ /dev/null @@ -1,26 +0,0 @@ -instance = $instance; - $this->step = $step; - $this->exception = $exception; - } -} diff --git a/src/Events/WorkflowCancelled.php b/src/Events/WorkflowCancelled.php deleted file mode 100644 index 613e7ce..0000000 --- a/src/Events/WorkflowCancelled.php +++ /dev/null @@ -1,17 +0,0 @@ -instance = $instance; - } -} diff --git a/src/Events/WorkflowFailed.php b/src/Events/WorkflowFailed.php deleted file mode 100644 index d8d3317..0000000 --- a/src/Events/WorkflowFailed.php +++ /dev/null @@ -1,18 +0,0 @@ -instance = $instance; - $this->exception = $exception; - } -} diff --git a/src/Events/WorkflowStarted.php b/src/Events/WorkflowStarted.php deleted file mode 100644 index e9745cb..0000000 --- a/src/Events/WorkflowStarted.php +++ /dev/null @@ -1,17 +0,0 @@ -instance = $instance; - } -} From fa6a3c0814eb8b60e06d5e7452ef36ac116004a3 Mon Sep 17 00:00:00 2001 From: alan Date: Thu, 29 May 2025 19:11:17 +0800 Subject: [PATCH 5/6] wip: pint --- src/Adapters/LaravelEventDispatcher.php | 2 +- src/Adapters/LaravelLogger.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapters/LaravelEventDispatcher.php b/src/Adapters/LaravelEventDispatcher.php index fd8e563..4d582cb 100644 --- a/src/Adapters/LaravelEventDispatcher.php +++ b/src/Adapters/LaravelEventDispatcher.php @@ -9,7 +9,7 @@ /** * Laravel adapter for the workflow engine event dispatcher. - * + * * This adapter bridges Laravel's event system with the framework-agnostic * event dispatcher interface used by the workflow engine core. */ diff --git a/src/Adapters/LaravelLogger.php b/src/Adapters/LaravelLogger.php index 396bc68..3e4a4fe 100644 --- a/src/Adapters/LaravelLogger.php +++ b/src/Adapters/LaravelLogger.php @@ -9,7 +9,7 @@ /** * Laravel adapter for the workflow engine logger. - * + * * This adapter bridges Laravel's logging system with the framework-agnostic * logger interface used by the workflow engine core. */ From 05741d957227a0fdb75f09ac55c5a59b538657c1 Mon Sep 17 00:00:00 2001 From: alan Date: Thu, 29 May 2025 19:14:06 +0800 Subject: [PATCH 6/6] wip: fix error --- src/Providers/WorkflowEngineServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Providers/WorkflowEngineServiceProvider.php b/src/Providers/WorkflowEngineServiceProvider.php index d59bdcb..79d2a7f 100644 --- a/src/Providers/WorkflowEngineServiceProvider.php +++ b/src/Providers/WorkflowEngineServiceProvider.php @@ -21,7 +21,7 @@ public function configurePackage(Package $package): void */ $package ->name('workflow-engine') - ->hasConfigFile('workflow-engine') + ->hasConfigFile() ->hasViews() ->hasMigration('create_workflow_instances_table') ->hasCommand(LaravelWorkflowEngineCommand::class);