This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use ActionScheduler_Action;
class ActionScheduler {
private const GROUP_ID = 'mailpoet-automation';
public function enqueue(string $hook, array $args = []): int {
$result = as_enqueue_async_action($hook, $args, self::GROUP_ID);
return is_int($result) ? $result : 0;
}
public function schedule(int $timestamp, string $hook, array $args = []): int {
$result = as_schedule_single_action($timestamp, $hook, $args, self::GROUP_ID);
return is_int($result) ? $result : 0;
}
public function hasScheduledAction(string $hook, array $args = []): bool {
return as_has_scheduled_action($hook, $args, self::GROUP_ID);
}
/** @return ActionScheduler_Action[] */
public function getScheduledActions(array $args = []): array {
return as_get_scheduled_actions(array_merge($args, ['group' => self::GROUP_ID]));
}
public function unscheduleAction(string $hook, array $args = []): ?int {
return as_unschedule_action($hook, $args, self::GROUP_ID);
}
}
@@ -0,0 +1,57 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use ActionScheduler_CanceledAction;
use MailPoet\Automation\Engine\Data\AutomationRunLog;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Storage\AutomationRunLogStorage;
class AutomationController {
private ActionScheduler $actionScheduler;
private AutomationRunLogStorage $automationRunLogStorage;
public function __construct(
ActionScheduler $actionScheduler,
AutomationRunLogStorage $automationRunLogStorage
) {
$this->actionScheduler = $actionScheduler;
$this->automationRunLogStorage = $automationRunLogStorage;
}
public function enqueueProgress(int $runId, string $stepId): void {
$log = $this->automationRunLogStorage->getAutomationRunLogByRunAndStepId($runId, $stepId);
if (!$log) {
throw Exceptions::stepNotStarted($stepId, $runId);
}
if ($log->getStatus() !== AutomationRunLog::STATUS_RUNNING) {
throw Exceptions::stepNotRunning($stepId, $log->getStatus(), $runId);
}
$runNumber = $log->getRunNumber() + 1;
$args = [
'automation_run_id' => $runId,
'step_id' => $stepId,
'run_number' => $runNumber,
];
// if a pending action exists, unschedule it
$this->actionScheduler->unscheduleAction(Hooks::AUTOMATION_STEP, [$args]);
// if an action still exists (pending, in-progress, complete, failed), it's an error
$actions = $this->actionScheduler->getScheduledActions(['hook' => Hooks::AUTOMATION_STEP, 'args' => [$args]]);
$processedActions = array_filter($actions, function ($action) {
return !$action instanceof ActionScheduler_CanceledAction;
});
if (count($processedActions) > 0) {
throw Exceptions::stepActionProcessed($stepId, $runId, $runNumber);
}
$this->actionScheduler->enqueue(Hooks::AUTOMATION_STEP, [$args]);
}
}
@@ -0,0 +1,68 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Filter as FilterData;
use MailPoet\Automation\Engine\Data\FilterGroup;
use MailPoet\Automation\Engine\Data\Filters;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Integration\Filter;
use MailPoet\Automation\Engine\Registry;
class FilterHandler {
/** @var Registry */
private $registry;
public function __construct(
Registry $registry
) {
$this->registry = $registry;
}
public function matchesFilters(StepRunArgs $args): bool {
$filters = $args->getStep()->getFilters();
if (!$filters) {
return true;
}
$operator = $filters->getOperator();
foreach ($filters->getGroups() as $group) {
$matches = $this->matchesGroup($group, $args);
if ($operator === Filters::OPERATOR_AND && !$matches) {
return false;
}
if ($operator === Filters::OPERATOR_OR && $matches) {
return true;
}
}
return $operator === Filters::OPERATOR_AND;
}
public function matchesGroup(FilterGroup $group, StepRunArgs $args): bool {
$operator = $group->getOperator();
foreach ($group->getFilters() as $filterData) {
$filter = $this->getFilter($filterData);
$value = $args->getFieldValue($filterData->getFieldKey(), $filter->getFieldParams($filterData));
$matches = $filter->matches($filterData, $value);
if ($operator === FilterGroup::OPERATOR_AND && !$matches) {
return false;
}
if ($operator === FilterGroup::OPERATOR_OR && $matches) {
return true;
}
}
return $operator === FilterGroup::OPERATOR_AND;
}
private function getFilter(FilterData $data): Filter {
$filter = $this->registry->getFilter($data->getFieldType());
if (!$filter) {
throw Exceptions::filterNotFound($data->getFieldType());
}
return $filter;
}
}
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\StepValidationArgs;
use MailPoet\Automation\Engine\Integration\Step;
use MailPoet\Validator\Schema\ObjectSchema;
class RootStep implements Step {
public function getKey(): string {
return 'core:root';
}
public function getName(): string {
// translators: not shown to user, no need to translate
return __('Root step', 'mailpoet');
}
public function getArgsSchema(): ObjectSchema {
return new ObjectSchema();
}
public function getSubjectKeys(): array {
return [];
}
public function validate(StepValidationArgs $args): void {
}
}
@@ -0,0 +1,221 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use Exception;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\AutomationRun;
use MailPoet\Automation\Engine\Data\AutomationRunLog;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Engine\Data\StepValidationArgs;
use MailPoet\Automation\Engine\Data\SubjectEntry;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Integration\Action;
use MailPoet\Automation\Engine\Integration\Payload;
use MailPoet\Automation\Engine\Integration\Subject;
use MailPoet\Automation\Engine\Registry;
use MailPoet\Automation\Engine\Storage\AutomationRunStorage;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Engine\WordPress;
use Throwable;
class StepHandler {
/** @var SubjectLoader */
private $subjectLoader;
/** @var WordPress */
private $wordPress;
/** @var AutomationRunStorage */
private $automationRunStorage;
/** @var AutomationStorage */
private $automationStorage;
/** @var Registry */
private $registry;
/** @var StepRunControllerFactory */
private $stepRunControllerFactory;
/** @var StepRunLoggerFactory */
private $stepRunLoggerFactory;
/** @var StepScheduler */
private $stepScheduler;
public function __construct(
SubjectLoader $subjectLoader,
WordPress $wordPress,
AutomationRunStorage $automationRunStorage,
AutomationStorage $automationStorage,
Registry $registry,
StepRunControllerFactory $stepRunControllerFactory,
StepRunLoggerFactory $stepRunLoggerFactory,
StepScheduler $stepScheduler
) {
$this->subjectLoader = $subjectLoader;
$this->wordPress = $wordPress;
$this->automationRunStorage = $automationRunStorage;
$this->automationStorage = $automationStorage;
$this->registry = $registry;
$this->stepRunControllerFactory = $stepRunControllerFactory;
$this->stepRunLoggerFactory = $stepRunLoggerFactory;
$this->stepScheduler = $stepScheduler;
}
public function initialize(): void {
$this->wordPress->addAction(Hooks::AUTOMATION_STEP, [$this, 'handle']);
}
/** @param mixed $args */
public function handle($args): void {
// TODO: better args validation
if (!is_array($args) || !isset($args['automation_run_id']) || !array_key_exists('step_id', $args)) {
throw new InvalidStateException();
}
$runId = (int)$args['automation_run_id'];
$stepId = (string)$args['step_id'];
$runNumber = (int)($args['run_number'] ?? 1);
// BC — complete automation run if "step_id" is empty (was nullable in the past)
if (!$stepId) {
$this->automationRunStorage->updateStatus($runId, AutomationRun::STATUS_COMPLETE);
return;
}
$logger = $this->stepRunLoggerFactory->createLogger($runId, $stepId, AutomationRunLog::TYPE_ACTION, $runNumber);
$logger->logStart();
try {
$this->handleStep($runId, $stepId, $runNumber, $logger);
} catch (Throwable $e) {
$status = $e instanceof InvalidStateException && $e->getErrorCode() === 'mailpoet_automation_not_active'
? AutomationRun::STATUS_CANCELLED
: AutomationRun::STATUS_FAILED;
$this->automationRunStorage->updateStatus((int)$args['automation_run_id'], $status);
$logger->logFailure($e);
// Action Scheduler catches only Exception instances, not other errors.
// We need to convert them to exceptions to be processed and logged.
if (!$e instanceof Exception) {
throw new Exception($e->getMessage(), intval($e->getCode()), $e);
}
throw $e;
} finally {
$this->postProcessAutomationRun($runId);
}
}
private function handleStep(int $runId, string $stepId, int $runNumber, StepRunLogger $logger): void {
$automationRun = $this->automationRunStorage->getAutomationRun($runId);
if (!$automationRun) {
throw Exceptions::automationRunNotFound($runId);
}
if ($automationRun->getStatus() !== AutomationRun::STATUS_RUNNING) {
throw Exceptions::automationRunNotRunning($runId, $automationRun->getStatus());
}
$automation = $this->automationStorage->getAutomation($automationRun->getAutomationId(), $automationRun->getVersionId());
if (!$automation) {
throw Exceptions::automationVersionNotFound($automationRun->getAutomationId(), $automationRun->getVersionId());
}
if (!in_array($automation->getStatus(), [Automation::STATUS_ACTIVE, Automation::STATUS_DEACTIVATING], true)) {
throw Exceptions::automationNotActive($automationRun->getAutomationId());
}
$stepData = $automation->getStep($stepId);
if (!$stepData) {
throw Exceptions::automationStepNotFound($stepId);
}
$logger->logStepData($stepData);
$step = $this->registry->getStep($stepData->getKey());
if (!$step instanceof Action) {
throw new InvalidStateException();
}
$requiredSubjects = $step->getSubjectKeys();
$subjectEntries = $this->getSubjectEntries($automationRun, $requiredSubjects);
$args = new StepRunArgs($automation, $automationRun, $stepData, $subjectEntries, $runNumber);
$validationArgs = new StepValidationArgs($automation, $stepData, array_map(function (SubjectEntry $entry) {
return $entry->getSubject();
}, $subjectEntries));
$step->validate($validationArgs);
$step->run($args, $this->stepRunControllerFactory->createController($args, $logger));
// check if run is not completed by now (e.g., one of if/else branches is empty)
$automationRun = $this->automationRunStorage->getAutomationRun($runId);
if ($automationRun && $automationRun->getStatus() !== AutomationRun::STATUS_RUNNING) {
$logger->logSuccess();
return;
}
// schedule next step if not scheduled by action
if (!$this->stepScheduler->hasScheduledStep($args)) {
$this->stepScheduler->scheduleNextStep($args);
}
// logging
if ($this->stepScheduler->hasScheduledProgress($args)) {
$logger->logProgress();
} else {
$logger->logSuccess();
}
}
/** @return SubjectEntry<Subject<Payload>>[] */
private function getSubjectEntries(AutomationRun $automationRun, array $requiredSubjectKeys): array {
$subjectDataMap = [];
foreach ($automationRun->getSubjects() as $data) {
$subjectDataMap[$data->getKey()] = array_merge($subjectDataMap[$data->getKey()] ?? [], [$data]);
}
$subjectEntries = [];
foreach ($requiredSubjectKeys as $key) {
$subjectData = $subjectDataMap[$key] ?? null;
if (!$subjectData) {
throw Exceptions::subjectDataNotFound($key, $automationRun->getId());
}
}
foreach ($subjectDataMap as $subjectData) {
foreach ($subjectData as $data) {
$subjectEntries[] = $this->subjectLoader->getSubjectEntry($data);
}
}
return $subjectEntries;
}
private function postProcessAutomationRun(int $automationRunId): void {
$automationRun = $this->automationRunStorage->getAutomationRun($automationRunId);
if (!$automationRun) {
return;
}
$automation = $this->automationStorage->getAutomation($automationRun->getAutomationId());
if (!$automation) {
return;
}
$this->postProcessAutomation($automation);
}
private function postProcessAutomation(Automation $automation): void {
if ($automation->getStatus() === Automation::STATUS_DEACTIVATING) {
$activeRuns = $this->automationRunStorage->getCountForAutomation($automation, AutomationRun::STATUS_RUNNING);
// Set a deactivating Automation to draft once all automation runs are finished.
if ($activeRuns === 0) {
$automation->setStatus(Automation::STATUS_DRAFT);
$this->automationStorage->updateAutomation($automation);
}
}
}
}
@@ -0,0 +1,50 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Control\StepRunLogger;
use MailPoet\Automation\Engine\Data\StepRunArgs;
class StepRunController {
/** @var StepScheduler */
private $stepScheduler;
/** @var StepRunArgs */
private $stepRunArgs;
/** @var StepRunLogger */
private $stepRunLogger;
public function __construct(
StepScheduler $stepScheduler,
StepRunArgs $stepRunArgs,
StepRunLogger $stepRunLogger
) {
$this->stepScheduler = $stepScheduler;
$this->stepRunArgs = $stepRunArgs;
$this->stepRunLogger = $stepRunLogger;
}
public function scheduleProgress(int $timestamp = null): int {
return $this->stepScheduler->scheduleProgress($this->stepRunArgs, $timestamp);
}
public function scheduleNextStep(int $timestamp = null): int {
return $this->stepScheduler->scheduleNextStep($this->stepRunArgs, $timestamp);
}
public function scheduleNextStepByIndex(int $nextStepIndex, int $timestamp = null): int {
return $this->stepScheduler->scheduleNextStepByIndex($this->stepRunArgs, $nextStepIndex, $timestamp);
}
public function hasScheduledNextStep(): bool {
return $this->stepScheduler->hasScheduledNextStep($this->stepRunArgs);
}
public function getRunLog(): StepRunLogger {
return $this->stepRunLogger;
}
}
@@ -0,0 +1,24 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Control\StepRunLogger;
use MailPoet\Automation\Engine\Data\StepRunArgs;
class StepRunControllerFactory {
/** @var StepScheduler */
private $stepScheduler;
public function __construct(
StepScheduler $stepScheduler
) {
$this->stepScheduler = $stepScheduler;
}
public function createController(StepRunArgs $args, StepRunLogger $logger): StepRunController {
return new StepRunController($this->stepScheduler, $args, $logger);
}
}
@@ -0,0 +1,144 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use DateTimeImmutable;
use MailPoet\Automation\Engine\Data\AutomationRunLog;
use MailPoet\Automation\Engine\Data\Step;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Storage\AutomationRunLogStorage;
use MailPoet\InvalidStateException;
use Throwable;
class StepRunLogger {
/** @var AutomationRunLogStorage */
private $automationRunLogStorage;
/** @var Hooks */
private $hooks;
/** @var int */
private $runId;
/** @var string */
private $stepId;
/** @var AutomationRunLog|null */
private $log;
/** @var string */
private $stepType;
/** @var int */
private $runNumber;
/** @var bool */
private $isWpDebug;
public function __construct(
AutomationRunLogStorage $automationRunLogStorage,
Hooks $hooks,
int $runId,
string $stepId,
string $stepType,
int $runNumber,
bool $isWpDebug = null
) {
$this->automationRunLogStorage = $automationRunLogStorage;
$this->hooks = $hooks;
$this->runId = $runId;
$this->stepId = $stepId;
$this->stepType = $stepType;
$this->runNumber = $runNumber;
$this->isWpDebug = $isWpDebug !== null ? $isWpDebug : $this->getWpDebug();
}
private function getWpDebug(): bool {
if (!defined('WP_DEBUG')) {
return false;
}
if (!is_bool(WP_DEBUG)) {
return in_array(strtolower((string)WP_DEBUG), ['true', '1'], true);
}
return WP_DEBUG;
}
public function logStart(): void {
$log = $this->getLog();
$log->setStatus(AutomationRunLog::STATUS_RUNNING);
$log->setUpdatedAt(new DateTimeImmutable());
$this->automationRunLogStorage->updateAutomationRunLog($log);
}
public function logStepData(Step $step): void {
$log = $this->getLog();
$log->setStepKey($step->getKey());
$this->automationRunLogStorage->updateAutomationRunLog($log);
}
public function logProgress(): void {
$log = $this->getLog();
$log->setStatus(AutomationRunLog::STATUS_RUNNING);
$log->setUpdatedAt(new DateTimeImmutable());
$this->automationRunLogStorage->updateAutomationRunLog($log);
}
public function logSuccess(): void {
$log = $this->getLog();
$log->setStatus(AutomationRunLog::STATUS_COMPLETE);
$log->setUpdatedAt(new DateTimeImmutable());
$this->triggerAfterRunHook($log);
$this->automationRunLogStorage->updateAutomationRunLog($log);
}
public function logFailure(Throwable $error): void {
$log = $this->getLog();
$log->setStatus(AutomationRunLog::STATUS_FAILED);
$log->setError($error);
$log->setUpdatedAt(new DateTimeImmutable());
$this->triggerAfterRunHook($log);
$this->automationRunLogStorage->updateAutomationRunLog($log);
}
public function saveLogData(array $data): void {
$log = $this->getLog();
foreach ($data as $key => $value) {
$log->setData($key, $value);
}
$this->automationRunLogStorage->updateAutomationRunLog($log);
}
public function getLog(): AutomationRunLog {
if (!$this->log) {
$this->log = $this->automationRunLogStorage->getAutomationRunLogByRunAndStepId($this->runId, $this->stepId);
}
if (!$this->log) {
$log = new AutomationRunLog($this->runId, $this->stepId, $this->stepType);
$log->setRunNumber($this->runNumber);
$id = $this->automationRunLogStorage->createAutomationRunLog($log);
$this->log = $this->automationRunLogStorage->getAutomationRunLog($id);
}
if (!$this->log) {
throw new InvalidStateException('Failed to create automation run log');
}
$this->log->setRunNumber($this->runNumber);
return $this->log;
}
private function triggerAfterRunHook(AutomationRunLog $log): void {
try {
$this->hooks->doAutomationStepAfterRun($log);
} catch (Throwable $e) {
if ($this->isWpDebug) {
throw $e;
}
// ignore integration logging errors
}
}
}
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Storage\AutomationRunLogStorage;
class StepRunLoggerFactory {
/** @var AutomationRunLogStorage */
private $automationRunLogStorage;
/** @var Hooks */
private $hooks;
public function __construct(
AutomationRunLogStorage $automationRunLogStorage,
Hooks $hooks
) {
$this->automationRunLogStorage = $automationRunLogStorage;
$this->hooks = $hooks;
}
public function createLogger(int $runId, string $stepId, string $stepType, int $runNumber): StepRunLogger {
return new StepRunLogger($this->automationRunLogStorage, $this->hooks, $runId, $stepId, $stepType, $runNumber);
}
}
@@ -0,0 +1,122 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\AutomationRun;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Storage\AutomationRunStorage;
class StepScheduler {
/** @var ActionScheduler */
private $actionScheduler;
/** @var AutomationRunStorage */
private $automationRunStorage;
public function __construct(
ActionScheduler $actionScheduler,
AutomationRunStorage $automationRunStorage
) {
$this->actionScheduler = $actionScheduler;
$this->automationRunStorage = $automationRunStorage;
}
public function scheduleProgress(StepRunArgs $args, int $timestamp = null): int {
$runId = $args->getAutomationRun()->getId();
$data = $this->getActionData($runId, $args->getStep()->getId(), $args->getRunNumber() + 1);
return $this->scheduleStepAction($data, $timestamp);
}
public function scheduleNextStep(StepRunArgs $args, int $timestamp = null): int {
$step = $args->getStep();
$nextSteps = $step->getNextSteps();
// complete the automation run if there are no more steps
if (count($nextSteps) === 0) {
$this->completeAutomationRun($args);
return 0;
}
if (count($nextSteps) > 1) {
throw Exceptions::nextStepNotScheduled($step->getId());
}
return $this->scheduleNextStepByIndex($args, 0, $timestamp);
}
public function scheduleNextStepByIndex(StepRunArgs $args, int $nextStepIndex, int $timestamp = null): int {
$step = $args->getStep();
$nextStep = $step->getNextSteps()[$nextStepIndex] ?? null;
if (!$nextStep) {
throw Exceptions::nextStepNotFound($step->getId(), $nextStepIndex);
}
$runId = $args->getAutomationRun()->getId();
$nextStepId = $nextStep->getId();
if (!$nextStepId) {
$this->completeAutomationRun($args);
return 0;
}
$data = $this->getActionData($runId, $nextStepId);
$id = $this->scheduleStepAction($data, $timestamp);
$this->automationRunStorage->updateNextStep($runId, $nextStepId);
return $id;
}
public function hasScheduledNextStep(StepRunArgs $args): bool {
$runId = $args->getAutomationRun()->getId();
foreach ($args->getStep()->getNextStepIds() as $nextStepId) {
$data = $this->getActionData($runId, $nextStepId);
$hasStep = $this->actionScheduler->hasScheduledAction(Hooks::AUTOMATION_STEP, $data);
if ($hasStep) {
return true;
}
// BC for old steps without run number
unset($data[0]['run_number']);
$hasStep = $this->actionScheduler->hasScheduledAction(Hooks::AUTOMATION_STEP, $data);
if ($hasStep) {
return true;
}
}
return false;
}
public function hasScheduledProgress(StepRunArgs $args): bool {
$runId = $args->getAutomationRun()->getId();
$data = $this->getActionData($runId, $args->getStep()->getId(), $args->getRunNumber() + 1);
return $this->actionScheduler->hasScheduledAction(Hooks::AUTOMATION_STEP, $data);
}
public function hasScheduledStep(StepRunArgs $args): bool {
return $this->hasScheduledNextStep($args) || $this->hasScheduledProgress($args);
}
private function scheduleStepAction(array $data, int $timestamp = null): int {
return $timestamp === null
? $this->actionScheduler->enqueue(Hooks::AUTOMATION_STEP, $data)
: $this->actionScheduler->schedule($timestamp, Hooks::AUTOMATION_STEP, $data);
}
private function completeAutomationRun(StepRunArgs $args): void {
$runId = $args->getAutomationRun()->getId();
$this->automationRunStorage->updateNextStep($runId, null);
$this->automationRunStorage->updateStatus($runId, AutomationRun::STATUS_COMPLETE);
}
private function getActionData(int $runId, string $stepId, int $runNumber = 1): array {
return [
[
'automation_run_id' => $runId,
'step_id' => $stepId,
'run_number' => $runNumber,
],
];
}
}
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
use MailPoet\Automation\Engine\Data\SubjectEntry;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Integration\Payload;
use MailPoet\Automation\Engine\Integration\Subject;
use MailPoet\Automation\Engine\Registry;
class SubjectLoader {
/** @var Registry */
private $registry;
public function __construct(
Registry $registry
) {
$this->registry = $registry;
}
/**
* @param SubjectData[] $subjectData
* @return SubjectEntry<Subject<Payload>>[]
*/
public function getSubjectsEntries(array $subjectData): array {
$subjectEntries = [];
foreach ($subjectData as $data) {
$subjectEntries[] = $this->getSubjectEntry($data);
}
return $subjectEntries;
}
/**
* @param SubjectData $subjectData
* @return SubjectEntry<Subject<Payload>>
*/
public function getSubjectEntry(SubjectData $subjectData): SubjectEntry {
$key = $subjectData->getKey();
$subject = $this->registry->getSubject($key);
if (!$subject) {
throw Exceptions::subjectNotFound($key);
}
return new SubjectEntry($subject, $subjectData);
}
}
@@ -0,0 +1,103 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
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\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 getSubjectKeysForAutomation(Automation $automation): array {
$triggerData = array_values(array_filter(
$automation->getSteps(),
function (StepData $step): bool {
return $step->getType() === StepData::TYPE_TRIGGER;
}
));
$triggers = array_filter(array_map(
function (StepData $step): ?Trigger {
return $this->registry->getTrigger($step->getKey());
},
$triggerData
));
$all = [];
foreach ($triggers as $trigger) {
$all[] = $this->getSubjectKeysForTrigger($trigger);
}
$all = count($all) > 1 ? array_intersect(...$all) : $all[0] ?? [];
return array_values(array_unique($all));
}
public function getSubjectKeysForTrigger(Trigger $trigger): array {
$transformerMap = $this->getTransformerMap();
$all = $trigger->getSubjectKeys();
$queue = $all;
while ($key = array_shift($queue)) {
foreach ($transformerMap[$key] ?? [] as $transformer) {
$newKey = $transformer->returns();
if (!in_array($newKey, $all, true)) {
$all[] = $newKey;
$queue[] = $newKey;
}
}
}
sort($all);
return $all;
}
/**
* @param Subject[] $subjects
* @return Subject[]
*/
public function getAllSubjects(array $subjects): array {
$transformerMap = $this->getTransformerMap();
$all = [];
foreach ($subjects as $subject) {
$all[$subject->getKey()] = $subject;
}
$queue = array_keys($all);
while ($key = array_shift($queue)) {
foreach ($transformerMap[$key] ?? [] as $transformer) {
$newKey = $transformer->returns();
if (!isset($all[$newKey])) {
$newSubject = $transformer->transform($all[$key]);
if (!$newSubject) {
continue;
}
$all[$newKey] = $newSubject;
$queue[] = $newKey;
}
}
}
return array_values($all);
}
/**
* @return SubjectTransformer[][]
*/
private function getTransformerMap(): array {
$transformerMap = [];
foreach ($this->registry->getSubjectTransformers() as $transformer) {
$transformerMap[$transformer->accepts()] = array_merge($transformerMap[$transformer->accepts()] ?? [], [$transformer]);
}
return $transformerMap;
}
}
@@ -0,0 +1,119 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Control;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\AutomationRun;
use MailPoet\Automation\Engine\Data\AutomationRunLog;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Engine\Data\Subject;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Integration\Trigger;
use MailPoet\Automation\Engine\Storage\AutomationRunStorage;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Engine\WordPress;
class TriggerHandler {
/** @var AutomationStorage */
private $automationStorage;
/** @var AutomationRunStorage */
private $automationRunStorage;
/** @var SubjectLoader */
private $subjectLoader;
/** @var SubjectTransformerHandler */
private $subjectTransformerHandler;
/** @var FilterHandler */
private $filterHandler;
/** @var StepScheduler */
private $stepScheduler;
/** @var StepRunLoggerFactory */
private $stepRunLoggerFactory;
/** @var WordPress */
private $wordPress;
public function __construct(
AutomationStorage $automationStorage,
AutomationRunStorage $automationRunStorage,
SubjectLoader $subjectLoader,
SubjectTransformerHandler $subjectTransformerHandler,
FilterHandler $filterHandler,
StepScheduler $stepScheduler,
StepRunLoggerFactory $stepRunLoggerFactory,
WordPress $wordPress
) {
$this->automationStorage = $automationStorage;
$this->automationRunStorage = $automationRunStorage;
$this->subjectLoader = $subjectLoader;
$this->subjectTransformerHandler = $subjectTransformerHandler;
$this->filterHandler = $filterHandler;
$this->stepScheduler = $stepScheduler;
$this->stepRunLoggerFactory = $stepRunLoggerFactory;
$this->wordPress = $wordPress;
}
public function initialize(): void {
$this->wordPress->addAction(Hooks::TRIGGER, [$this, 'processTrigger'], 10, 2);
}
/** @param Subject[] $subjects */
public function processTrigger(Trigger $trigger, array $subjects): void {
$automations = $this->automationStorage->getActiveAutomationsByTrigger($trigger);
if (!$automations) {
return;
}
// expand all subject transformations and load subject entries
$subjects = $this->subjectTransformerHandler->getAllSubjects($subjects);
$subjectEntries = $this->subjectLoader->getSubjectsEntries($subjects);
foreach ($automations as $automation) {
$step = $automation->getTrigger($trigger->getKey());
if (!$step) {
throw Exceptions::automationTriggerNotFound($automation->getId(), $trigger->getKey());
}
$automationRun = new AutomationRun($automation->getId(), $automation->getVersionId(), $trigger->getKey(), $subjects);
$stepRunArgs = new StepRunArgs($automation, $automationRun, $step, $subjectEntries, 1);
$match = false;
try {
$match = $this->filterHandler->matchesFilters($stepRunArgs);
} catch (Exceptions\Exception $e) {
// failed filter evaluation won't match
;
}
if (!$match) {
continue;
}
$createAutomationRun = $trigger->isTriggeredBy($stepRunArgs);
$createAutomationRun = $this->wordPress->applyFilters(
Hooks::AUTOMATION_RUN_CREATE,
$createAutomationRun,
$stepRunArgs
);
if (!$createAutomationRun) {
continue;
}
$automationRunId = $this->automationRunStorage->createAutomationRun($automationRun);
$automationRun->setId($automationRunId);
$this->stepScheduler->scheduleNextStep($stepRunArgs);
$logger = $this->stepRunLoggerFactory->createLogger($automationRunId, $step->getId(), AutomationRunLog::TYPE_TRIGGER, 1);
$logger->logStepData($step);
$logger->logSuccess();
}
}
}
@@ -0,0 +1 @@
<?php