Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define required values for the container #3

Merged
merged 4 commits into from
Apr 21, 2022
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
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