Skip to content

Commit 48c6886

Browse files
authored
Define required values for the container (#3)
* Define required values for the container by implementing step dependencies. See README.md
1 parent aa93507 commit 48c6886

9 files changed

Lines changed: 604 additions & 2 deletions

README.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,21 @@ Bonus: you will get an execution log for each executed workflow - if you want to
1818

1919
* [Installation](#Installation)
2020
* [Example workflow](#Example-workflow)
21+
* [Workflow container](#Workflow-container)
2122
* [Stages](#Stages)
2223
* [Workflow control](#Workflow-control)
2324
* [Nested workflows](#Nested-workflows)
2425
* [Loops](#Loops)
26+
* [Step dependencies](#Step-dependencies)
27+
* [Required container values](#Required-container-values)
2528
* [Error handling, logging and debugging](#Error-handling-logging-and-debugging)
2629
* [Custom output formatter](#Custom-output-formatter)
2730
* [Tests](#Tests)
2831

2932
## Installation
3033

3134
The recommended way to install php-workflow is through [Composer](http://getcomposer.org):
35+
3236
```
3337
$ composer require wol-soft/php-workflow
3438
```
@@ -155,6 +159,8 @@ class AcceptOpenSuggestionForSong implements \PHPWorkflow\Step\WorkflowStep {
155159
}
156160
```
157161

162+
## Workflow container
163+
158164
Now let's have a more detailed look at the **WorkflowContainer** which helps us, to share data and objects between our workflow steps.
159165
The relevant objects for our example workflow is the **User** who wants to add the song, the **Song** object of the song to add and the **Playlist** object.
160166
Before we execute our workflow we can set up a **WorkflowContainer** which contains all relevant objects:
@@ -166,6 +172,22 @@ $workflowContainer = (new \PHPWorkflow\State\WorkflowContainer())
166172
->set('playlist', (new PlaylistRepository())->getPlaylistById($request->get('playlistId')));
167173
```
168174

175+
The workflow container provides the following interface:
176+
177+
```php
178+
// returns an item or null if the key doesn't exist
179+
public function get(string $key)
180+
// set or update a value
181+
public function set(string $key, $value): self
182+
// remove an entry
183+
public function unset(string $key): self
184+
// check if a key exists
185+
public function has(string $key): bool
186+
```
187+
188+
Each workflow step may define requirements, which entries must be present in the workflow container before the step is executed.
189+
For more details have a look at [Required container values](#Required-container-values).
190+
169191
Alternatively to set and get the values from the **WorkflowContainer** via string keys you can extend the **WorkflowContainer** and add typed properties/functions to handle values in a type-safe manner:
170192

171193
```php
@@ -192,7 +214,7 @@ $workflowResult = (new \PHPWorkflow\Workflow('AddSongToPlaylist'))
192214
->executeWorkflow($workflowContainer);
193215
```
194216

195-
Another possibility would be to define a step in the **Prepare** stage (e.g. **PopulateAddSongToPlaylistContainer**) which populates the injected **WorkflowContainer** object.
217+
Another possibility would be to define a step in the **Prepare** stage (e.g. **PopulateAddSongToPlaylistContainer**) which populates the automatically injected empty **WorkflowContainer** object.
196218

197219
## Stages
198220

@@ -425,6 +447,40 @@ If you enable this option a failed step will not result in a failed workflow.
425447
Instead, a warning will be added to the process log.
426448
Calls to `failWorkflow` and `skipWorkflow` will always cancel the loop (and consequently the workflow) independent of the option.
427449

450+
## Step dependencies
451+
452+
Each step implementation may apply dependencies to the step.
453+
By defining dependencies you can set up validation rules which are checked before your step is executed (for example: which data nust be provided in the workflow container).
454+
If any of the dependencies is not fulfilled the step will not be executed and is handled as a failed step.
455+
456+
Note: as this feature uses [Attributes](https://www.php.net/manual/de/language.attributes.overview.php), it is only available if you use PHP >= 8.0.
457+
458+
### Required container values
459+
460+
With the `\PHPWorkflow\Step\Dependency\Required` attribute you can define keys which must be present in the provided workflow container.
461+
The keys consequently must be provided in the initial workflow or be populated by a previous step.
462+
Additionally to the key you can also provide the type of the value (eg. `string`).
463+
464+
To define the dependency you simply annotate the provided workflow container parameter:
465+
466+
```php
467+
public function run(
468+
\PHPWorkflow\WorkflowControl $control,
469+
// The key customerId must contain a string
470+
#[\PHPWorkflow\Step\Dependency\Required('customerId', 'string')]
471+
// The customerAge must contain an integer. But also null is accepted.
472+
// Each type definition can be prefixed with a ? to accept null.
473+
#[\PHPWorkflow\Step\Dependency\Required('customerAge', '?int')]
474+
// Objects can also be type hinted
475+
#[\PHPWorkflow\Step\Dependency\Required('created', \DateTime::class)]
476+
\PHPWorkflow\State\WorkflowContainer $container,
477+
) {
478+
// Implementation which can rely on the defined keys to be present in the container.
479+
}
480+
```
481+
482+
The following types are supported: `string`, `bool`, `int`, `float`, `object`, `array`, `iterable`, `scalar` as well as object type hints by providing the corresponding FQCN
483+
428484
## Error handling, logging and debugging
429485

430486
The **executeWorkflow** method returns an **WorkflowResult** object which provides the following methods to determine the result of the workflow:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPWorkflow\Exception;
6+
7+
use Exception;
8+
9+
class WorkflowStepDependencyNotFulfilledException extends Exception
10+
{
11+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPWorkflow\Middleware;
6+
7+
use PHPWorkflow\Exception\WorkflowStepDependencyNotFulfilledException;
8+
use PHPWorkflow\State\WorkflowContainer;
9+
use PHPWorkflow\Step\Dependency\StepDependencyInterface;
10+
use PHPWorkflow\Step\WorkflowStep;
11+
use PHPWorkflow\WorkflowControl;
12+
use ReflectionAttribute;
13+
use ReflectionException;
14+
use ReflectionMethod;
15+
16+
class WorkflowStepDependencyCheck
17+
{
18+
/**
19+
* @throws ReflectionException
20+
* @throws WorkflowStepDependencyNotFulfilledException
21+
*/
22+
public function __invoke(
23+
callable $next,
24+
WorkflowControl $control,
25+
WorkflowContainer $container,
26+
WorkflowStep $step,
27+
) {
28+
$containerParameter = (new ReflectionMethod($step, 'run'))->getParameters()[1] ?? null;
29+
30+
if ($containerParameter) {
31+
foreach ($containerParameter->getAttributes(
32+
StepDependencyInterface::class,
33+
ReflectionAttribute::IS_INSTANCEOF,
34+
) as $dependencyAttribute
35+
) {
36+
/** @var StepDependencyInterface $dependency */
37+
$dependency = $dependencyAttribute->newInstance();
38+
$dependency->check($container);
39+
}
40+
}
41+
42+
return $next();
43+
}
44+
}

src/State/WorkflowContainer.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,15 @@ public function set(string $key, $value): self
1818
$this->items[$key] = $value;
1919
return $this;
2020
}
21+
22+
public function unset(string $key): self
23+
{
24+
unset($this->items[$key]);
25+
return $this;
26+
}
27+
28+
public function has(string $key): bool
29+
{
30+
return array_key_exists($key, $this->items);
31+
}
2132
}

src/Step/Dependency/Requires.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPWorkflow\Step\Dependency;
6+
7+
use Attribute;
8+
use PHPWorkflow\Exception\WorkflowStepDependencyNotFulfilledException;
9+
use PHPWorkflow\State\WorkflowContainer;
10+
11+
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::IS_REPEATABLE)]
12+
class Requires implements StepDependencyInterface
13+
{
14+
public function __construct(private string $key, private ?string $type = null) {}
15+
16+
public function check(WorkflowContainer $container): void
17+
{
18+
if (!$container->has($this->key)) {
19+
throw new WorkflowStepDependencyNotFulfilledException("Missing '$this->key' in container");
20+
}
21+
22+
$value = $container->get($this->key);
23+
24+
if ($this->type === null || (str_starts_with($this->type, '?') && $value === null)) {
25+
return;
26+
}
27+
28+
$type = str_replace('?', '', $this->type);
29+
30+
if (preg_match('/^(string|bool|int|float|object|array|iterable|scalar)$/', $type, $matches) === 1) {
31+
$checkMethod = 'is_' . $matches[1];
32+
33+
if ($checkMethod($value)) {
34+
return;
35+
}
36+
} elseif (class_exists($type) && ($value instanceof $type)) {
37+
return;
38+
}
39+
40+
throw new WorkflowStepDependencyNotFulfilledException(
41+
sprintf(
42+
"Value for '%s' has an invalid type. Expected %s, got %s",
43+
$this->key,
44+
$this->type,
45+
gettype($value) . (is_object($value) ? sprintf(' (%s)', $value::class) : ''),
46+
),
47+
);
48+
}
49+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPWorkflow\Step\Dependency;
6+
7+
use PHPWorkflow\Exception\WorkflowStepDependencyNotFulfilledException;
8+
use PHPWorkflow\State\WorkflowContainer;
9+
10+
interface StepDependencyInterface
11+
{
12+
/**
13+
* @throws WorkflowStepDependencyNotFulfilledException
14+
*/
15+
public function check(WorkflowContainer $container): void;
16+
}

src/Step/StepExecutionTrait.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPWorkflow\Exception\WorkflowControl\LoopControlException;
1010
use PHPWorkflow\Exception\WorkflowControl\SkipStepException;
1111
use PHPWorkflow\Exception\WorkflowControl\SkipWorkflowException;
12+
use PHPWorkflow\Middleware\WorkflowStepDependencyCheck;
1213
use PHPWorkflow\State\ExecutionLog\ExecutionLog;
1314
use PHPWorkflow\State\WorkflowState;
1415

@@ -65,11 +66,18 @@ private function resolveMiddleware(WorkflowStep $step, WorkflowState $workflowSt
6566
{
6667
$tip = fn () => $step->run($workflowState->getWorkflowControl(), $workflowState->getWorkflowContainer());
6768

68-
foreach ($workflowState->getMiddlewares() as $middleware) {
69+
$middlewares = $workflowState->getMiddlewares();
70+
71+
if (PHP_MAJOR_VERSION >= 8) {
72+
array_unshift($middlewares, new WorkflowStepDependencyCheck());
73+
}
74+
75+
foreach ($middlewares as $middleware) {
6976
$tip = fn () => $middleware(
7077
$tip,
7178
$workflowState->getWorkflowControl(),
7279
$workflowState->getWorkflowContainer(),
80+
$step,
7381
);
7482
}
7583

0 commit comments

Comments
 (0)