Implerment depth-first pre-order workflow graph walker with plug-in node visitors
[MAILPOET-4629]
This commit is contained in:
@ -0,0 +1,31 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\WorkflowGraph;
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
|
||||
class WorkflowNode {
|
||||
/** @var Step */
|
||||
private $step;
|
||||
|
||||
/** @var array */
|
||||
private $parents;
|
||||
|
||||
/* @param Step[] $parents */
|
||||
public function __construct(
|
||||
Step $step,
|
||||
array $parents
|
||||
) {
|
||||
$this->step = $step;
|
||||
$this->parents = $parents;
|
||||
}
|
||||
|
||||
public function getStep(): Step {
|
||||
return $this->step;
|
||||
}
|
||||
|
||||
/** @return Step[] */
|
||||
public function getParents(): array {
|
||||
return $this->parents;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\WorkflowGraph;
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Workflow;
|
||||
|
||||
interface WorkflowNodeVisitor {
|
||||
public function initialize(Workflow $workflow): void;
|
||||
|
||||
public function visitNode(Workflow $workflow, WorkflowNode $node): void;
|
||||
|
||||
public function complete(Workflow $workflow): void;
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\WorkflowGraph;
|
||||
|
||||
use Generator;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Data\Workflow;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException;
|
||||
|
||||
class WorkflowWalker {
|
||||
/** @param WorkflowNodeVisitor[] $visitors */
|
||||
public function walk(Workflow $workflow, array $visitors = []): void {
|
||||
$steps = $workflow->getSteps();
|
||||
$root = $steps['root'] ?? null;
|
||||
if (!$root) {
|
||||
throw Exceptions::workflowStructureNotValid(__("Workflow must contain a 'root' step", 'mailpoet'));
|
||||
}
|
||||
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->initialize($workflow);
|
||||
}
|
||||
|
||||
foreach ($this->walkStepsDepthFirstPreOrder($steps, $root) as $record) {
|
||||
[$step, $parents] = $record;
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->visitNode($workflow, new WorkflowNode($step, array_values($parents)));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->complete($workflow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Step> $steps
|
||||
* @return Generator<array{0: Step, 1: array<string, Step>}>
|
||||
*/
|
||||
private function walkStepsDepthFirstPreOrder(array $steps, Step $root): Generator {
|
||||
/** @var array{0: Step, 1: array<string, Step>}[] $stack */
|
||||
$stack = [
|
||||
[$root, []],
|
||||
];
|
||||
|
||||
do {
|
||||
$record = array_pop($stack);
|
||||
if (!$record) {
|
||||
throw new InvalidStateException();
|
||||
}
|
||||
yield $record;
|
||||
[$step, $parents] = $record;
|
||||
|
||||
foreach (array_reverse($step->getNextSteps()) as $nextStepData) {
|
||||
$nextStepId = $nextStepData->getId();
|
||||
$nextStep = $steps[$nextStepId] ?? null;
|
||||
if (!$nextStep) {
|
||||
throw $this->createStepNotFoundException($nextStepId, $step->getId());
|
||||
}
|
||||
|
||||
$nextStepParents = array_merge($parents, [$step->getId() => $step]);
|
||||
if (isset($nextStepParents[$nextStepId])) {
|
||||
continue; // cycle detected, do not enter the path again
|
||||
}
|
||||
array_push($stack, [$nextStep, $nextStepParents]);
|
||||
}
|
||||
} while (count($stack) > 0);
|
||||
}
|
||||
|
||||
private function createStepNotFoundException(string $stepId, string $parentStepId): UnexpectedValueException {
|
||||
return Exceptions::workflowStructureNotValid(
|
||||
// translators: %1$s is ID of the step not found, %2$s is ID of the step that references it
|
||||
sprintf(
|
||||
__("Step with ID '%1\$s' not found (referenced from '%2\$s')", 'mailpoet'),
|
||||
$stepId,
|
||||
$parentStepId
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user