AutomationNode.php
3 years ago
AutomationNodeVisitor.php
3 years ago
AutomationWalker.php
2 months ago
index.php
3 years ago
AutomationWalker.php
89 lines
| 1 | <?php declare(strict_types = 1); |
| 2 | |
| 3 | namespace MailPoet\Automation\Engine\Validation\AutomationGraph; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use Generator; |
| 9 | use MailPoet\Automation\Engine\Data\Automation; |
| 10 | use MailPoet\Automation\Engine\Data\Step; |
| 11 | use MailPoet\Automation\Engine\Exceptions; |
| 12 | use MailPoet\Automation\Engine\Exceptions\InvalidStateException; |
| 13 | use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException; |
| 14 | |
| 15 | class AutomationWalker { |
| 16 | /** @param AutomationNodeVisitor[] $visitors */ |
| 17 | public function walk(Automation $automation, array $visitors = []): void { |
| 18 | $steps = $automation->getSteps(); |
| 19 | $root = $steps['root'] ?? null; |
| 20 | if (!$root) { |
| 21 | throw Exceptions::automationStructureNotValid(__("Automation must contain a 'root' step", 'mailpoet'), 'no-root'); |
| 22 | } |
| 23 | |
| 24 | foreach ($visitors as $visitor) { |
| 25 | $visitor->initialize($automation); |
| 26 | } |
| 27 | |
| 28 | foreach ($this->walkStepsDepthFirstPreOrder($steps, $root) as $record) { |
| 29 | [$step, $parents] = $record; |
| 30 | foreach ($visitors as $visitor) { |
| 31 | $visitor->visitNode($automation, new AutomationNode($step, array_values($parents))); |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | foreach ($visitors as $visitor) { |
| 36 | $visitor->complete($automation); |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * @param array<string|int, Step> $steps |
| 42 | * @return Generator<array{0: Step, 1: array<string|int, Step>}> |
| 43 | */ |
| 44 | private function walkStepsDepthFirstPreOrder(array $steps, Step $root): Generator { |
| 45 | /** @var array{0: Step, 1: array<string|int, Step>}[] $stack */ |
| 46 | $stack = [ |
| 47 | [$root, []], |
| 48 | ]; |
| 49 | |
| 50 | do { |
| 51 | $record = array_pop($stack); |
| 52 | if (!$record) { |
| 53 | throw new InvalidStateException('Unexpected empty stack during automation graph traversal'); |
| 54 | } |
| 55 | yield $record; |
| 56 | [$step, $parents] = $record; |
| 57 | |
| 58 | foreach (array_reverse($step->getNextSteps()) as $nextStepData) { |
| 59 | $nextStepId = $nextStepData->getId(); |
| 60 | if (!$nextStepId) { |
| 61 | continue; // empty edge |
| 62 | } |
| 63 | $nextStep = $steps[$nextStepId] ?? null; |
| 64 | if (!$nextStep) { |
| 65 | throw $this->createStepNotFoundException($nextStepId, $step->getId()); |
| 66 | } |
| 67 | |
| 68 | $nextStepParents = array_merge($parents, [$step->getId() => $step]); |
| 69 | if (isset($nextStepParents[$nextStepId])) { |
| 70 | continue; // cycle detected, do not enter the path again |
| 71 | } |
| 72 | array_push($stack, [$nextStep, $nextStepParents]); |
| 73 | } |
| 74 | } while (count($stack) > 0); |
| 75 | } |
| 76 | |
| 77 | private function createStepNotFoundException(string $stepId, string $parentStepId): UnexpectedValueException { |
| 78 | return Exceptions::automationStructureNotValid( |
| 79 | // translators: %1$s is ID of the step not found, %2$s is ID of the step that references it |
| 80 | sprintf( |
| 81 | __("Step with ID '%1\$s' not found (referenced from '%2\$s')", 'mailpoet'), |
| 82 | $stepId, |
| 83 | $parentStepId |
| 84 | ), |
| 85 | 'step-not-found' |
| 86 | ); |
| 87 | } |
| 88 | } |
| 89 |