Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ jobs:
fail-fast: true
matrix:
php: [8.4, 8.3]
laravel: [12.*, 11.*]
laravel: [13.*, 12.*]
stability: [prefer-stable]
include:
# Keep one lowest-deps lane to validate minimum compatibility.
- php: 8.3
laravel: 11.*
laravel: 12.*
stability: prefer-lowest
testbench: 9.*
testbench: 10.*
- laravel: 12.*
testbench: 10.*
- laravel: 11.*
testbench: 9.*
- laravel: 13.*
testbench: 11.*

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }}

Expand Down
22 changes: 11 additions & 11 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@
],
"require": {
"php": "^8.3",
"illuminate/contracts": "^10.0||^11.0||^12.0",
"illuminate/database": "^10.0||^11.0||^12.0",
"illuminate/events": "^10.0||^11.0||^12.0",
"illuminate/support": "^10.0||^11.0||^12.0",
"solution-forest/workflow-engine-core": "dev-main || ^0.0.3-alpha"
"illuminate/contracts": "^10.0||^11.0||^12.0||^13.0",
"illuminate/database": "^10.0||^11.0||^12.0||^13.0",
"illuminate/events": "^10.0||^11.0||^12.0||^13.0",
"illuminate/support": "^10.0||^11.0||^12.0||^13.0",
"solution-forest/workflow-engine-core": "^1.0"
},
"conflict": {
"conflict": {
"laravel/framework": "<11.0.0"
},
"require-dev": {
"laravel/pint": "^1.14",
"nunomaduro/collision": "^8.1.1||^7.10.0",
"larastan/larastan": "^2.9||^3.0",
"orchestra/testbench": "^9.0.0|^10.0.0",
"pestphp/pest": "^3.0",
"pestphp/pest-plugin-arch": "^3.0",
"pestphp/pest-plugin-laravel": "^3.0",
"orchestra/testbench": "^9.0.0|^10.0.0|^11.0.0",
"pestphp/pest": "^3.0|^4.0",
"pestphp/pest-plugin-arch": "^3.0|^4.0",
"pestphp/pest-plugin-laravel": "^3.0|^4.0",
"phpstan/extension-installer": "^1.3||^2.0",
"phpstan/phpstan-deprecation-rules": "^1.1||^2.0",
"phpstan/phpstan-phpunit": "^1.3||^2.0",
Expand Down Expand Up @@ -82,6 +82,6 @@
}
}
},
"minimum-stability": "dev",
"minimum-stability": "stable",
"prefer-stable": true
}
2 changes: 1 addition & 1 deletion src/Facades/WorkflowEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use SolutionForest\WorkflowEngine\Core\WorkflowEngine as CoreWorkflowEngine;

