Skip to content

Commit

Permalink
Define required values for the container (#3)
Browse files Browse the repository at this point in the history
* Define required values for the container by implementing step dependencies. See README.md
  • Loading branch information
wol-soft authored Apr 21, 2022
1 parent aa93507 commit 48c6886
Show file tree
Hide file tree
Showing 9 changed files with 604 additions and 2 deletions.
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@ Bonus: you will get an execution log for each executed workflow - if you want to

* [Installation](#Installation)
* [Example workflow](#Example-workflow)
* [Workflow container](#Workflow-container)
* [Stages](#Stages)
* [Workflow control](#Workflow-control)
* [Nested workflows](#Nested-workflows)
* [Loops](#Loops)
* [Step dependencies](#Step-dependencies)
* [Required container values](#Required-container-values)
* [Error handling, logging and debugging](#Error-handling-logging-and-debugging)
* [Custom output formatter](#Custom-output-formatter)
* [Tests](#Tests)

## Installation

The recommended way to install php-workflow is through [Composer](http://getcomposer.org):

```
$ composer require wol-soft/php-workflow
```
Expand Down Expand Up @@ -155,6 +159,8 @@ class AcceptOpenSuggestionForSong implements \PHPWorkflow\Step\WorkflowStep {
}
```

## Workflow container

Now let's have a more detailed look at the **WorkflowContainer** which helps us, to share data and objects between our workflow steps.
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.
Before we execute our workflow we can set up a **WorkflowContainer** which contains all relevant objects:
Expand All @@ -166,6 +172,22 @@ $workflowContainer = (new \PHPWorkflow\State\WorkflowContainer())
->set('playlist', (new PlaylistRepository())->getPlaylistById($request->get('playlistId')));
```

The workflow container provides the following interface:

```php
// returns an item or null if the key doesn't exist
public function get(string $key)
// set or update a value
public function set(string $key, $value): self
// remove an entry
public function unset(string $key): self
// check if a key exists
public function has(string $key): bool
```

Each workflow step may define requirements, which entries must be present in the workflow container before the step is executed.
For more details have a look at [Required container values](#Required-container-values).

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:

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

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

## Stages

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

## Step dependencies

Each step implementation may apply dependencies to the step.
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).
If any of the dependencies is not fulfilled the step will not be executed and is handled as a failed step.

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.

### Required container values

With the `\PHPWorkflow\Step\Dependency\Required` attribute you can define keys which must be present in the provided workflow container.
The keys consequently must be provided in the initial workflow or be populated by a previous step.
Additionally to the key you can also provide the type of the value (eg. `string`).

To define the dependency you simply annotate the provided workflow container parameter:

```php
public function run(
\PHPWorkflow\WorkflowControl $control,
// The key customerId must contain a string
#[\PHPWorkflow\Step\Dependency\Required('customerId', 'string')]
// The customerAge must contain an integer. But also null is accepted.
// Each type definition can be prefixed with a ? to accept null.
#[\PHPWorkflow\Step\Dependency\Required('customerAge', '?int')]
// Objects can also be type hinted
#[\PHPWorkflow\Step\Dependency\Required('created', \DateTime::class)]
\PHPWorkflow\State\WorkflowContainer $container,
) {
// Implementation which can rely on the defined keys to be present in the container.
}
```

The following types are supported: `string`, `bool`, `int`, `float`, `object`, `array`, `iterable`, `scalar` as well as object type hints by providing the corresponding FQCN

## Error handling, logging and debugging

The **executeWorkflow** method returns an **WorkflowResult** object which provides the following methods to determine the result of the workflow:
Expand Down
11 changes: 11 additions & 0 deletions src/Exception/WorkflowStepDependencyNotFulfilledException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace PHPWorkflow\Exception;

use Exception;

class WorkflowStepDependencyNotFulfilledException extends Exception
{
}
44 changes: 44 additions & 0 deletions src/Middleware/WorkflowStepDependencyCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace PHPWorkflow\Middleware;

use PHPWorkflow\Exception\WorkflowStepDependencyNotFulfilledException;
use PHPWorkflow\State\WorkflowContainer;
use PHPWorkflow\Step\Dependency\StepDependencyInterface;
use PHPWorkflow\Step\WorkflowStep;
use PHPWorkflow\WorkflowControl;
use ReflectionAttribute;
use ReflectionException;
use ReflectionMethod;

class WorkflowStepDependencyCheck
{
/**
* @throws ReflectionException
* @throws WorkflowStepDependencyNotFulfilledException
*/
public function __invoke(
callable $next,
WorkflowControl $control,
WorkflowContainer $container,
WorkflowStep $step,
) {
$containerParameter = (new ReflectionMethod($step, 'run'))->getParameters()[1] ?? null;

if ($containerParameter) {
foreach ($containerParameter->getAttributes(
StepDependencyInterface::class,
ReflectionAttribute::IS_INSTANCEOF,
) as $dependencyAttribute
) {
/** @var StepDependencyInterface $dependency */
$dependency = $dependencyAttribute->newInstance();
$dependency->check($container);
}
}

return $next();
}
}
11 changes: 11 additions & 0 deletions src/State/WorkflowContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,15 @@ public function set(string $key, $value): self
$this->items[$key] = $value;
return $this;
}

public function unset(string $key): self
{
unset($this->items[$key]);
return $this;
}

public function has(string $key): bool
{
return array_key_exists($key, $this->items);
}
}
49 changes: 49 additions & 0 deletions src/Step/Dependency/Requires.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace PHPWorkflow\Step\Dependency;

use Attribute;
use PHPWorkflow\Exception\WorkflowStepDependencyNotFulfilledException;
use PHPWorkflow\State\WorkflowContainer;

#[Attribute(Attribute::TARGET_PARAMETER | Attribute::IS_REPEATABLE)]
class Requires implements StepDependencyInterface
{
public function __construct(private string $key, private ?string $type = null) {}

public function check(WorkflowContainer $container): void
{
if (!$container->has($this->key)) {
throw new WorkflowStepDependencyNotFulfilledException("Missing '$this->key' in container");
}

$value = $container->get($this->key);

if ($this->type === null || (str_starts_with($this->type, '?') && $value === null)) {
return;
}

$type = str_replace('?', '', $this->type);

if (preg_match('/^(string|bool|int|float|object|array|iterable|scalar)$/', $type, $matches) === 1) {
$checkMethod = 'is_' . $matches[1];

if ($checkMethod($value)) {
return;
}
} elseif (class_exists($type) && ($value instanceof $type)) {
return;
}

throw new WorkflowStepDependencyNotFulfilledException(
sprintf(
"Value for '%s' has an invalid type. Expected %s, got %s",
$this->key,
$this->type,
gettype($value) . (is_object($value) ? sprintf(' (%s)', $value::class) : ''),
),
);
}
}
16 changes: 16 additions & 0 deletions src/Step/Dependency/StepDependencyInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace PHPWorkflow\Step\Dependency;

use PHPWorkflow\Exception\WorkflowStepDependencyNotFulfilledException;
use PHPWorkflow\State\WorkflowContainer;

interface StepDependencyInterface
{
/**
* @throws WorkflowStepDependencyNotFulfilledException
*/
public function check(WorkflowContainer $container): void;
}
10 changes: 9 additions & 1 deletion src/Step/StepExecutionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPWorkflow\Exception\WorkflowControl\LoopControlException;
use PHPWorkflow\Exception\WorkflowControl\SkipStepException;
use PHPWorkflow\Exception\WorkflowControl\SkipWorkflowException;
use PHPWorkflow\Middleware\WorkflowStepDependencyCheck;
use PHPWorkflow\State\ExecutionLog\ExecutionLog;
use PHPWorkflow\State\WorkflowState;

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

foreach ($workflowState->getMiddlewares() as $middleware) {
$middlewares = $workflowState->getMiddlewares();

if (PHP_MAJOR_VERSION >= 8) {
array_unshift($middlewares, new WorkflowStepDependencyCheck());
}

foreach ($middlewares as $middleware) {
$tip = fn () => $middleware(
$tip,
$workflowState->getWorkflowControl(),
$workflowState->getWorkflowContainer(),
$step,
);
}

Expand Down
Loading

0 comments on commit 48c6886

Please sign in to comment.