Skip to content

Commit

Permalink
Define required values for the container by implementing step depende…
Browse files Browse the repository at this point in the history
…ncies. See README.md
  • Loading branch information
Enno Woortmann committed Apr 20, 2022
1 parent aa93507 commit 0d7adc4
Show file tree
Hide file tree
Showing 8 changed files with 557 additions and 6 deletions.
46 changes: 41 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Bonus: you will get an execution log for each executed workflow - if you want to
* [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)
Expand Down Expand Up @@ -425,6 +427,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 Expand Up @@ -521,11 +557,11 @@ By default a string representation of the execution will be returned (just like

Currently the following additional formatters are implemented:

| Formatter | Description |
| --------------- | ------------- |
| `StringLog` | The default formatter. Creates a string representation. <br />Example:<br />`$result->debug();` |
| `WorkflowGraph` | Creates a SVG file containing a graph which represents the workflow execution. The generated image will be stored in the provided target directory. Requires `dot` executable.<br />Example:<br />`$result->debug(new WorkflowGraph('/var/log/workflow/graph'));` |
| `GraphViz` | Returns a string containing [GraphViz](https://graphviz.org/) code for a graph representing the workflow execution. <br />Example:<br />`$result->debug(new GraphViz());`|
`, ` Formatter `, ` Description `, `
`, ` --------------- `, ` ------------- `, `
`, ` `StringLog` `, ` The default formatter. Creates a string representation. <br />Example:<br />`$result->debug();` `, `
`, ` `WorkflowGraph` `, ` Creates a SVG file containing a graph which represents the workflow execution. The generated image will be stored in the provided target directory. Requires `dot` executable.<br />Example:<br />`$result->debug(new WorkflowGraph('/var/log/workflow/graph'));` `, `
`, ` `GraphViz` `, ` Returns a string containing [GraphViz](https://graphviz.org/) code for a graph representing the workflow execution. <br />Example:<br />`$result->debug(new GraphViz());``, `

## Tests

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
{
$this->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 0d7adc4

Please sign in to comment.