/**
* @see \SolutionForest\WorkflowEngine\Core\WorkflowEngine
* @see CoreWorkflowEngine
*/
class WorkflowEngine extends Facade
{
Expand Down
5 changes: 3 additions & 2 deletions src/Models/WorkflowInstance.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace SolutionForest\WorkflowEngine\Laravel\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use SolutionForest\WorkflowEngine\Core\WorkflowDefinition;
use SolutionForest\WorkflowEngine\Core\WorkflowInstance as CoreWorkflowInstance;
Expand All @@ -18,8 +19,8 @@
* @property array $completed_steps
* @property array $failed_steps
* @property string|null $error_message
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class WorkflowInstance extends Model
{
Expand Down
20 changes: 13 additions & 7 deletions src/Providers/WorkflowEngineServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

namespace SolutionForest\WorkflowEngine\Laravel\Providers;

use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\DatabaseManager;
use Illuminate\Log\LogManager;
use Illuminate\Support\ServiceProvider;
use SolutionForest\WorkflowEngine\Contracts\EventDispatcher;
use SolutionForest\WorkflowEngine\Contracts\Logger;
use SolutionForest\WorkflowEngine\Contracts\StorageAdapter;
use SolutionForest\WorkflowEngine\Core\WorkflowEngine;
use SolutionForest\WorkflowEngine\Laravel\Adapters\LaravelEventDispatcher;
use SolutionForest\WorkflowEngine\Laravel\Adapters\LaravelLogger;
use SolutionForest\WorkflowEngine\Laravel\Commands\LaravelWorkflowEngineCommand;
use SolutionForest\WorkflowEngine\Laravel\Storage\DatabaseStorage;

Expand Down Expand Up @@ -33,24 +39,24 @@ 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)
$this->app->singleton(EventDispatcher::class, function ($app) {
return new LaravelEventDispatcher(
$app->make(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)
$this->app->singleton(Logger::class, function ($app) {
return new LaravelLogger(
$app->make(LogManager::class)
);
});

// Register workflow engine
$this->app->singleton(WorkflowEngine::class, function ($app): WorkflowEngine {
return new WorkflowEngine(
$app->make(StorageAdapter::class),
$app->make(\SolutionForest\WorkflowEngine\Contracts\EventDispatcher::class)
$app->make(EventDispatcher::class)
);
});

Expand Down
17 changes: 8 additions & 9 deletions tests/Actions/ECommerce/ProcessPaymentAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,20 @@ public function execute(WorkflowContext $context): ActionResult
$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');
}

// WorkflowContext is immutable, so what the step produces is returned
// rather than written back into the context it was handed.
return new ActionResult(
success: $success,
data: [
'payment_id' => $success ? $paymentId : null,
'amount' => $order['total'],
'status' => $success ? 'completed' : 'failed',
'payment' => [
'id' => $success ? $paymentId : null,
'success' => $success,
'amount' => $success ? $order['total'] : null,
'error' => $success ? null : 'Payment declined',
],
],
errorMessage: $success ? null : 'Payment processing failed'
);
Expand Down
4 changes: 3 additions & 1 deletion tests/Integration/PackageIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php

use SolutionForest\WorkflowEngine\Core\WorkflowEngine;

test('package integration works', function () {
// Just test core workflow engine
expect(class_exists(\SolutionForest\WorkflowEngine\Core\WorkflowEngine::class))->toBeTrue();
expect(class_exists(WorkflowEngine::class))->toBeTrue();
});
17 changes: 9 additions & 8 deletions tests/Unit/AdvancedFeaturesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use SolutionForest\WorkflowEngine\Core\WorkflowBuilder;
use SolutionForest\WorkflowEngine\Core\WorkflowEngine;
use SolutionForest\WorkflowEngine\Exceptions\InvalidWorkflowDefinitionException;
use SolutionForest\WorkflowEngine\Laravel\Tests\Support\TestActions\CreateUserProfileAction;
use SolutionForest\WorkflowEngine\Laravel\Tests\Support\TestActions\RiskyAction;
use SolutionForest\WorkflowEngine\Laravel\Tests\Support\TestActions\SendWelcomeEmailAction;
Expand All @@ -24,7 +25,7 @@
->build();

$steps = $workflow->getSteps();
$step = $steps['email_0'];
$step = $steps['email_1'];
expect($step->getConfig())->toBe([
'template' => 'welcome-email',
'to' => '{{ user.email }}',
Expand All @@ -43,9 +44,9 @@
expect($workflow->getSteps())->toHaveCount(3);

$steps = $workflow->getSteps();
expect($steps['delay_0']->getConfig()['seconds'])->toBe(30);
expect($steps['delay_1']->getConfig()['seconds'])->toBe(300);
expect($steps['delay_2']->getConfig()['seconds'])->toBe(5400); // 1.5 hours
expect($steps['delay_1']->getConfig()['seconds'])->toBe(30);
expect($steps['delay_2']->getConfig()['seconds'])->toBe(300);
expect($steps['delay_3']->getConfig()['seconds'])->toBe(5400); // 1.5 hours
});

test('http action supports all documented parameters', function () {
Expand Down Expand Up @@ -204,21 +205,21 @@
test('workflow validation catches invalid configurations', function () {
expect(function () {
WorkflowBuilder::create(''); // Empty name should fail
})->toThrow(\SolutionForest\WorkflowEngine\Exceptions\InvalidWorkflowDefinitionException::class);
})->toThrow(InvalidWorkflowDefinitionException::class);

expect(function () {
WorkflowBuilder::create('123invalid'); // Invalid name format
})->toThrow(\SolutionForest\WorkflowEngine\Exceptions\InvalidWorkflowDefinitionException::class);
})->toThrow(InvalidWorkflowDefinitionException::class);

expect(function () {
WorkflowBuilder::create('valid-name')
->delay(); // No delay specified should fail
})->toThrow(\SolutionForest\WorkflowEngine\Exceptions\InvalidWorkflowDefinitionException::class);
})->toThrow(InvalidWorkflowDefinitionException::class);

expect(function () {
WorkflowBuilder::create('valid-name')
->when('', function ($builder) {}); // Empty condition should fail
})->toThrow(\SolutionForest\WorkflowEngine\Exceptions\InvalidWorkflowDefinitionException::class);
})->toThrow(InvalidWorkflowDefinitionException::class);
});

});
47 changes: 35 additions & 12 deletions tests/Unit/DocumentationExamplesTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use SolutionForest\WorkflowEngine\Core\WorkflowBuilder;
use SolutionForest\WorkflowEngine\Core\WorkflowContext;
use SolutionForest\WorkflowEngine\Core\WorkflowEngine;
use SolutionForest\WorkflowEngine\Core\WorkflowState;
use SolutionForest\WorkflowEngine\Laravel\Tests\Actions\ECommerce\ProcessPaymentAction;
Expand Down Expand Up @@ -50,7 +51,7 @@

