init
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
+24
@@ -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);
|
||||
}
|
||||
}
|
||||
+103
@@ -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
|
||||
Reference in New Issue
Block a user