Implerment depth-first pre-order workflow graph walker with plug-in node visitors

[MAILPOET-4629]
This commit is contained in:
Jan Jakes
2022-09-14 15:10:18 +02:00
committed by David Remer
parent e3668a8187
commit e472c00b1c
6 changed files with 264 additions and 0 deletions

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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
)
);
}
}