// Direct test of the getData method
$contextClass = get_class($context);
expect($contextClass)->toBe(\SolutionForest\WorkflowEngine\Core\WorkflowContext::class);
expect($contextClass)->toBe(WorkflowContext::class);

// Test the getData method implementation directly
$allData = $context->getData();
Expand Down Expand Up @@ -134,7 +135,7 @@
expect($workflow->getSteps())->toHaveCount(1);

$steps = $workflow->getSteps();
$step = $steps['email_0'];
$step = $steps['email_1'];
expect($step->getActionClass())->toBe('SolutionForest\\WorkflowEngine\\Actions\\EmailAction');
expect($step->getConfig()['template'])->toBe('welcome-email');
expect($step->getConfig()['to'])->toBe('{{ user.email }}');
Expand All @@ -153,7 +154,7 @@
expect($workflow->getSteps())->toHaveCount(1);

$steps = $workflow->getSteps();
$step = $steps['http_0'];
$step = $steps['http_1'];
expect($step->getActionClass())->toBe('SolutionForest\\WorkflowEngine\\Actions\\HttpAction');
expect($step->getConfig()['url'])->toBe('https://api.example.com/webhooks');
expect($step->getConfig()['method'])->toBe('POST');
Expand All @@ -175,13 +176,13 @@
$steps = $workflow->getSteps();

// 30 minutes = 1800 seconds
expect($steps['delay_0']->getConfig()['seconds'])->toBe(1800);
expect($steps['delay_1']->getConfig()['seconds'])->toBe(1800);

// 2 hours = 7200 seconds
expect($steps['delay_1']->getConfig()['seconds'])->toBe(7200);
expect($steps['delay_2']->getConfig()['seconds'])->toBe(7200);

// 24 hours = 86400 seconds
expect($steps['delay_2']->getConfig()['seconds'])->toBe(86400);
expect($steps['delay_3']->getConfig()['seconds'])->toBe(86400);
});

test('api reference - when conditional method works', function () {
Expand Down Expand Up @@ -245,16 +246,38 @@

expect($workflow->getName())->toBe('complex-workflow');
expect($workflow->getSteps())->toHaveCount(6); // startWith + email + conditional + delay + http + then
});

// Test execution
$definition = $workflow->toArray();
$workflowId = $this->engine->start('complex-workflow', $definition, [
test('a workflow combining multiple features executes', function () {
// The http step is deliberately absent here. HttpAction issues a real
// curl request, so executing one in a unit test reaches the network:
// it made this suite depend on DNS and took five minutes to fail when
// the host could not be resolved. The builder assertions above still
// cover the http step; this covers execution.
$workflow = WorkflowBuilder::create('complex-workflow')
->description('A complex workflow showcasing all features')
->version('2.0')
->startWith(CreateUserProfileAction::class, ['profile_type' => 'premium'])
->email('welcome-email', '{{ user.email }}', 'Welcome to Premium!')
->when('user.age >= 21', function ($builder) {
$builder->addStep('age-verification', VerifyIdentityAction::class, [], '60s', 2);
})
// One second, not the five minutes the builder example uses:
// DelayAction sleeps for real, so executing that example made this
// single test take five minutes of wall clock.
->delay(seconds: 1)
->then(ProcessPaymentAction::class, ['amount' => 99.99], '120s', 3)
->build();

$workflowId = $this->engine->start('complex-workflow', $workflow->toArray(), [
'user' => ['id' => 1, 'email' => 'test@example.com', 'age' => 25],
// The payment step needs an order. The original test never reached
// it, because the http step before it failed on DNS first.
'order' => ['id' => 1, 'total' => 9999],
]);

expect($workflowId)->not->toBeEmpty();
$instance = $this->engine->getInstance($workflowId);
expect($instance)->not->toBeNull();
expect($workflowId)->not->toBeEmpty()
->and($this->engine->getInstance($workflowId))->not->toBeNull();
});

});
Loading