Files
piratepoet/mailpoet/lib/Automation/Engine/Control/SubjectTransformerHandler.php
David Remer 68f09b6bd1 Prevent infinite loop when a subject from the trigger is missing
Given the trigger provides two subjects but only one subject was given via parameter and no
transformer was able to generate the second from the first, this method runs into an
infinite loop. This commit prevents this infinite loop.

[MAILPOET-4935]
2023-03-30 12:21:25 +02:00

202 lines
5.9 KiB
PHP

<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\Step as StepData;
use MailPoet\Automation\Engine\Data\Subject;
use MailPoet\Automation\Engine\Integration\Step;
use MailPoet\Automation\Engine\Integration\SubjectTransformer;
use MailPoet\Automation\Engine\Integration\Trigger;
use MailPoet\Automation\Engine\Registry;
class SubjectTransformerHandler {
/* @var Registry */
private $registry;
public function __construct(
Registry $registry
) {
$this->registry = $registry;
}
public function subjectKeysForTrigger(Trigger $trigger): array {
$subjectKeys = $trigger->getSubjectKeys();
$possibleKeys = [];
foreach ($subjectKeys as $key) {
$possibleKeys = array_merge($possibleKeys, $this->getPossibleTransformations($key));
}
return array_unique(array_values(array_merge($subjectKeys, $possibleKeys)));
}
/** @return string[] */
public function subjectKeysForAutomation(Automation $automation): array {
$configuredTriggers = array_filter(
$automation->getSteps(),
function (StepData $step): bool {
return $step->getType() === StepData::TYPE_TRIGGER;
}
);
$triggers = array_filter(array_map(
function(StepData $step): ?Step {
return $this->registry->getStep($step->getKey());
},
$configuredTriggers
));
$triggerKeys = [];
foreach ($triggers as $trigger) {
$triggerKeys[$trigger->getKey()] = $trigger->getSubjectKeys();
}
if (!$triggerKeys) {
return [];
}
$triggerKeys = count($triggerKeys) === 1 ? current($triggerKeys) : array_intersect(...array_values($triggerKeys));
$possibleKeys = [];
foreach ($triggerKeys as $key) {
$possibleKeys = array_merge($possibleKeys, $this->getPossibleTransformations($key));
}
return array_unique(array_values(array_merge($triggerKeys, $possibleKeys)));
}
private function getPossibleTransformations(string $key, string $stopKey = null): array {
if (!$stopKey) {
$stopKey = $key;
}
$allTransformer = $this->registry->getSubjectTransformer();
$possibleTransformer = array_filter(
$allTransformer,
function (SubjectTransformer $transformer) use ($key): bool {
return $transformer->accepts() === $key;
}
);
$possibleKeys = [];
foreach ($possibleTransformer as $transformer) {
$possibleKey = $transformer->returns();
if ($possibleKey === $stopKey) {
continue;
}
$possibleKeys[$possibleKey] = $possibleKey;
$possibleKeys = array_merge(
$possibleKeys,
$this->getPossibleTransformations($possibleKey, $stopKey)
);
}
return $possibleKeys;
}
/**
* @param Trigger $trigger
* @param Subject ...$subjects
* @return Subject[]
*/
public function provideAllSubjects(Trigger $trigger, Subject ...$subjects): array {
$allSubjectsKeys = $this->subjectKeysForTrigger($trigger);
$allSubjectKeyTargets = array_filter(
array_diff($allSubjectsKeys, array_map(
function(Subject $subject): string {
return $subject->getKey();
},
$subjects
)),
function(string $target) use ($subjects): bool {
return $this->canSubjectsTransformTo($target, ...$subjects);
}
);
$allSubjects = [];
foreach ($subjects as $existingSubject) {
$allSubjects[$existingSubject->getKey()] = $existingSubject;
}
foreach ($allSubjectKeyTargets as $target) {
if (isset($allSubjects[$target])) {
continue;
}
foreach ($subjects as $subject) {
$transformedSubject = $this->transformSubjectTo($subject, $target);
if ($transformedSubject) {
$allSubjects[$transformedSubject->getKey()] = $transformedSubject;
}
}
}
$allSubjectsTransformed = true;
do {
foreach ($allSubjectKeyTargets as $key) {
if (!isset($allSubjects[$key])) {
$allSubjectsTransformed = false;
break;
}
}
$allSubjects = $allSubjectsTransformed ? $allSubjects : $this->provideAllSubjects($trigger, ...array_values($allSubjects));
} while (!$allSubjectsTransformed);
return array_values($allSubjects);
}
private function transformSubjectTo(Subject $subject, string $target): ?Subject {
$transformerChain = $this->getTransformerChain($subject->getKey(), $target);
if (!$transformerChain) {
return null;
}
foreach ($transformerChain as $transformer) {
$subject = $transformer->transform($subject);
}
return $subject;
}
/**
* @return SubjectTransformer[]
*/
private function getTransformerChain(string $from, string $to): array {
$transformers = $this->registry->getSubjectTransformer();
$transformerChain = [];
//walk the graph of transformers to find the shortest path
$queue = [];
$queue[] = [$from, []];
while (count($queue) > 0) {
$current = array_shift($queue);
$currentKey = $current[0];
$currentPath = $current[1];
if ($currentKey === $to) {
$transformerChain = $currentPath;
break;
}
foreach ($transformers as $transformer) {
if ($transformer->accepts() === $currentKey) {
$newPath = $currentPath;
$newPath[] = $transformer;
$queue[] = [$transformer->returns(), $newPath];
}
}
}
return $transformerChain;
}
private function canSubjectsTransformTo(string $target, Subject ...$subjects): bool {
if (
in_array($target, array_map(
function(Subject $subject): string {
return $subject->getKey();
},
$subjects
))
) {
return true;
}
foreach ($subjects as $subject) {
$possibleTransformations = $this->getPossibleTransformations($subject->getKey());
if (in_array($target, $possibleTransformations)) {
return true;
}
}
return false;
}
}