init
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\API;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\API\REST\API as MailPoetApi;
|
||||
use MailPoet\Automation\Engine\Hooks;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
|
||||
class API extends MailPoetApi {
|
||||
/** @var MailPoetApi */
|
||||
private $api;
|
||||
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
public function __construct(
|
||||
MailPoetApi $api,
|
||||
WordPress $wordPress
|
||||
) {
|
||||
$this->api = $api;
|
||||
$this->wordPress = $wordPress;
|
||||
}
|
||||
|
||||
public function initialize(): void {
|
||||
$this->wordPress->addAction(MailPoetApi::REST_API_INIT_ACTION, function () {
|
||||
$this->wordPress->doAction(Hooks::API_INITIALIZE, [$this]);
|
||||
});
|
||||
}
|
||||
|
||||
public function registerGetRoute(string $route, string $endpoint): void {
|
||||
$this->api->registerGetRoute($route, $endpoint);
|
||||
}
|
||||
|
||||
public function registerPostRoute(string $route, string $endpoint): void {
|
||||
$this->api->registerPostRoute($route, $endpoint);
|
||||
}
|
||||
|
||||
public function registerPutRoute(string $route, string $endpoint): void {
|
||||
$this->api->registerPutRoute($route, $endpoint);
|
||||
}
|
||||
|
||||
public function registerPatchRoute(string $route, string $endpoint): void {
|
||||
$this->api->registerPatchRoute($route, $endpoint);
|
||||
}
|
||||
|
||||
public function registerDeleteRoute(string $route, string $endpoint): void {
|
||||
$this->api->registerDeleteRoute($route, $endpoint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\API;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\API\REST\Endpoint as MailPoetEndpoint;
|
||||
use MailPoet\Automation\Engine\Engine;
|
||||
|
||||
abstract class Endpoint extends MailPoetEndpoint {
|
||||
public function checkPermissions(): bool {
|
||||
return current_user_can(Engine::CAPABILITY_MANAGE_AUTOMATIONS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Builder;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationValidator;
|
||||
|
||||
class CreateAutomationFromTemplateController {
|
||||
/** @var AutomationStorage */
|
||||
private $storage;
|
||||
|
||||
/** @var AutomationValidator */
|
||||
private $automationValidator;
|
||||
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
public function __construct(
|
||||
AutomationStorage $storage,
|
||||
AutomationValidator $automationValidator,
|
||||
Registry $registry
|
||||
) {
|
||||
$this->storage = $storage;
|
||||
$this->automationValidator = $automationValidator;
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function createAutomation(string $slug): Automation {
|
||||
$template = $this->registry->getTemplate($slug);
|
||||
if (!$template) {
|
||||
throw Exceptions::automationTemplateNotFound($slug);
|
||||
}
|
||||
|
||||
$automation = $template->createAutomation();
|
||||
$this->automationValidator->validate($automation);
|
||||
$automationId = $this->storage->createAutomation($automation);
|
||||
$savedAutomation = $this->storage->getAutomation($automationId);
|
||||
if (!$savedAutomation) {
|
||||
throw new InvalidStateException('Automation not found.');
|
||||
}
|
||||
return $savedAutomation;
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Builder;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
|
||||
class DeleteAutomationController {
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
public function __construct(
|
||||
AutomationStorage $automationStorage
|
||||
) {
|
||||
$this->automationStorage = $automationStorage;
|
||||
}
|
||||
|
||||
public function deleteAutomation(int $id): Automation {
|
||||
$automation = $this->automationStorage->getAutomation($id);
|
||||
if (!$automation) {
|
||||
throw Exceptions::automationNotFound($id);
|
||||
}
|
||||
|
||||
if ($automation->getStatus() !== Automation::STATUS_TRASH) {
|
||||
throw Exceptions::automationNotTrashed($id);
|
||||
}
|
||||
|
||||
$this->automationStorage->deleteAutomation($automation);
|
||||
return $automation;
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Builder;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\NextStep;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Util\Security;
|
||||
|
||||
class DuplicateAutomationController {
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wordPress,
|
||||
AutomationStorage $automationStorage
|
||||
) {
|
||||
$this->wordPress = $wordPress;
|
||||
$this->automationStorage = $automationStorage;
|
||||
}
|
||||
|
||||
public function duplicateAutomation(int $id): Automation {
|
||||
$automation = $this->automationStorage->getAutomation($id);
|
||||
if (!$automation) {
|
||||
throw Exceptions::automationNotFound($id);
|
||||
}
|
||||
|
||||
$duplicate = new Automation(
|
||||
$this->getName($automation->getName()),
|
||||
$this->getSteps($automation->getSteps()),
|
||||
$this->wordPress->wpGetCurrentUser()
|
||||
);
|
||||
$duplicate->setStatus(Automation::STATUS_DRAFT);
|
||||
|
||||
$automationId = $this->automationStorage->createAutomation($duplicate);
|
||||
$savedAutomation = $this->automationStorage->getAutomation($automationId);
|
||||
if (!$savedAutomation) {
|
||||
throw new InvalidStateException('Automation not found.');
|
||||
}
|
||||
return $savedAutomation;
|
||||
}
|
||||
|
||||
private function getName(string $name): string {
|
||||
// translators: %s is the original automation name.
|
||||
$newName = sprintf(__('Copy of %s', 'mailpoet'), $name);
|
||||
$maxLength = $this->automationStorage->getNameColumnLength();
|
||||
if (strlen($newName) > $maxLength) {
|
||||
$append = '…';
|
||||
return substr($newName, 0, $maxLength - strlen($append)) . $append;
|
||||
}
|
||||
return $newName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Step[] $steps
|
||||
* @return Step[]
|
||||
*/
|
||||
private function getSteps(array $steps): array {
|
||||
$newIds = [];
|
||||
foreach ($steps as $step) {
|
||||
$id = $step->getId();
|
||||
$newIds[$id] = $id === 'root' ? 'root' : $this->getId();
|
||||
}
|
||||
|
||||
$newSteps = [];
|
||||
foreach ($steps as $step) {
|
||||
$newId = $newIds[$step->getId()];
|
||||
$newSteps[$newId] = new Step(
|
||||
$newId,
|
||||
$step->getType(),
|
||||
$step->getKey(),
|
||||
$step->getArgs(),
|
||||
array_map(function (NextStep $nextStep) use ($newIds): NextStep {
|
||||
$nextStepId = $nextStep->getId();
|
||||
return new NextStep($nextStepId ? $newIds[$nextStepId] : null);
|
||||
}, $step->getNextSteps())
|
||||
);
|
||||
}
|
||||
return $newSteps;
|
||||
}
|
||||
|
||||
private function getId(): string {
|
||||
return Security::generateRandomString(16);
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Builder;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use ActionScheduler_Store;
|
||||
use MailPoet\Automation\Engine\Control\ActionScheduler;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\AutomationRun;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException;
|
||||
use MailPoet\Automation\Engine\Hooks;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationRunStorage;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStatisticsStorage;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationValidator;
|
||||
|
||||
class UpdateAutomationController {
|
||||
/** @var Hooks */
|
||||
private $hooks;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $storage;
|
||||
|
||||
/** @var AutomationStatisticsStorage */
|
||||
private $statisticsStorage;
|
||||
|
||||
/** @var AutomationValidator */
|
||||
private $automationValidator;
|
||||
|
||||
/** @var UpdateStepsController */
|
||||
private $updateStepsController;
|
||||
|
||||
private AutomationRunStorage $automationRunStorage;
|
||||
|
||||
private ActionScheduler $actionScheduler;
|
||||
|
||||
public function __construct(
|
||||
Hooks $hooks,
|
||||
AutomationStorage $storage,
|
||||
AutomationStatisticsStorage $statisticsStorage,
|
||||
AutomationValidator $automationValidator,
|
||||
AutomationRunStorage $automationRunStorage,
|
||||
ActionScheduler $actionScheduler,
|
||||
UpdateStepsController $updateStepsController
|
||||
) {
|
||||
$this->hooks = $hooks;
|
||||
$this->storage = $storage;
|
||||
$this->statisticsStorage = $statisticsStorage;
|
||||
$this->automationValidator = $automationValidator;
|
||||
$this->updateStepsController = $updateStepsController;
|
||||
$this->automationRunStorage = $automationRunStorage;
|
||||
$this->actionScheduler = $actionScheduler;
|
||||
}
|
||||
|
||||
public function updateAutomation(int $id, array $data): Automation {
|
||||
$automation = $this->storage->getAutomation($id);
|
||||
if (!$automation) {
|
||||
throw Exceptions::automationNotFound($id);
|
||||
}
|
||||
$this->validateIfAutomationCanBeUpdated($automation, $data);
|
||||
|
||||
if (array_key_exists('name', $data)) {
|
||||
$automation->setName($data['name']);
|
||||
}
|
||||
$originalStatus = $automation->getStatus();
|
||||
|
||||
if (array_key_exists('status', $data)) {
|
||||
$this->checkAutomationStatus($data['status']);
|
||||
$automation->setStatus($data['status']);
|
||||
}
|
||||
|
||||
if (array_key_exists('steps', $data)) {
|
||||
$this->validateAutomationSteps($automation, $data['steps']);
|
||||
$this->updateStepsController->updateSteps($automation, $data['steps']);
|
||||
foreach ($automation->getSteps() as $step) {
|
||||
$this->hooks->doAutomationStepBeforeSave($step, $automation);
|
||||
$this->hooks->doAutomationStepByKeyBeforeSave($step, $automation);
|
||||
}
|
||||
}
|
||||
|
||||
if (($automation->getStatus() === Automation::STATUS_DRAFT) && ($originalStatus === Automation::STATUS_ACTIVE)) {
|
||||
$this->unscheduleAutomationRuns($automation);
|
||||
}
|
||||
|
||||
if (array_key_exists('meta', $data)) {
|
||||
$automation->deleteAllMetas();
|
||||
foreach ($data['meta'] as $key => $value) {
|
||||
$automation->setMeta($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$this->hooks->doAutomationBeforeSave($automation);
|
||||
|
||||
$this->automationValidator->validate($automation);
|
||||
$this->storage->updateAutomation($automation);
|
||||
|
||||
$automation = $this->storage->getAutomation($id);
|
||||
if (!$automation) {
|
||||
throw Exceptions::automationNotFound($id);
|
||||
}
|
||||
return $automation;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a temporary validation, see MAILPOET-4744
|
||||
*/
|
||||
private function validateIfAutomationCanBeUpdated(Automation $automation, array $data): void {
|
||||
|
||||
if (
|
||||
!in_array(
|
||||
$automation->getStatus(),
|
||||
[
|
||||
Automation::STATUS_ACTIVE,
|
||||
Automation::STATUS_DEACTIVATING,
|
||||
],
|
||||
true
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statistics = $this->statisticsStorage->getAutomationStats($automation->getId());
|
||||
if ($statistics->getInProgress() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($data['status']) || $data['status'] === $automation->getStatus()) {
|
||||
throw Exceptions::automationHasActiveRuns($automation->getId());
|
||||
}
|
||||
}
|
||||
|
||||
private function checkAutomationStatus(string $status): void {
|
||||
if (!in_array($status, Automation::STATUS_ALL, true)) {
|
||||
// translators: %s is the status.
|
||||
throw UnexpectedValueException::create()->withMessage(sprintf(__('Invalid status: %s', 'mailpoet'), $status));
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateAutomationSteps(Automation $automation, array $steps): void {
|
||||
$existingSteps = $automation->getSteps();
|
||||
if (count($steps) !== count($existingSteps)) {
|
||||
throw Exceptions::automationStructureModificationNotSupported();
|
||||
}
|
||||
|
||||
foreach ($steps as $id => $data) {
|
||||
$existingStep = $existingSteps[$id] ?? null;
|
||||
if (!$existingStep || !$this->stepChanged(Step::fromArray($data), $existingStep)) {
|
||||
throw Exceptions::automationStructureModificationNotSupported();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function stepChanged(Step $a, Step $b): bool {
|
||||
$aData = $a->toArray();
|
||||
$bData = $b->toArray();
|
||||
unset($aData['args']);
|
||||
unset($bData['args']);
|
||||
return $aData === $bData;
|
||||
}
|
||||
|
||||
private function unscheduleAutomationRuns(Automation $automation): void {
|
||||
$runIds = [];
|
||||
$runs = $this->automationRunStorage->getAutomationRunsForAutomation($automation);
|
||||
foreach ($runs as $run) {
|
||||
if ($run->getStatus() === AutomationRun::STATUS_RUNNING) {
|
||||
$this->automationRunStorage->updateStatus($run->getId(), AutomationRun::STATUS_CANCELLED);
|
||||
}
|
||||
$runIds[$run->getId()] = $run;
|
||||
}
|
||||
|
||||
$actions = $this->actionScheduler->getScheduledActions(['hook' => Hooks::AUTOMATION_STEP, 'status' => ActionScheduler_Store::STATUS_PENDING]);
|
||||
foreach ($actions as $action) {
|
||||
$args = $action->get_args();
|
||||
$automationArgs = reset($args);
|
||||
if (isset($automationArgs['automation_run_id']) && isset($runIds[$automationArgs['automation_run_id']])) {
|
||||
$this->actionScheduler->unscheduleAction(Hooks::AUTOMATION_STEP, $args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Builder;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
|
||||
class UpdateStepsController {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function updateSteps(Automation $automation, array $data): Automation {
|
||||
$steps = [];
|
||||
foreach ($data as $index => $stepData) {
|
||||
$step = $this->processStep($stepData, $automation->getStep($stepData['id']));
|
||||
$steps[$index] = $step;
|
||||
}
|
||||
$automation->setSteps($steps);
|
||||
return $automation;
|
||||
}
|
||||
|
||||
private function processStep(array $data, ?Step $existingStep): Step {
|
||||
$key = $data['key'];
|
||||
$step = $this->registry->getStep($key);
|
||||
if (!$step && $existingStep && $data !== $existingStep->toArray()) {
|
||||
throw Exceptions::automationStepNotFound($key);
|
||||
}
|
||||
return Step::fromArray($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -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
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Utils\Json;
|
||||
|
||||
class Automation {
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
public const STATUS_DEACTIVATING = 'deactivating';
|
||||
public const STATUS_DRAFT = 'draft';
|
||||
public const STATUS_TRASH = 'trash';
|
||||
public const STATUS_ALL = [
|
||||
self::STATUS_ACTIVE,
|
||||
self::STATUS_DEACTIVATING,
|
||||
self::STATUS_DRAFT,
|
||||
self::STATUS_TRASH,
|
||||
];
|
||||
|
||||
/** @var int|null */
|
||||
private $id;
|
||||
|
||||
/** @var int|null */
|
||||
private $versionId;
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
/** @var \WP_User */
|
||||
private $author;
|
||||
|
||||
/** @var string */
|
||||
private $status = self::STATUS_DRAFT;
|
||||
|
||||
/** @var DateTimeImmutable */
|
||||
private $createdAt;
|
||||
|
||||
/** @var DateTimeImmutable */
|
||||
private $updatedAt;
|
||||
|
||||
/** @var ?DateTimeImmutable */
|
||||
private $activatedAt = null;
|
||||
|
||||
/** @var array<string|int, Step> */
|
||||
private $steps;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
private $meta = [];
|
||||
|
||||
/** @param array<string, Step> $steps */
|
||||
public function __construct(
|
||||
string $name,
|
||||
array $steps,
|
||||
\WP_User $author,
|
||||
int $id = null,
|
||||
int $versionId = null
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->steps = $steps;
|
||||
$this->author = $author;
|
||||
$this->id = $id;
|
||||
$this->versionId = $versionId;
|
||||
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
if ($this->id === null) {
|
||||
throw InvalidStateException::create()->withMessage('No automation ID was set');
|
||||
}
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(int $id): void {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getVersionId(): int {
|
||||
if (!$this->versionId) {
|
||||
throw InvalidStateException::create()->withMessage('No automation version ID was set');
|
||||
}
|
||||
return $this->versionId;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): void {
|
||||
$this->name = $name;
|
||||
$this->setUpdatedAt();
|
||||
}
|
||||
|
||||
public function getStatus(): string {
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(string $status): void {
|
||||
if ($status === self::STATUS_ACTIVE && $this->status !== self::STATUS_ACTIVE) {
|
||||
$this->activatedAt = new DateTimeImmutable();
|
||||
}
|
||||
$this->status = $status;
|
||||
$this->setUpdatedAt();
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable {
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(DateTimeImmutable $createdAt): void {
|
||||
$this->createdAt = $createdAt;
|
||||
}
|
||||
|
||||
public function getAuthor(): \WP_User {
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable {
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function getActivatedAt(): ?DateTimeImmutable {
|
||||
return $this->activatedAt;
|
||||
}
|
||||
|
||||
/** @return array<string|int, Step> */
|
||||
public function getSteps(): array {
|
||||
return $this->steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string|int, Step>
|
||||
*/
|
||||
public function getTriggers(): array {
|
||||
return array_filter(
|
||||
$this->steps,
|
||||
function (Step $step) {
|
||||
return $step->getType() === Step::TYPE_TRIGGER;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string|int, Step> $steps */
|
||||
public function setSteps(array $steps): void {
|
||||
$this->steps = $steps;
|
||||
$this->setUpdatedAt();
|
||||
}
|
||||
|
||||
public function getStep(string $id): ?Step {
|
||||
return $this->steps[$id] ?? null;
|
||||
}
|
||||
|
||||
public function getTrigger(string $key): ?Step {
|
||||
foreach ($this->steps as $step) {
|
||||
if ($step->getType() === Step::TYPE_TRIGGER && $step->getKey() === $key) {
|
||||
return $step;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function equals(Automation $compare): bool {
|
||||
$compareArray = $compare->toArray();
|
||||
$currentArray = $this->toArray();
|
||||
$ignoreValues = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
foreach ($ignoreValues as $ignore) {
|
||||
unset($compareArray[$ignore]);
|
||||
unset($currentArray[$ignore]);
|
||||
}
|
||||
return $compareArray === $currentArray;
|
||||
}
|
||||
|
||||
public function needsFullValidation(): bool {
|
||||
return in_array($this->status, [Automation::STATUS_ACTIVE, Automation::STATUS_DEACTIVATING], true);
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'status' => $this->status,
|
||||
'author' => $this->author->ID,
|
||||
'created_at' => $this->createdAt->format(DateTimeImmutable::W3C),
|
||||
'updated_at' => $this->updatedAt->format(DateTimeImmutable::W3C),
|
||||
'activated_at' => $this->activatedAt ? $this->activatedAt->format(DateTimeImmutable::W3C) : null,
|
||||
'steps' => Json::encode(
|
||||
array_map(function (Step $step) {
|
||||
return $step->toArray();
|
||||
}, $this->steps)
|
||||
),
|
||||
'meta' => Json::encode($this->meta),
|
||||
];
|
||||
}
|
||||
|
||||
private function setUpdatedAt(): void {
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getMeta(string $key) {
|
||||
return $this->meta[$key] ?? null;
|
||||
}
|
||||
|
||||
public function getAllMetas(): array {
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function setMeta(string $key, $value): void {
|
||||
$this->meta[$key] = $value;
|
||||
$this->setUpdatedAt();
|
||||
}
|
||||
|
||||
public function deleteMeta(string $key): void {
|
||||
unset($this->meta[$key]);
|
||||
$this->setUpdatedAt();
|
||||
}
|
||||
|
||||
public function deleteAllMetas(): void {
|
||||
$this->meta = [];
|
||||
$this->setUpdatedAt();
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self {
|
||||
// TODO: validation
|
||||
$automation = new self(
|
||||
$data['name'],
|
||||
array_map(function (array $stepData): Step {
|
||||
return Step::fromArray($stepData);
|
||||
}, Json::decode($data['steps'])),
|
||||
new \WP_User((int)$data['author'])
|
||||
);
|
||||
$automation->id = (int)$data['id'];
|
||||
$automation->versionId = (int)$data['version_id'];
|
||||
$automation->status = $data['status'];
|
||||
$automation->createdAt = new DateTimeImmutable($data['created_at']);
|
||||
$automation->updatedAt = new DateTimeImmutable($data['updated_at']);
|
||||
$automation->activatedAt = $data['activated_at'] !== null ? new DateTimeImmutable($data['activated_at']) : null;
|
||||
|
||||
$automation->meta = $data['meta'] ? Json::decode($data['meta']) : [];
|
||||
return $automation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
class AutomationRun {
|
||||
public const STATUS_RUNNING = 'running';
|
||||
public const STATUS_COMPLETE = 'complete';
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
public const STATUS_FAILED = 'failed';
|
||||
|
||||
/** @var int */
|
||||
private $id;
|
||||
|
||||
/** @var int */
|
||||
private $automationId;
|
||||
|
||||
/** @var int */
|
||||
private $versionId;
|
||||
|
||||
/** @var string */
|
||||
private $triggerKey;
|
||||
|
||||
/** @var string */
|
||||
private $status = self::STATUS_RUNNING;
|
||||
|
||||
/** @var DateTimeImmutable */
|
||||
private $createdAt;
|
||||
|
||||
/** @var DateTimeImmutable */
|
||||
private $updatedAt;
|
||||
|
||||
/** @var Subject[] */
|
||||
private $subjects;
|
||||
|
||||
/**
|
||||
* @param Subject[] $subjects
|
||||
*/
|
||||
public function __construct(
|
||||
int $automationId,
|
||||
int $versionId,
|
||||
string $triggerKey,
|
||||
array $subjects,
|
||||
int $id = null
|
||||
) {
|
||||
$this->automationId = $automationId;
|
||||
$this->versionId = $versionId;
|
||||
$this->triggerKey = $triggerKey;
|
||||
$this->subjects = $subjects;
|
||||
|
||||
if ($id) {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(int $id): void {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getAutomationId(): int {
|
||||
return $this->automationId;
|
||||
}
|
||||
|
||||
public function getVersionId(): int {
|
||||
return $this->versionId;
|
||||
}
|
||||
|
||||
public function getTriggerKey(): string {
|
||||
return $this->triggerKey;
|
||||
}
|
||||
|
||||
public function getStatus(): string {
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable {
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable {
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/** @return Subject[] */
|
||||
public function getSubjects(string $key = null): array {
|
||||
if ($key) {
|
||||
return array_values(
|
||||
array_filter($this->subjects, function (Subject $subject) use ($key) {
|
||||
return $subject->getKey() === $key;
|
||||
})
|
||||
);
|
||||
}
|
||||
return $this->subjects;
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'automation_id' => $this->automationId,
|
||||
'version_id' => $this->versionId,
|
||||
'trigger_key' => $this->triggerKey,
|
||||
'status' => $this->status,
|
||||
'created_at' => $this->createdAt->format(DateTimeImmutable::W3C),
|
||||
'updated_at' => $this->updatedAt->format(DateTimeImmutable::W3C),
|
||||
'subjects' => array_map(function (Subject $subject): array {
|
||||
return $subject->toArray();
|
||||
}, $this->subjects),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self {
|
||||
$automationRun = new AutomationRun(
|
||||
(int)$data['automation_id'],
|
||||
(int)$data['version_id'],
|
||||
$data['trigger_key'],
|
||||
array_map(function (array $subject) {
|
||||
return Subject::fromArray($subject);
|
||||
}, $data['subjects'])
|
||||
);
|
||||
$automationRun->id = (int)$data['id'];
|
||||
$automationRun->status = $data['status'];
|
||||
$automationRun->createdAt = new DateTimeImmutable($data['created_at']);
|
||||
$automationRun->updatedAt = new DateTimeImmutable($data['updated_at']);
|
||||
return $automationRun;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
use InvalidArgumentException;
|
||||
use MailPoet\Automation\Engine\Utils\Json;
|
||||
use Throwable;
|
||||
|
||||
class AutomationRunLog {
|
||||
public const STATUS_RUNNING = 'running';
|
||||
public const STATUS_COMPLETE = 'complete';
|
||||
public const STATUS_FAILED = 'failed';
|
||||
|
||||
public const STATUS_ALL = [
|
||||
self::STATUS_RUNNING,
|
||||
self::STATUS_COMPLETE,
|
||||
self::STATUS_FAILED,
|
||||
];
|
||||
|
||||
public const TYPE_ACTION = 'action';
|
||||
public const TYPE_TRIGGER = 'trigger';
|
||||
|
||||
public const KEY_UNKNOWN = 'unknown';
|
||||
|
||||
/** @var int */
|
||||
private $id;
|
||||
|
||||
/** @var int */
|
||||
private $automationRunId;
|
||||
|
||||
/** @var string */
|
||||
private $stepId;
|
||||
|
||||
/** @var string */
|
||||
private $stepType;
|
||||
|
||||
/** @var string */
|
||||
private $stepKey;
|
||||
|
||||
/** @var string */
|
||||
private $status;
|
||||
|
||||
/** @var DateTimeImmutable */
|
||||
private $startedAt;
|
||||
|
||||
/** @var DateTimeImmutable */
|
||||
private $updatedAt;
|
||||
|
||||
/** @var int */
|
||||
private $runNumber = 1;
|
||||
|
||||
/** @var array */
|
||||
private $data = [];
|
||||
|
||||
/** @var array|null */
|
||||
private $error;
|
||||
|
||||
public function __construct(
|
||||
int $automationRunId,
|
||||
string $stepId,
|
||||
string $stepType,
|
||||
int $id = null
|
||||
) {
|
||||
$this->automationRunId = $automationRunId;
|
||||
$this->stepId = $stepId;
|
||||
$this->stepType = $stepType;
|
||||
$this->stepKey = self::KEY_UNKNOWN;
|
||||
$this->status = self::STATUS_RUNNING;
|
||||
|
||||
$now = new DateTimeImmutable();
|
||||
$this->startedAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if ($id) {
|
||||
$this->id = $id;
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): ?int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getAutomationRunId(): int {
|
||||
return $this->automationRunId;
|
||||
}
|
||||
|
||||
public function getStepId(): string {
|
||||
return $this->stepId;
|
||||
}
|
||||
|
||||
public function getStepType(): string {
|
||||
return $this->stepType;
|
||||
}
|
||||
|
||||
public function getStepKey(): string {
|
||||
return $this->stepKey;
|
||||
}
|
||||
|
||||
public function setStepKey(string $stepKey): void {
|
||||
$this->stepKey = $stepKey;
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getStatus(): string {
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(string $status): void {
|
||||
if (!in_array($status, self::STATUS_ALL, true)) {
|
||||
throw new InvalidArgumentException("Invalid status '$status'.");
|
||||
}
|
||||
$this->status = $status;
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getStartedAt(): DateTimeImmutable {
|
||||
return $this->startedAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable {
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function getRunNumber(): int {
|
||||
return $this->runNumber;
|
||||
}
|
||||
|
||||
public function setRunNumber(int $runNumber): void {
|
||||
$this->runNumber = $runNumber;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(DateTimeImmutable $updatedAt): void {
|
||||
$this->updatedAt = $updatedAt;
|
||||
}
|
||||
|
||||
public function getData(): array {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/** @param mixed $value */
|
||||
public function setData(string $key, $value): void {
|
||||
if (!$this->isDataStorable($value)) {
|
||||
throw new InvalidArgumentException("Invalid data provided for key '$key'. Only scalar values and arrays of scalar values are allowed.");
|
||||
}
|
||||
$this->data[$key] = $value;
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getError(): ?array {
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'automation_run_id' => $this->automationRunId,
|
||||
'step_id' => $this->stepId,
|
||||
'step_type' => $this->stepType,
|
||||
'step_key' => $this->stepKey,
|
||||
'status' => $this->status,
|
||||
'started_at' => $this->startedAt->format(DateTimeImmutable::W3C),
|
||||
'updated_at' => $this->updatedAt->format(DateTimeImmutable::W3C),
|
||||
'run_number' => $this->runNumber,
|
||||
'data' => Json::encode($this->data),
|
||||
'error' => $this->error ? Json::encode($this->error) : null,
|
||||
];
|
||||
}
|
||||
|
||||
public function setError(Throwable $error): void {
|
||||
// Normalize all nested objects in error trace to associative arrays.
|
||||
// Empty objects would then get decoded to "[]" instead of "{}".
|
||||
$trace = Json::decode(Json::encode($error->getTrace()));
|
||||
$this->error = [
|
||||
'message' => $error->getMessage(),
|
||||
'errorClass' => get_class($error),
|
||||
'code' => $error->getCode(),
|
||||
'trace' => $trace,
|
||||
];
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self {
|
||||
$log = new AutomationRunLog((int)$data['automation_run_id'], $data['step_id'], $data['step_type']);
|
||||
$log->id = (int)$data['id'];
|
||||
$log->stepKey = $data['step_key'];
|
||||
$log->status = $data['status'];
|
||||
$log->startedAt = new DateTimeImmutable($data['started_at']);
|
||||
$log->updatedAt = new DateTimeImmutable($data['updated_at']);
|
||||
$log->runNumber = (int)$data['run_number'];
|
||||
$log->data = Json::decode($data['data']);
|
||||
$log->error = isset($data['error']) ? Json::decode($data['error']) : null;
|
||||
return $log;
|
||||
}
|
||||
|
||||
/** @param mixed $data */
|
||||
private function isDataStorable($data): bool {
|
||||
if (is_scalar($data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($data as $value) {
|
||||
if (!$this->isDataStorable($value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class AutomationStatistics {
|
||||
|
||||
private $automationId;
|
||||
private $versionId;
|
||||
private $entered;
|
||||
private $inProgress;
|
||||
|
||||
public function __construct(
|
||||
int $automationId,
|
||||
int $entered = 0,
|
||||
int $inProcess = 0,
|
||||
?int $versionId = null
|
||||
) {
|
||||
$this->automationId = $automationId;
|
||||
$this->entered = $entered;
|
||||
$this->inProgress = $inProcess;
|
||||
$this->versionId = $versionId;
|
||||
}
|
||||
|
||||
public function getAutomationId(): int {
|
||||
return $this->automationId;
|
||||
}
|
||||
|
||||
public function getVersionId(): ?int {
|
||||
return $this->versionId;
|
||||
}
|
||||
|
||||
public function getEntered(): int {
|
||||
return $this->entered;
|
||||
}
|
||||
|
||||
public function getInProgress(): int {
|
||||
return $this->inProgress;
|
||||
}
|
||||
|
||||
public function getExited(): int {
|
||||
return $this->getEntered() - $this->getInProgress();
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'automation_id' => $this->getAutomationId(),
|
||||
'totals' => [
|
||||
'entered' => $this->getEntered(),
|
||||
'in_progress' => $this->getInProgress(),
|
||||
'exited' => $this->getExited(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class AutomationTemplate {
|
||||
public const TYPE_DEFAULT = 'default';
|
||||
public const TYPE_PREMIUM = 'premium';
|
||||
public const TYPE_COMING_SOON = 'coming-soon';
|
||||
|
||||
/** @var string */
|
||||
private $slug;
|
||||
|
||||
/** @var string */
|
||||
private $category;
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
/** @var string */
|
||||
private $description;
|
||||
|
||||
/** @var callable(): Automation */
|
||||
private $automationFactory;
|
||||
|
||||
/** @var array<string, int|bool> */
|
||||
private $requiredCapabilities;
|
||||
|
||||
/** @var string */
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @param callable(): Automation $automationFactory
|
||||
* @param array<string, int|bool> $requiredCapabilities
|
||||
*/
|
||||
public function __construct(
|
||||
string $slug,
|
||||
string $category,
|
||||
string $name,
|
||||
string $description,
|
||||
callable $automationFactory,
|
||||
array $requiredCapabilities = [],
|
||||
string $type = self::TYPE_DEFAULT
|
||||
) {
|
||||
$this->slug = $slug;
|
||||
$this->category = $category;
|
||||
$this->name = $name;
|
||||
$this->description = $description;
|
||||
$this->automationFactory = $automationFactory;
|
||||
$this->requiredCapabilities = $requiredCapabilities;
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function getSlug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getCategory(): string {
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getDescription(): string {
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/** @return array<string, int|bool> */
|
||||
public function getRequiredCapabilities(): array {
|
||||
return $this->requiredCapabilities;
|
||||
}
|
||||
|
||||
public function createAutomation(): Automation {
|
||||
return ($this->automationFactory)();
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'slug' => $this->getSlug(),
|
||||
'name' => $this->getName(),
|
||||
'category' => $this->getCategory(),
|
||||
'type' => $this->getType(),
|
||||
'required_capabilities' => $this->getRequiredCapabilities(),
|
||||
'description' => $this->getDescription(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class AutomationTemplateCategory {
|
||||
/** @var string */
|
||||
private $slug;
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
public function __construct(
|
||||
string $slug,
|
||||
string $name
|
||||
) {
|
||||
$this->slug = $slug;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getSlug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
|
||||
class Field {
|
||||
public const TYPE_BOOLEAN = 'boolean';
|
||||
public const TYPE_INTEGER = 'integer';
|
||||
public const TYPE_NUMBER = 'number';
|
||||
public const TYPE_STRING = 'string';
|
||||
public const TYPE_ENUM = 'enum';
|
||||
public const TYPE_ENUM_ARRAY = 'enum_array';
|
||||
public const TYPE_DATETIME = 'datetime';
|
||||
|
||||
/** @var string */
|
||||
private $key;
|
||||
|
||||
/** @var string */
|
||||
private $type;
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
/** @var callable */
|
||||
private $factory;
|
||||
|
||||
/** @var array */
|
||||
private $args;
|
||||
|
||||
public function __construct(
|
||||
string $key,
|
||||
string $type,
|
||||
string $name,
|
||||
callable $factory,
|
||||
array $args = []
|
||||
) {
|
||||
|
||||
$this->key = $key;
|
||||
$this->type = $type;
|
||||
$this->name = $name;
|
||||
$this->factory = $factory;
|
||||
$this->args = $args;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getFactory(): callable {
|
||||
return $this->factory;
|
||||
}
|
||||
|
||||
/** @return mixed */
|
||||
public function getValue(Payload $payload, array $params = []) {
|
||||
return $this->getFactory()($payload, $params);
|
||||
}
|
||||
|
||||
public function getArgs(): array {
|
||||
return $this->args;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class Filter {
|
||||
/** @var string */
|
||||
private $id;
|
||||
|
||||
/** @var string */
|
||||
private $fieldType;
|
||||
|
||||
/** @var string */
|
||||
private $fieldKey;
|
||||
|
||||
/** @var string */
|
||||
private $condition;
|
||||
|
||||
/** @var array */
|
||||
private $args;
|
||||
|
||||
public function __construct(
|
||||
string $id,
|
||||
string $fieldType,
|
||||
string $fieldKey,
|
||||
string $condition,
|
||||
array $args
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->fieldType = $fieldType;
|
||||
$this->fieldKey = $fieldKey;
|
||||
$this->condition = $condition;
|
||||
$this->args = $args;
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getFieldType(): string {
|
||||
return $this->fieldType;
|
||||
}
|
||||
|
||||
public function getFieldKey(): string {
|
||||
return $this->fieldKey;
|
||||
}
|
||||
|
||||
public function getCondition(): string {
|
||||
return $this->condition;
|
||||
}
|
||||
|
||||
public function getArgs(): array {
|
||||
return $this->args;
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'field_type' => $this->fieldType,
|
||||
'field_key' => $this->fieldKey,
|
||||
'condition' => $this->condition,
|
||||
'args' => $this->args,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self {
|
||||
return new self(
|
||||
$data['id'],
|
||||
$data['field_type'],
|
||||
$data['field_key'],
|
||||
$data['condition'],
|
||||
$data['args']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class FilterGroup {
|
||||
public const OPERATOR_AND = 'and';
|
||||
public const OPERATOR_OR = 'or';
|
||||
|
||||
/** @var string */
|
||||
private $id;
|
||||
|
||||
/** @var string */
|
||||
private $operator;
|
||||
|
||||
/** @var Filter[] */
|
||||
private $filters;
|
||||
|
||||
public function __construct(
|
||||
string $id,
|
||||
string $operator,
|
||||
array $filters
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->operator = $operator;
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getOperator(): string {
|
||||
return $this->operator;
|
||||
}
|
||||
|
||||
public function getFilters(): array {
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'operator' => $this->operator,
|
||||
'filters' => array_map(function (Filter $filter): array {
|
||||
return $filter->toArray();
|
||||
}, $this->filters),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self {
|
||||
return new self(
|
||||
$data['id'],
|
||||
$data['operator'],
|
||||
array_map(function (array $filter) {
|
||||
return Filter::fromArray($filter);
|
||||
}, $data['filters'])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class Filters {
|
||||
public const OPERATOR_AND = 'and';
|
||||
public const OPERATOR_OR = 'or';
|
||||
|
||||
/** @var string */
|
||||
private $operator;
|
||||
|
||||
/** @var FilterGroup[] */
|
||||
private $groups;
|
||||
|
||||
public function __construct(
|
||||
string $operator,
|
||||
array $groups
|
||||
) {
|
||||
$this->operator = $operator;
|
||||
$this->groups = $groups;
|
||||
}
|
||||
|
||||
public function getOperator(): string {
|
||||
return $this->operator;
|
||||
}
|
||||
|
||||
public function getGroups(): array {
|
||||
return $this->groups;
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'operator' => $this->operator,
|
||||
'groups' => array_map(function (FilterGroup $group): array {
|
||||
return $group->toArray();
|
||||
}, $this->groups),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self {
|
||||
return new self(
|
||||
$data['operator'],
|
||||
array_map(function (array $group) {
|
||||
return FilterGroup::fromArray($group);
|
||||
}, $data['groups'])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class NextStep {
|
||||
/** @var string|null */
|
||||
protected $id;
|
||||
|
||||
public function __construct(
|
||||
?string $id
|
||||
) {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getId(): ?string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self {
|
||||
return new self($data['id']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class Step {
|
||||
public const TYPE_ROOT = 'root';
|
||||
public const TYPE_TRIGGER = 'trigger';
|
||||
public const TYPE_ACTION = 'action';
|
||||
|
||||
/** @var string */
|
||||
private $id;
|
||||
|
||||
/** @var string */
|
||||
private $type;
|
||||
|
||||
/** @var string */
|
||||
private $key;
|
||||
|
||||
/** @var array */
|
||||
protected $args;
|
||||
|
||||
/** @var NextStep[] */
|
||||
protected $nextSteps;
|
||||
|
||||
/** @var Filters|null */
|
||||
private $filters;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
* @param NextStep[] $nextSteps
|
||||
*/
|
||||
public function __construct(
|
||||
string $id,
|
||||
string $type,
|
||||
string $key,
|
||||
array $args,
|
||||
array $nextSteps,
|
||||
Filters $filters = null
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->type = $type;
|
||||
$this->key = $key;
|
||||
$this->args = $args;
|
||||
$this->nextSteps = $nextSteps;
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/** @return NextStep[] */
|
||||
public function getNextSteps(): array {
|
||||
return $this->nextSteps;
|
||||
}
|
||||
|
||||
public function getNextStepIds(): array {
|
||||
$ids = [];
|
||||
foreach ($this->nextSteps as $nextStep) {
|
||||
$nextStepId = $nextStep->getId();
|
||||
if ($nextStepId) {
|
||||
$ids[] = $nextStep->getId();
|
||||
}
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/** @param NextStep[] $nextSteps */
|
||||
public function setNextSteps(array $nextSteps): void {
|
||||
$this->nextSteps = $nextSteps;
|
||||
}
|
||||
|
||||
public function getArgs(): array {
|
||||
return $this->args;
|
||||
}
|
||||
|
||||
public function getFilters(): ?Filters {
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'type' => $this->type,
|
||||
'key' => $this->key,
|
||||
'args' => $this->args,
|
||||
'next_steps' => array_map(function (NextStep $nextStep) {
|
||||
return $nextStep->toArray();
|
||||
}, $this->nextSteps),
|
||||
'filters' => $this->filters ? $this->filters->toArray() : null,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self {
|
||||
return new self(
|
||||
$data['id'],
|
||||
$data['type'],
|
||||
$data['key'],
|
||||
$data['args'],
|
||||
array_map(function (array $nextStep) {
|
||||
return NextStep::fromArray($nextStep);
|
||||
}, $data['next_steps']),
|
||||
isset($data['filters']) ? Filters::fromArray($data['filters']) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use Throwable;
|
||||
|
||||
class StepRunArgs {
|
||||
/** @var Automation */
|
||||
private $automation;
|
||||
|
||||
/** @var AutomationRun */
|
||||
private $automationRun;
|
||||
|
||||
/** @var Step */
|
||||
private $step;
|
||||
|
||||
/** @var array<string, SubjectEntry<Subject<Payload>>[]> */
|
||||
private $subjectEntries = [];
|
||||
|
||||
/** @var array<class-string, string> */
|
||||
private $subjectKeyClassMap = [];
|
||||
|
||||
/** @var array<string, Field> */
|
||||
private $fields = [];
|
||||
|
||||
/** @var array<string, string> */
|
||||
private $fieldToSubjectMap = [];
|
||||
|
||||
/** @var int */
|
||||
private $runNumber;
|
||||
|
||||
/** @param SubjectEntry<Subject<Payload>>[] $subjectsEntries */
|
||||
public function __construct(
|
||||
Automation $automation,
|
||||
AutomationRun $automationRun,
|
||||
Step $step,
|
||||
array $subjectsEntries,
|
||||
int $runNumber
|
||||
) {
|
||||
$this->automation = $automation;
|
||||
$this->step = $step;
|
||||
$this->automationRun = $automationRun;
|
||||
$this->runNumber = $runNumber;
|
||||
|
||||
foreach ($subjectsEntries as $entry) {
|
||||
$subject = $entry->getSubject();
|
||||
$key = $subject->getKey();
|
||||
$this->subjectEntries[$key] = array_merge($this->subjectEntries[$key] ?? [], [$entry]);
|
||||
$this->subjectKeyClassMap[get_class($subject)] = $key;
|
||||
|
||||
foreach ($subject->getFields() as $field) {
|
||||
$this->fields[$field->getKey()] = $field;
|
||||
$this->fieldToSubjectMap[$field->getKey()] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getAutomation(): Automation {
|
||||
return $this->automation;
|
||||
}
|
||||
|
||||
public function getAutomationRun(): AutomationRun {
|
||||
return $this->automationRun;
|
||||
}
|
||||
|
||||
public function getStep(): Step {
|
||||
return $this->step;
|
||||
}
|
||||
|
||||
/** @return array<string, SubjectEntry<Subject<Payload>>[]> */
|
||||
public function getSubjectEntries(): array {
|
||||
return $this->subjectEntries;
|
||||
}
|
||||
|
||||
/** @return SubjectEntry<Subject<Payload>> */
|
||||
public function getSingleSubjectEntry(string $key): SubjectEntry {
|
||||
$subjects = $this->subjectEntries[$key] ?? [];
|
||||
if (count($subjects) === 0) {
|
||||
throw Exceptions::subjectDataNotFound($key, $this->automationRun->getId());
|
||||
}
|
||||
if (count($subjects) > 1) {
|
||||
throw Exceptions::multipleSubjectsFound($key, $this->automationRun->getId());
|
||||
}
|
||||
return $subjects[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @template P of Payload
|
||||
* @template S of Subject<P>
|
||||
* @param class-string<S> $class
|
||||
* @return SubjectEntry<S<P>>
|
||||
*/
|
||||
public function getSingleSubjectEntryByClass(string $class): SubjectEntry {
|
||||
$key = $this->subjectKeyClassMap[$class] ?? null;
|
||||
if (!$key) {
|
||||
throw Exceptions::subjectClassNotFound($class);
|
||||
}
|
||||
|
||||
/** @var SubjectEntry<S<P>> $entry -- for PHPStan */
|
||||
$entry = $this->getSingleSubjectEntry($key);
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template P of Payload
|
||||
* @param class-string<P> $class
|
||||
* @return P
|
||||
*/
|
||||
public function getSinglePayloadByClass(string $class): Payload {
|
||||
$payloads = [];
|
||||
foreach ($this->subjectEntries as $entries) {
|
||||
foreach ($entries as $entry) {
|
||||
$payload = $entry->getPayload();
|
||||
if (get_class($payload) === $class) {
|
||||
$payloads[] = $payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($payloads) === 0) {
|
||||
throw Exceptions::payloadNotFound($class, $this->automationRun->getId());
|
||||
}
|
||||
if (count($payloads) > 1) {
|
||||
throw Exceptions::multiplePayloadsFound($class, $this->automationRun->getId());
|
||||
}
|
||||
|
||||
// ensure PHPStan we're indeed returning an instance of $class
|
||||
$payload = $payloads[0];
|
||||
if (!$payload instanceof $class) {
|
||||
throw InvalidStateException::create();
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/** @return mixed */
|
||||
public function getFieldValue(string $key, array $params = []) {
|
||||
$field = $this->fields[$key] ?? null;
|
||||
$subjectKey = $this->fieldToSubjectMap[$key] ?? null;
|
||||
if (!$field || !$subjectKey) {
|
||||
throw Exceptions::fieldNotFound($key);
|
||||
}
|
||||
|
||||
$entry = $this->getSingleSubjectEntry($subjectKey);
|
||||
try {
|
||||
$value = $field->getValue($entry->getPayload(), $params);
|
||||
} catch (Throwable $e) {
|
||||
throw Exceptions::fieldLoadFailed($field->getKey(), $field->getArgs());
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getRunNumber(): int {
|
||||
return $this->runNumber;
|
||||
}
|
||||
|
||||
public function isFirstRun(): bool {
|
||||
return $this->runNumber === 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
|
||||
class StepValidationArgs {
|
||||
/** @var Automation */
|
||||
private $automation;
|
||||
|
||||
/** @var Step */
|
||||
private $step;
|
||||
|
||||
/** @var array<string, Subject<Payload>> */
|
||||
private $subjects = [];
|
||||
|
||||
/** @var array<class-string, string> */
|
||||
private $subjectKeyClassMap = [];
|
||||
|
||||
/** @param Subject<Payload>[] $subjects */
|
||||
public function __construct(
|
||||
Automation $automation,
|
||||
Step $step,
|
||||
array $subjects
|
||||
) {
|
||||
$this->automation = $automation;
|
||||
$this->step = $step;
|
||||
|
||||
foreach ($subjects as $subject) {
|
||||
$key = $subject->getKey();
|
||||
$this->subjects[$key] = $subject;
|
||||
$this->subjectKeyClassMap[get_class($subject)] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
public function getAutomation(): Automation {
|
||||
return $this->automation;
|
||||
}
|
||||
|
||||
public function getStep(): Step {
|
||||
return $this->step;
|
||||
}
|
||||
|
||||
/** @return Subject<Payload>[] */
|
||||
public function getSubjects(): array {
|
||||
return array_values($this->subjects);
|
||||
}
|
||||
|
||||
/** @return Subject<Payload> */
|
||||
public function getSingleSubject(string $key): Subject {
|
||||
$subject = $this->subjects[$key] ?? null;
|
||||
if (!$subject) {
|
||||
throw Exceptions::subjectNotFound($key);
|
||||
}
|
||||
return $subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template P of Payload
|
||||
* @template S of Subject<P>
|
||||
* @param class-string<S> $class
|
||||
* @return S<P>
|
||||
*/
|
||||
public function getSingleSubjectByClass(string $class): Subject {
|
||||
$key = $this->subjectKeyClassMap[$class] ?? null;
|
||||
if (!$key) {
|
||||
throw Exceptions::subjectClassNotFound($class);
|
||||
}
|
||||
|
||||
/** @var S<P> $subject -- for PHPStan */
|
||||
$subject = $this->getSingleSubject($key);
|
||||
return $subject;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Utils\Json;
|
||||
|
||||
class Subject {
|
||||
/** @var string */
|
||||
private $key;
|
||||
|
||||
/** @var array */
|
||||
private $args;
|
||||
|
||||
public function __construct(
|
||||
string $key,
|
||||
array $args
|
||||
) {
|
||||
$this->key = $key;
|
||||
$this->args = $args;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function getArgs(): array {
|
||||
return $this->args;
|
||||
}
|
||||
|
||||
public function getHash(): string {
|
||||
return md5($this->getKey() . serialize($this->getArgs()));
|
||||
}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'key' => $this->getKey(),
|
||||
'args' => Json::encode($this->getArgs()),
|
||||
'hash' => $this->getHash(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self {
|
||||
return new self($data['key'], Json::decode($data['args']));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Data;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @template-covariant S of Subject<Payload>
|
||||
*/
|
||||
class SubjectEntry {
|
||||
/** @var S */
|
||||
private $subject;
|
||||
|
||||
/** @var SubjectData */
|
||||
private $subjectData;
|
||||
|
||||
/** @var Payload|null */
|
||||
private $payloadCache;
|
||||
|
||||
/** @param S $subject */
|
||||
public function __construct(
|
||||
Subject $subject,
|
||||
SubjectData $subjectData
|
||||
) {
|
||||
$this->subject = $subject;
|
||||
$this->subjectData = $subjectData;
|
||||
}
|
||||
|
||||
/** @return S */
|
||||
public function getSubject(): Subject {
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
public function getSubjectData(): SubjectData {
|
||||
return $this->subjectData;
|
||||
}
|
||||
|
||||
/** @return Payload */
|
||||
public function getPayload() {
|
||||
if ($this->payloadCache === null) {
|
||||
try {
|
||||
$this->payloadCache = $this->subject->getPayload($this->subjectData);
|
||||
} catch (Throwable $e) {
|
||||
throw Exceptions::subjectLoadFailed($this->subject->getKey(), $this->subjectData->getArgs());
|
||||
}
|
||||
}
|
||||
return $this->payloadCache;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Endpoints\Automations;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\API\REST\Request;
|
||||
use MailPoet\API\REST\Response;
|
||||
use MailPoet\Automation\Engine\API\Endpoint;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Mappers\AutomationMapper;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationValidator;
|
||||
use MailPoet\Validator\Builder;
|
||||
|
||||
class AutomationTemplateGetEndpoint extends Endpoint {
|
||||
/** @var AutomationMapper */
|
||||
private $automationMapper;
|
||||
|
||||
/** @var AutomationValidator */
|
||||
private $automationValidator;
|
||||
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
public function __construct(
|
||||
AutomationMapper $automationMapper,
|
||||
AutomationValidator $automationValidator,
|
||||
Registry $registry
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
$this->automationValidator = $automationValidator;
|
||||
$this->automationMapper = $automationMapper;
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response {
|
||||
/** @var string|null $slug - for PHPStan because strval() doesn't accept a value of mixed */
|
||||
$slug = $request->getParam('slug');
|
||||
$slug = strval($slug);
|
||||
$template = $this->registry->getTemplate($slug);
|
||||
if (!$template) {
|
||||
throw Exceptions::automationTemplateNotFound($slug);
|
||||
}
|
||||
|
||||
$automation = $template->createAutomation();
|
||||
$automation->setId(0);
|
||||
$this->automationValidator->validate($automation);
|
||||
|
||||
$data = $template->toArray() + [
|
||||
'automation' => $this->automationMapper->buildAutomation($automation),
|
||||
];
|
||||
return new Response($data);
|
||||
}
|
||||
|
||||
public static function getRequestSchema(): array {
|
||||
return [
|
||||
'slug' => Builder::string()->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Endpoints\Automations;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\API\REST\Request;
|
||||
use MailPoet\API\REST\Response;
|
||||
use MailPoet\Automation\Engine\API\Endpoint;
|
||||
use MailPoet\Automation\Engine\Data\AutomationTemplate;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Validator\Builder;
|
||||
|
||||
class AutomationTemplatesGetEndpoint extends Endpoint {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response {
|
||||
/** @var string|null $category */
|
||||
$category = $request->getParam('category');
|
||||
$templates = array_values($this->registry->getTemplates($category ? strval($category) : null));
|
||||
return new Response(array_map(function (AutomationTemplate $automation) {
|
||||
return $automation->toArray();
|
||||
}, $templates));
|
||||
}
|
||||
|
||||
public static function getRequestSchema(): array {
|
||||
return [
|
||||
'category' => Builder::string(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Endpoints\Automations;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\API\REST\Request;
|
||||
use MailPoet\API\REST\Response;
|
||||
use MailPoet\Automation\Engine\API\Endpoint;
|
||||
use MailPoet\Automation\Engine\Builder\CreateAutomationFromTemplateController;
|
||||
use MailPoet\Automation\Engine\Mappers\AutomationMapper;
|
||||
use MailPoet\Validator\Builder;
|
||||
|
||||
class AutomationsCreateFromTemplateEndpoint extends Endpoint {
|
||||
/** @var CreateAutomationFromTemplateController */
|
||||
private $createAutomationFromTemplateController;
|
||||
|
||||
/** @var AutomationMapper */
|
||||
private $automationMapper;
|
||||
|
||||
public function __construct(
|
||||
CreateAutomationFromTemplateController $createAutomationFromTemplateController,
|
||||
AutomationMapper $automationMapper
|
||||
) {
|
||||
$this->createAutomationFromTemplateController = $createAutomationFromTemplateController;
|
||||
$this->automationMapper = $automationMapper;
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response {
|
||||
$automation = $this->createAutomationFromTemplateController->createAutomation((string)$request->getParam('slug'));
|
||||
return new Response($this->automationMapper->buildAutomation($automation));
|
||||
}
|
||||
|
||||
public static function getRequestSchema(): array {
|
||||
return [
|
||||
'slug' => Builder::string()->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Endpoints\Automations;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\API\REST\Request;
|
||||
use MailPoet\API\REST\Response;
|
||||
use MailPoet\Automation\Engine\API\Endpoint;
|
||||
use MailPoet\Automation\Engine\Builder\DeleteAutomationController;
|
||||
use MailPoet\Validator\Builder;
|
||||
|
||||
class AutomationsDeleteEndpoint extends Endpoint {
|
||||
/** @var DeleteAutomationController */
|
||||
private $deleteController;
|
||||
|
||||
public function __construct(
|
||||
DeleteAutomationController $deleteController
|
||||
) {
|
||||
$this->deleteController = $deleteController;
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response {
|
||||
/** @var int $automationId */
|
||||
$automationId = $request->getParam('id');
|
||||
$automationId = intval($automationId);
|
||||
$this->deleteController->deleteAutomation($automationId);
|
||||
return new Response(null);
|
||||
}
|
||||
|
||||
public static function getRequestSchema(): array {
|
||||
return [
|
||||
'id' => Builder::integer()->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Endpoints\Automations;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\API\REST\Request;
|
||||
use MailPoet\API\REST\Response;
|
||||
use MailPoet\Automation\Engine\API\Endpoint;
|
||||
use MailPoet\Automation\Engine\Builder\DuplicateAutomationController;
|
||||
use MailPoet\Automation\Engine\Mappers\AutomationMapper;
|
||||
use MailPoet\Validator\Builder;
|
||||
|
||||
class AutomationsDuplicateEndpoint extends Endpoint {
|
||||
/** @var AutomationMapper */
|
||||
private $automationMapper;
|
||||
|
||||
/** @var DuplicateAutomationController */
|
||||
private $duplicateController;
|
||||
|
||||
public function __construct(
|
||||
DuplicateAutomationController $duplicateController,
|
||||
AutomationMapper $automationMapper
|
||||
) {
|
||||
$this->automationMapper = $automationMapper;
|
||||
$this->duplicateController = $duplicateController;
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response {
|
||||
/** @var int $automationId */
|
||||
$automationId = $request->getParam('id');
|
||||
$automationId = intval($automationId);
|
||||
$duplicate = $this->duplicateController->duplicateAutomation($automationId);
|
||||
return new Response($this->automationMapper->buildAutomation($duplicate));
|
||||
}
|
||||
|
||||
public static function getRequestSchema(): array {
|
||||
return [
|
||||
'id' => Builder::integer()->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Endpoints\Automations;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\API\REST\Request;
|
||||
use MailPoet\API\REST\Response;
|
||||
use MailPoet\Automation\Engine\API\Endpoint;
|
||||
use MailPoet\Automation\Engine\Mappers\AutomationMapper;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Validator\Builder;
|
||||
|
||||
class AutomationsGetEndpoint extends Endpoint {
|
||||
/** @var AutomationMapper */
|
||||
private $automationMapper;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
public function __construct(
|
||||
AutomationMapper $automationMapper,
|
||||
AutomationStorage $automationStorage
|
||||
) {
|
||||
$this->automationMapper = $automationMapper;
|
||||
$this->automationStorage = $automationStorage;
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response {
|
||||
$status = $request->getParam('status') ? (array)$request->getParam('status') : null;
|
||||
$automations = $this->automationStorage->getAutomations($status);
|
||||
return new Response($this->automationMapper->buildAutomationList($automations));
|
||||
}
|
||||
|
||||
public static function getRequestSchema(): array {
|
||||
return [
|
||||
'status' => Builder::array(Builder::string()),
|
||||
];
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Endpoints\Automations;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\API\REST\Request;
|
||||
use MailPoet\API\REST\Response;
|
||||
use MailPoet\Automation\Engine\API\Endpoint;
|
||||
use MailPoet\Automation\Engine\Builder\UpdateAutomationController;
|
||||
use MailPoet\Automation\Engine\Mappers\AutomationMapper;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationSchema;
|
||||
use MailPoet\Validator\Builder;
|
||||
|
||||
class AutomationsPutEndpoint extends Endpoint {
|
||||
/** @var UpdateAutomationController */
|
||||
private $updateController;
|
||||
|
||||
/** @var AutomationMapper */
|
||||
private $automationMapper;
|
||||
|
||||
public function __construct(
|
||||
UpdateAutomationController $updateController,
|
||||
AutomationMapper $automationMapper
|
||||
) {
|
||||
$this->updateController = $updateController;
|
||||
$this->automationMapper = $automationMapper;
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response {
|
||||
$data = $request->getParams();
|
||||
/** @var int $automationId */
|
||||
$automationId = $request->getParam('id');
|
||||
$automation = $this->updateController->updateAutomation(intval($automationId), $data);
|
||||
return new Response($this->automationMapper->buildAutomation($automation));
|
||||
}
|
||||
|
||||
public static function getRequestSchema(): array {
|
||||
return [
|
||||
'id' => Builder::integer()->required(),
|
||||
'name' => Builder::string()->minLength(1),
|
||||
'status' => Builder::string(),
|
||||
'steps' => AutomationSchema::getStepsSchema(),
|
||||
'meta' => Builder::object(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\API\API;
|
||||
use MailPoet\Automation\Engine\Control\StepHandler;
|
||||
use MailPoet\Automation\Engine\Control\TriggerHandler;
|
||||
use MailPoet\Automation\Engine\Endpoints\Automations\AutomationsCreateFromTemplateEndpoint;
|
||||
use MailPoet\Automation\Engine\Endpoints\Automations\AutomationsDeleteEndpoint;
|
||||
use MailPoet\Automation\Engine\Endpoints\Automations\AutomationsDuplicateEndpoint;
|
||||
use MailPoet\Automation\Engine\Endpoints\Automations\AutomationsGetEndpoint;
|
||||
use MailPoet\Automation\Engine\Endpoints\Automations\AutomationsPutEndpoint;
|
||||
use MailPoet\Automation\Engine\Endpoints\Automations\AutomationTemplateGetEndpoint;
|
||||
use MailPoet\Automation\Engine\Endpoints\Automations\AutomationTemplatesGetEndpoint;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Automation\Integrations\Core\CoreIntegration;
|
||||
use MailPoet\Automation\Integrations\WordPress\WordPressIntegration;
|
||||
|
||||
class Engine {
|
||||
const CAPABILITY_MANAGE_AUTOMATIONS = 'mailpoet_manage_automations';
|
||||
|
||||
/** @var API */
|
||||
private $api;
|
||||
|
||||
/** @var CoreIntegration */
|
||||
private $coreIntegration;
|
||||
|
||||
/** @var WordPressIntegration */
|
||||
private $wordPressIntegration;
|
||||
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
/** @var StepHandler */
|
||||
private $stepHandler;
|
||||
|
||||
/** @var TriggerHandler */
|
||||
private $triggerHandler;
|
||||
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
public function __construct(
|
||||
API $api,
|
||||
CoreIntegration $coreIntegration,
|
||||
WordPressIntegration $wordPressIntegration,
|
||||
Registry $registry,
|
||||
StepHandler $stepHandler,
|
||||
TriggerHandler $triggerHandler,
|
||||
WordPress $wordPress,
|
||||
AutomationStorage $automationStorage
|
||||
) {
|
||||
$this->api = $api;
|
||||
$this->coreIntegration = $coreIntegration;
|
||||
$this->wordPressIntegration = $wordPressIntegration;
|
||||
$this->registry = $registry;
|
||||
$this->stepHandler = $stepHandler;
|
||||
$this->triggerHandler = $triggerHandler;
|
||||
$this->wordPress = $wordPress;
|
||||
$this->automationStorage = $automationStorage;
|
||||
}
|
||||
|
||||
public function initialize(): void {
|
||||
$this->registerApiRoutes();
|
||||
|
||||
$this->api->initialize();
|
||||
$this->stepHandler->initialize();
|
||||
$this->triggerHandler->initialize();
|
||||
|
||||
$this->coreIntegration->register($this->registry);
|
||||
$this->wordPressIntegration->register($this->registry);
|
||||
$this->wordPress->doAction(Hooks::INITIALIZE, [$this->registry]);
|
||||
$this->registerActiveTriggerHooks();
|
||||
}
|
||||
|
||||
private function registerApiRoutes(): void {
|
||||
$this->wordPress->addAction(Hooks::API_INITIALIZE, function (API $api) {
|
||||
$api->registerGetRoute('automations', AutomationsGetEndpoint::class);
|
||||
$api->registerPutRoute('automations/(?P<id>\d+)', AutomationsPutEndpoint::class);
|
||||
$api->registerDeleteRoute('automations/(?P<id>\d+)', AutomationsDeleteEndpoint::class);
|
||||
$api->registerPostRoute('automations/(?P<id>\d+)/duplicate', AutomationsDuplicateEndpoint::class);
|
||||
$api->registerPostRoute('automations/create-from-template', AutomationsCreateFromTemplateEndpoint::class);
|
||||
$api->registerGetRoute('automation-templates', AutomationTemplatesGetEndpoint::class);
|
||||
$api->registerGetRoute('automation-templates/(?P<slug>.+)', AutomationTemplateGetEndpoint::class);
|
||||
});
|
||||
}
|
||||
|
||||
private function registerActiveTriggerHooks(): void {
|
||||
$triggerKeys = $this->automationStorage->getActiveTriggerKeys();
|
||||
foreach ($triggerKeys as $triggerKey) {
|
||||
$instance = $this->registry->getTrigger($triggerKey);
|
||||
if ($instance) {
|
||||
$instance->registerHooks();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Exceptions\NotFoundException;
|
||||
use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException;
|
||||
use MailPoet\Automation\Engine\Utils\Json;
|
||||
|
||||
class Exceptions {
|
||||
private const DATABASE_ERROR = 'mailpoet_automation_database_error';
|
||||
private const JSON_NOT_OBJECT = 'mailpoet_automation_json_not_object';
|
||||
private const AUTOMATION_NOT_FOUND = 'mailpoet_automation_not_found';
|
||||
private const AUTOMATION_VERSION_NOT_FOUND = 'mailpoet_automation_version_not_found';
|
||||
private const AUTOMATION_NOT_ACTIVE = 'mailpoet_automation_not_active';
|
||||
private const AUTOMATION_RUN_NOT_FOUND = 'mailpoet_automation_run_not_found';
|
||||
private const AUTOMATION_STEP_NOT_FOUND = 'mailpoet_automation_step_not_found';
|
||||
private const AUTOMATION_TRIGGER_NOT_FOUND = 'mailpoet_automation_trigger_not_found';
|
||||
private const AUTOMATION_RUN_NOT_RUNNING = 'mailpoet_automation_run_not_running';
|
||||
private const SUBJECT_NOT_FOUND = 'mailpoet_automation_subject_not_found';
|
||||
private const SUBJECT_LOAD_FAILED = 'mailpoet_automation_subject_load_failed';
|
||||
private const SUBJECT_DATA_NOT_FOUND = 'mailpoet_automation_subject_data_not_found';
|
||||
private const MULTIPLE_SUBJECTS_FOUND = 'mailpoet_automation_multiple_subjects_found';
|
||||
private const PAYLOAD_NOT_FOUND = 'mailpoet_automation_payload_not_found';
|
||||
private const MULTIPLE_PAYLOADS_FOUND = 'mailpoet_automation_multiple_payloads_found';
|
||||
private const FIELD_NOT_FOUND = 'mailpoet_automation_field_not_found';
|
||||
private const FIELD_LOAD_FAILED = 'mailpoet_automation_field_load_failed';
|
||||
private const FILTER_NOT_FOUND = 'mailpoet_automation_filter_not_found';
|
||||
private const NEXT_STEP_NOT_FOUND = 'mailpoet_next_step_not_found';
|
||||
private const NEXT_STEP_NOT_SCHEDULED = 'mailpoet_next_step_not_scheduled';
|
||||
private const AUTOMATION_STRUCTURE_MODIFICATION_NOT_SUPPORTED = 'mailpoet_automation_structure_modification_not_supported';
|
||||
private const AUTOMATION_STRUCTURE_NOT_VALID = 'mailpoet_automation_structure_not_valid';
|
||||
private const AUTOMATION_STEP_MODIFIED_WHEN_UNKNOWN = 'mailpoet_automation_step_modified_when_unknown';
|
||||
private const AUTOMATION_NOT_VALID = 'mailpoet_automation_not_valid';
|
||||
private const MISSING_REQUIRED_SUBJECTS = 'mailpoet_automation_missing_required_subjects';
|
||||
private const AUTOMATION_NOT_TRASHED = 'mailpoet_automation_not_trashed';
|
||||
private const AUTOMATION_TEMPLATE_NOT_FOUND = 'mailpoet_automation_template_not_found';
|
||||
private const AUTOMATION_HAS_ACTIVE_RUNS = 'mailpoet_automation_has_active_runs';
|
||||
private const AUTOMATION_STEP_NOT_STARTED = 'mailpoet_automation_step_not_started';
|
||||
private const AUTOMATION_STEP_NOT_RUNNING = 'mailpoet_automation_step_not_running';
|
||||
private const AUTOMATION_STEP_ACTION_PROCESSED = 'mailpoet_automation_step_action_processed';
|
||||
|
||||
public function __construct() {
|
||||
throw new InvalidStateException(
|
||||
"This is a static factory class. Use it via 'Exception::someError()' factories."
|
||||
);
|
||||
}
|
||||
|
||||
public static function databaseError(string $error): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::DATABASE_ERROR)
|
||||
// translators: %s is the error message.
|
||||
->withMessage(sprintf(__('Database error: %s', 'mailpoet'), $error));
|
||||
}
|
||||
|
||||
public static function jsonNotObject(string $json): UnexpectedValueException {
|
||||
return UnexpectedValueException::create()
|
||||
->withErrorCode(self::JSON_NOT_OBJECT)
|
||||
// translators: %s is the mentioned JSON string.
|
||||
->withMessage(sprintf(__("JSON string '%s' doesn't encode an object.", 'mailpoet'), $json));
|
||||
}
|
||||
|
||||
public static function automationNotFound(int $id): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::AUTOMATION_NOT_FOUND)
|
||||
// translators: %d is the ID of the automation.
|
||||
->withMessage(sprintf(__("Automation with ID '%d' not found.", 'mailpoet'), $id));
|
||||
}
|
||||
|
||||
public static function automationNotFoundInTimeSpan(int $id): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::AUTOMATION_NOT_FOUND)
|
||||
// translators: %d is the ID of the automation.
|
||||
->withMessage(sprintf(__("Automation with ID '%d' not found in selected time span.", 'mailpoet'), $id));
|
||||
}
|
||||
|
||||
public static function automationVersionNotFound(int $automation, int $version): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::AUTOMATION_VERSION_NOT_FOUND)
|
||||
// translators: %1$s is the ID of the automation, %2$s the version.
|
||||
->withMessage(sprintf(__('Automation with ID "%1$s" in version "%2$s" not found.', 'mailpoet'), $automation, $version));
|
||||
}
|
||||
|
||||
public static function automationNotActive(int $automation): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::AUTOMATION_NOT_ACTIVE)
|
||||
// translators: %1$s is the ID of the automation.
|
||||
->withMessage(sprintf(__('Automation with ID "%1$s" is no longer active.', 'mailpoet'), $automation));
|
||||
}
|
||||
|
||||
public static function automationRunNotFound(int $id): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::AUTOMATION_RUN_NOT_FOUND)
|
||||
// translators: %d is the ID of the automation run.
|
||||
->withMessage(sprintf(__("Automation run with ID '%d' not found.", 'mailpoet'), $id));
|
||||
}
|
||||
|
||||
public static function automationStepNotFound(string $key): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::AUTOMATION_STEP_NOT_FOUND)
|
||||
// translators: %s is the key of the automation step.
|
||||
->withMessage(sprintf(__("Automation step with key '%s' not found.", 'mailpoet'), $key));
|
||||
}
|
||||
|
||||
public static function automationTriggerNotFound(int $automationId, string $key): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::AUTOMATION_TRIGGER_NOT_FOUND)
|
||||
// translators: %1$s is the key, %2$d is the automation ID.
|
||||
->withMessage(sprintf(__('Automation trigger with key "%1$s" not found in automation ID "%2$d".', 'mailpoet'), $key, $automationId));
|
||||
}
|
||||
|
||||
public static function automationRunNotRunning(int $id, string $status): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::AUTOMATION_RUN_NOT_RUNNING)
|
||||
// translators: %1$d is the ID of the automation run, %2$s its current status.
|
||||
->withMessage(sprintf(__('Automation run with ID "%1$d" is not running. Status: %2$s', 'mailpoet'), $id, $status));
|
||||
}
|
||||
|
||||
public static function subjectNotFound(string $key): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::SUBJECT_NOT_FOUND)
|
||||
// translators: %s is the key of the subject not found.
|
||||
->withMessage(sprintf(__("Subject with key '%s' not found.", 'mailpoet'), $key));
|
||||
}
|
||||
|
||||
public static function subjectClassNotFound(string $class): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::SUBJECT_NOT_FOUND)
|
||||
// translators: %s is the class name of the subject not found.
|
||||
->withMessage(sprintf(__("Subject of class '%s' not found.", 'mailpoet'), $class));
|
||||
}
|
||||
|
||||
public static function subjectLoadFailed(string $key, array $args): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::SUBJECT_LOAD_FAILED)
|
||||
// translators: %1$s is the name of the key, %2$s the arguments.
|
||||
->withMessage(sprintf(__('Subject with key "%1$s" and args "%2$s" failed to load.', 'mailpoet'), $key, Json::encode($args)));
|
||||
}
|
||||
|
||||
public static function subjectDataNotFound(string $key, int $automationRunId): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::SUBJECT_DATA_NOT_FOUND)
|
||||
// translators: %1$s is the key of the subject, %2$d is automation run ID.
|
||||
->withMessage(
|
||||
sprintf(__("Subject data for subject with key '%1\$s' not found for automation run with ID '%2\$d'.", 'mailpoet'), $key, $automationRunId)
|
||||
);
|
||||
}
|
||||
|
||||
public static function multipleSubjectsFound(string $key, int $automationRunId): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::MULTIPLE_SUBJECTS_FOUND)
|
||||
// translators: %1$s is the key of the subject, %2$d is automation run ID.
|
||||
->withMessage(
|
||||
sprintf(__("Multiple subjects with key '%1\$s' found for automation run with ID '%2\$d', only one expected.", 'mailpoet'), $key, $automationRunId)
|
||||
);
|
||||
}
|
||||
|
||||
public static function payloadNotFound(string $class, int $automationRunId): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::PAYLOAD_NOT_FOUND)
|
||||
// translators: %1$s is the class of the payload, %2$d is automation run ID.
|
||||
->withMessage(
|
||||
sprintf(__("Payload of class '%1\$s' not found for automation run with ID '%2\$d'.", 'mailpoet'), $class, $automationRunId)
|
||||
);
|
||||
}
|
||||
|
||||
public static function multiplePayloadsFound(string $class, int $automationRunId): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::MULTIPLE_PAYLOADS_FOUND)
|
||||
// translators: %1$s is the class of the payloads, %2$d is automation run ID.
|
||||
->withMessage(
|
||||
sprintf(__("Multiple payloads of class '%1\$s' found for automation run with ID '%2\$d'.", 'mailpoet'), $class, $automationRunId)
|
||||
);
|
||||
}
|
||||
|
||||
public static function fieldNotFound(string $key): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::FIELD_NOT_FOUND)
|
||||
// translators: %s is the key of the field not found.
|
||||
->withMessage(sprintf(__("Field with key '%s' not found.", 'mailpoet'), $key));
|
||||
}
|
||||
|
||||
public static function fieldLoadFailed(string $key, array $args): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::FIELD_LOAD_FAILED)
|
||||
// translators: %1$s is the key of the field, %2$s its arguments.
|
||||
->withMessage(sprintf(__('Field with key "%1$s" and args "%2$s" failed to load.', 'mailpoet'), $key, Json::encode($args)));
|
||||
}
|
||||
|
||||
public static function filterNotFound(string $fieldType): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::FILTER_NOT_FOUND)
|
||||
// translators: %s is the type of the field for which a filter was not found.
|
||||
->withMessage(sprintf(__("Filter for field of type '%s' not found.", 'mailpoet'), $fieldType));
|
||||
}
|
||||
|
||||
public static function nextStepNotFound(string $stepId, int $nextStepId): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::NEXT_STEP_NOT_FOUND)
|
||||
// translators: %1$d is the ID of the automation step, %2$s is the ID of the next step.
|
||||
->withMessage(sprintf(__("Automation step with ID '%1\$s' doesn't have a next step with index '%2\$d'.", 'mailpoet'), $stepId, $nextStepId));
|
||||
}
|
||||
|
||||
public static function nextStepNotScheduled(string $stepId): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::NEXT_STEP_NOT_SCHEDULED)
|
||||
// translators: %1$d is the ID of the automation step, %2$s is the ID of the next step.
|
||||
->withMessage(sprintf(__("Automation step with ID '%s' did not schedule a specific next step, even though multiple next steps are possible.", 'mailpoet'), $stepId));
|
||||
}
|
||||
|
||||
public static function automationStructureModificationNotSupported(): UnexpectedValueException {
|
||||
return UnexpectedValueException::create()
|
||||
->withErrorCode(self::AUTOMATION_STRUCTURE_MODIFICATION_NOT_SUPPORTED)
|
||||
->withMessage(__('Automation structure modification not supported.', 'mailpoet'));
|
||||
}
|
||||
|
||||
public static function automationStructureNotValid(string $detail, string $ruleId): UnexpectedValueException {
|
||||
return UnexpectedValueException::create()
|
||||
->withErrorCode(self::AUTOMATION_STRUCTURE_NOT_VALID)
|
||||
// translators: %s is a detailed information
|
||||
->withMessage(sprintf(__("Invalid automation structure: %s", 'mailpoet'), $detail))
|
||||
->withErrors(['rule_id' => $ruleId]);
|
||||
}
|
||||
|
||||
public static function automationStepModifiedWhenUnknown(Step $step): UnexpectedValueException {
|
||||
return UnexpectedValueException::create()
|
||||
->withErrorCode(self::AUTOMATION_STEP_MODIFIED_WHEN_UNKNOWN)
|
||||
// translators: %1$s is the key of the step, %2$s is the type of the step, %3\$s is its ID.
|
||||
->withMessage(
|
||||
sprintf(
|
||||
__("Modification of step '%1\$s' of type '%2\$s' with ID '%3\$s' is not supported when the related plugin is not active.", 'mailpoet'),
|
||||
$step->getKey(),
|
||||
$step->getType(),
|
||||
$step->getId()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function automationNotValid(string $detail, array $errors): UnexpectedValueException {
|
||||
return UnexpectedValueException::create()
|
||||
->withErrorCode(self::AUTOMATION_NOT_VALID)
|
||||
// translators: %s is a detailed information
|
||||
->withMessage(sprintf(__("Automation validation failed: %s", 'mailpoet'), $detail))
|
||||
->withErrors($errors);
|
||||
}
|
||||
|
||||
public static function missingRequiredSubjects(Step $step, array $missingSubjectKeys): UnexpectedValueException {
|
||||
return UnexpectedValueException::create()
|
||||
->withErrorCode(self::MISSING_REQUIRED_SUBJECTS)
|
||||
// translators: %1$s is the key of the step, %2$s are the missing subject keys.
|
||||
->withMessage(
|
||||
sprintf(
|
||||
__("Step with ID '%1\$s' is missing required subjects with keys: %2\$s", 'mailpoet'),
|
||||
$step->getId(),
|
||||
implode(', ', $missingSubjectKeys)
|
||||
)
|
||||
)
|
||||
->withErrors(
|
||||
['general' => __('This step can not be used with the selected trigger.', 'mailpoet')]
|
||||
);
|
||||
}
|
||||
|
||||
public static function automationNotTrashed(int $id): UnexpectedValueException {
|
||||
return UnexpectedValueException::create()
|
||||
->withErrorCode(self::AUTOMATION_NOT_TRASHED)
|
||||
// translators: %d is the ID of the automation.
|
||||
->withMessage(sprintf(__("Can't delete automation with ID '%d' because it was not trashed.", 'mailpoet'), $id));
|
||||
}
|
||||
|
||||
public static function automationTemplateNotFound(string $id): NotFoundException {
|
||||
return NotFoundException::create()
|
||||
->withErrorCode(self::AUTOMATION_TEMPLATE_NOT_FOUND)
|
||||
// translators: %d is the ID of the automation template.
|
||||
->withMessage(sprintf(__("Automation template with ID '%d' not found.", 'mailpoet'), $id));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a temporary block, see MAILPOET-4744
|
||||
*/
|
||||
public static function automationHasActiveRuns(int $id): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::AUTOMATION_HAS_ACTIVE_RUNS)
|
||||
// translators: %d is the ID of the automation.
|
||||
->withMessage(sprintf(__("Can not update automation with ID '%d' because users are currently active.", 'mailpoet'), $id));
|
||||
}
|
||||
|
||||
public static function stepNotStarted(string $id, int $runId): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::AUTOMATION_STEP_NOT_STARTED)
|
||||
// translators: %1$s is the ID of the automation step, %2$d is the automation run ID.
|
||||
->withMessage(sprintf(__("Automation step with ID '%1\$s' was not started in automation run with ID '%2\$d'.", 'mailpoet'), $id, $runId));
|
||||
}
|
||||
|
||||
public static function stepNotRunning(string $id, string $status, int $runId): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::AUTOMATION_STEP_NOT_RUNNING)
|
||||
// translators: %1$s is the ID of the automation step, %2$s its current status, %3$d is the automation run ID.
|
||||
->withMessage(sprintf(__("Automation step with ID '%1\$s' is not running in automation run with ID '%2\$d'. Status: '%3\$s'", 'mailpoet'), $id, $runId, $status));
|
||||
}
|
||||
|
||||
public static function stepActionProcessed(string $id, int $runId, int $runNumber): InvalidStateException {
|
||||
return InvalidStateException::create()
|
||||
->withErrorCode(self::AUTOMATION_STEP_ACTION_PROCESSED)
|
||||
// translators: %1$d is the automation run ID, %2$s is the ID of the automation step, %3$d is the run number.
|
||||
->withMessage(sprintf(__("Automation run with ID '%1\$d' already has a processed action for step with ID '%2\$s' and run number '%3\$d'.", 'mailpoet'), $runId, $id, $runNumber));
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
/**
|
||||
* USE: When an action is forbidden for given actor (although generally valid).
|
||||
* API: 403 Forbidden
|
||||
*/
|
||||
class AccessDeniedException extends UnexpectedValueException {
|
||||
protected $statusCode = 403;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
/**
|
||||
* USE: When the main action produces conflict (i.e. duplicate key).
|
||||
* API: 409 Conflict
|
||||
*/
|
||||
class ConflictException extends UnexpectedValueException {
|
||||
protected $statusCode = 409;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use Exception as PhpException;
|
||||
use MailPoet\API\REST\Exception as RestException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Frames all MailPoet Automation exceptions ("$e instanceof MailPoet\Automation\Exception").
|
||||
*/
|
||||
abstract class Exception extends PhpException implements RestException {
|
||||
/** @var int */
|
||||
protected $statusCode = 500;
|
||||
|
||||
/** @var string */
|
||||
protected $errorCode;
|
||||
|
||||
/** @var string[] */
|
||||
protected $errors = [];
|
||||
|
||||
final public function __construct(
|
||||
string $message = null,
|
||||
string $errorCode = null,
|
||||
Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message ?? __('Unknown error.', 'mailpoet'), 0, $previous);
|
||||
$this->errorCode = $errorCode ?? 'mailpoet_automation_unknown_error';
|
||||
}
|
||||
|
||||
/** @return static */
|
||||
public static function create(Throwable $previous = null) {
|
||||
return new static(null, null, $previous);
|
||||
}
|
||||
|
||||
/** @return static */
|
||||
public function withStatusCode(int $statusCode) {
|
||||
$this->statusCode = $statusCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return static */
|
||||
public function withError(string $id, string $error) {
|
||||
$this->errors[$id] = $error;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return static */
|
||||
public function withErrorCode(string $errorCode) {
|
||||
$this->errorCode = $errorCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return static */
|
||||
public function withMessage(string $message) {
|
||||
$this->message = $message;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return static */
|
||||
public function withErrors(array $errors) {
|
||||
$this->errors = $errors;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatusCode(): int {
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
public function getErrorCode(): string {
|
||||
return $this->errorCode;
|
||||
}
|
||||
|
||||
public function getErrors(): array {
|
||||
return $this->errors;
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
/**
|
||||
* USE: An application state that should not occur. Can be subclassed for feature-specific exceptions.
|
||||
* API: 500 Server Error
|
||||
*/
|
||||
class InvalidStateException extends RuntimeException {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
/**
|
||||
* USE: When the main resource we're interested in doesn't exist.
|
||||
* API: 404 Not Found
|
||||
*/
|
||||
class NotFoundException extends UnexpectedValueException {
|
||||
protected $statusCode = 404;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
/**
|
||||
* USE: Generic runtime error. When possible, use a more specific exception instead.
|
||||
* API: 500 Server Error
|
||||
*/
|
||||
class RuntimeException extends Exception {
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
/**
|
||||
* USE: When wrong data VALUE is received.
|
||||
* API: 400 Bad Request
|
||||
*/
|
||||
class UnexpectedValueException extends RuntimeException {
|
||||
protected $statusCode = 400;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\AutomationRunLog;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
|
||||
class Hooks {
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wordPress
|
||||
) {
|
||||
$this->wordPress = $wordPress;
|
||||
}
|
||||
|
||||
public const INITIALIZE = 'mailpoet/automation/initialize';
|
||||
public const API_INITIALIZE = 'mailpoet/automation/api/initialize';
|
||||
public const TRIGGER = 'mailpoet/automation/trigger';
|
||||
public const AUTOMATION_STEP = 'mailpoet/automation/step';
|
||||
|
||||
public const EDITOR_BEFORE_LOAD = 'mailpoet/automation/editor/before_load';
|
||||
|
||||
public const AUTOMATION_BEFORE_SAVE = 'mailpoet/automation/before_save';
|
||||
public const AUTOMATION_STEP_BEFORE_SAVE = 'mailpoet/automation/step/before_save';
|
||||
|
||||
public const AUTOMATION_STEP_LOG_AFTER_RUN = 'mailpoet/automation/step/log_after_run';
|
||||
|
||||
public const AUTOMATION_RUN_CREATE = 'mailpoet/automation/run/create';
|
||||
|
||||
public function doAutomationBeforeSave(Automation $automation): void {
|
||||
$this->wordPress->doAction(self::AUTOMATION_BEFORE_SAVE, $automation);
|
||||
}
|
||||
|
||||
public function doAutomationStepBeforeSave(Step $step, Automation $automation): void {
|
||||
$this->wordPress->doAction(self::AUTOMATION_STEP_BEFORE_SAVE, $step, $automation);
|
||||
}
|
||||
|
||||
public function doAutomationStepByKeyBeforeSave(Step $step, Automation $automation): void {
|
||||
$this->wordPress->doAction(self::AUTOMATION_STEP_BEFORE_SAVE . '/key=' . $step->getKey(), $step, $automation);
|
||||
}
|
||||
|
||||
public function doAutomationStepAfterRun(AutomationRunLog $automationRunLog): void {
|
||||
$this->wordPress->doAction(self::AUTOMATION_STEP_LOG_AFTER_RUN, $automationRunLog);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
interface Integration {
|
||||
public function register(Registry $registry): void;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Integration;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Control\StepRunController;
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
|
||||
interface Action extends Step {
|
||||
public function run(StepRunArgs $args, StepRunController $controller): void;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Integration;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Filter as FilterData;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
interface Filter {
|
||||
public function getFieldType(): string;
|
||||
|
||||
/** @return array<string, string> */
|
||||
public function getConditions(): array;
|
||||
|
||||
public function getArgsSchema(string $condition): ObjectSchema;
|
||||
|
||||
public function getFieldParams(FilterData $data): array;
|
||||
|
||||
/** @param mixed $value */
|
||||
public function matches(FilterData $data, $value): bool;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Integration;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
interface Payload {
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Integration;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\StepValidationArgs;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
interface Step {
|
||||
public function getKey(): string;
|
||||
|
||||
public function getName(): string;
|
||||
|
||||
public function getArgsSchema(): ObjectSchema;
|
||||
|
||||
/** @return string[] */
|
||||
public function getSubjectKeys(): array;
|
||||
|
||||
public function validate(StepValidationArgs $args): void;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Integration;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
/**
|
||||
* @template-covariant T of Payload
|
||||
*/
|
||||
interface Subject {
|
||||
public function getKey(): string;
|
||||
|
||||
public function getName(): string;
|
||||
|
||||
public function getArgsSchema(): ObjectSchema;
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array;
|
||||
|
||||
/** @return T */
|
||||
public function getPayload(SubjectData $subjectData): Payload;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Integration;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Subject;
|
||||
|
||||
interface SubjectTransformer {
|
||||
public function transform(Subject $data): ?Subject;
|
||||
|
||||
public function returns(): string;
|
||||
|
||||
public function accepts(): string;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Integration;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
|
||||
interface Trigger extends Step {
|
||||
public function registerHooks(): void;
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Integration;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException;
|
||||
|
||||
class ValidationException extends UnexpectedValueException {
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Mappers;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\AutomationStatistics;
|
||||
use MailPoet\Automation\Engine\Data\NextStep;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStatisticsStorage;
|
||||
|
||||
class AutomationMapper {
|
||||
/** @var AutomationStatisticsStorage */
|
||||
private $statisticsStorage;
|
||||
|
||||
public function __construct(
|
||||
AutomationStatisticsStorage $statisticsStorage
|
||||
) {
|
||||
$this->statisticsStorage = $statisticsStorage;
|
||||
}
|
||||
|
||||
public function buildAutomation(Automation $automation, AutomationStatistics $statistics = null): array {
|
||||
|
||||
return [
|
||||
'id' => $automation->getId(),
|
||||
'name' => $automation->getName(),
|
||||
'status' => $automation->getStatus(),
|
||||
'created_at' => $automation->getCreatedAt()->format(DateTimeImmutable::W3C),
|
||||
'updated_at' => $automation->getUpdatedAt()->format(DateTimeImmutable::W3C),
|
||||
'activated_at' => $automation->getActivatedAt() ? $automation->getActivatedAt()->format(DateTimeImmutable::W3C) : null,
|
||||
'author' => [
|
||||
'id' => $automation->getAuthor()->ID,
|
||||
'name' => $automation->getAuthor()->display_name,
|
||||
],
|
||||
'stats' => $statistics ? $statistics->toArray() : $this->statisticsStorage->getAutomationStats($automation->getId())->toArray(),
|
||||
'steps' => array_map(function (Step $step) {
|
||||
return [
|
||||
'id' => $step->getId(),
|
||||
'type' => $step->getType(),
|
||||
'key' => $step->getKey(),
|
||||
'args' => $step->getArgs(),
|
||||
'next_steps' => array_map(function (NextStep $nextStep) {
|
||||
return $nextStep->toArray();
|
||||
}, $step->getNextSteps()),
|
||||
'filters' => $step->getFilters() ? $step->getFilters()->toArray() : null,
|
||||
];
|
||||
}, $automation->getSteps()),
|
||||
'meta' => (object)$automation->getAllMetas(),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param Automation[] $automations */
|
||||
public function buildAutomationList(array $automations): array {
|
||||
$statistics = $this->statisticsStorage->getAutomationStatisticsForAutomations(...$automations);
|
||||
return array_map(function (Automation $automation) use ($statistics) {
|
||||
return $this->buildAutomationListItem($automation, $statistics[$automation->getId()]);
|
||||
}, $automations);
|
||||
}
|
||||
|
||||
private function buildAutomationListItem(Automation $automation, AutomationStatistics $statistics): array {
|
||||
return [
|
||||
'id' => $automation->getId(),
|
||||
'name' => $automation->getName(),
|
||||
'status' => $automation->getStatus(),
|
||||
'created_at' => $automation->getCreatedAt()->format(DateTimeImmutable::W3C),
|
||||
'updated_at' => $automation->getUpdatedAt()->format(DateTimeImmutable::W3C),
|
||||
'stats' => $statistics->toArray(),
|
||||
'activated_at' => $automation->getActivatedAt() ? $automation->getActivatedAt()->format(DateTimeImmutable::W3C) : null,
|
||||
'author' => [
|
||||
'id' => $automation->getAuthor()->ID,
|
||||
'name' => $automation->getAuthor()->display_name,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Control\RootStep;
|
||||
use MailPoet\Automation\Engine\Data\AutomationTemplate;
|
||||
use MailPoet\Automation\Engine\Data\AutomationTemplateCategory;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Integration\Action;
|
||||
use MailPoet\Automation\Engine\Integration\Filter;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Step;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Engine\Integration\SubjectTransformer;
|
||||
use MailPoet\Automation\Engine\Integration\Trigger;
|
||||
|
||||
class Registry {
|
||||
/** @var array<string, AutomationTemplate> */
|
||||
private $templates;
|
||||
|
||||
/** @var array<string, AutomationTemplateCategory> */
|
||||
private $templateCategories = [];
|
||||
|
||||
/** @var array<string, Step> */
|
||||
private $steps = [];
|
||||
|
||||
/** @var array<string, Subject<Payload>> */
|
||||
private $subjects = [];
|
||||
|
||||
/** @var SubjectTransformer[] */
|
||||
private $subjectTransformers = [];
|
||||
|
||||
/** @var array<string, Field>|null */
|
||||
private $fields = null;
|
||||
|
||||
/** @var array<string, Filter> */
|
||||
private $filters = [];
|
||||
|
||||
/** @var array<string, Trigger> */
|
||||
private $triggers = [];
|
||||
|
||||
/** @var array<string, Action> */
|
||||
private $actions = [];
|
||||
|
||||
/** @var array<string, callable> */
|
||||
private $contextFactories = [];
|
||||
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
public function __construct(
|
||||
RootStep $rootStep,
|
||||
WordPress $wordPress
|
||||
) {
|
||||
$this->wordPress = $wordPress;
|
||||
$this->steps[$rootStep->getKey()] = $rootStep;
|
||||
}
|
||||
|
||||
public function setupTemplateCategories(): void {
|
||||
$this->templateCategories = [
|
||||
'welcome' => new AutomationTemplateCategory('welcome', __('Welcome', 'mailpoet')),
|
||||
'abandoned-cart' => new AutomationTemplateCategory('abandoned-cart', __('Abandoned Cart', 'mailpoet')),
|
||||
'reengagement' => new AutomationTemplateCategory('reengagement', __('Re-engagement', 'mailpoet')),
|
||||
'woocommerce' => new AutomationTemplateCategory('woocommerce', __('WooCommerce', 'mailpoet')),
|
||||
];
|
||||
}
|
||||
|
||||
public function addTemplate(AutomationTemplate $template): void {
|
||||
$category = $template->getCategory();
|
||||
$templateCategories = $this->getTemplateCategories();
|
||||
|
||||
if (!isset($templateCategories[$category])) {
|
||||
throw InvalidStateException::create()->withMessage(
|
||||
sprintf("Category '%s' was not registered", $category)
|
||||
);
|
||||
}
|
||||
|
||||
$this->templates[$template->getSlug()] = $template;
|
||||
|
||||
// keep coming soon templates at the end
|
||||
uasort(
|
||||
$this->templates,
|
||||
function (AutomationTemplate $a, AutomationTemplate $b): int {
|
||||
if ($a->getType() === AutomationTemplate::TYPE_COMING_SOON) {
|
||||
return 1;
|
||||
}
|
||||
if ($b->getType() === AutomationTemplate::TYPE_COMING_SOON) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function getTemplate(string $slug): ?AutomationTemplate {
|
||||
return $this->getTemplates()[$slug] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, AutomationTemplate> */
|
||||
public function getTemplates(string $category = null): array {
|
||||
return $category
|
||||
? array_filter(
|
||||
$this->templates,
|
||||
function(AutomationTemplate $template) use ($category): bool {
|
||||
return $template->getCategory() === $category;
|
||||
}
|
||||
)
|
||||
: $this->templates;
|
||||
}
|
||||
|
||||
public function removeTemplate(string $slug): void {
|
||||
unset($this->templates[$slug]);
|
||||
}
|
||||
|
||||
/** @return array<string, AutomationTemplateCategory> */
|
||||
public function getTemplateCategories(): array {
|
||||
if (empty($this->templateCategories)) {
|
||||
$this->setupTemplateCategories();
|
||||
}
|
||||
return $this->templateCategories;
|
||||
}
|
||||
|
||||
/** @param Subject<Payload> $subject */
|
||||
public function addSubject(Subject $subject): void {
|
||||
$key = $subject->getKey();
|
||||
if (isset($this->subjects[$key])) {
|
||||
throw new \Exception(); // TODO
|
||||
}
|
||||
$this->subjects[$key] = $subject;
|
||||
|
||||
// reset fields cache
|
||||
$this->fields = null;
|
||||
}
|
||||
|
||||
/** @return Subject<Payload>|null */
|
||||
public function getSubject(string $key): ?Subject {
|
||||
return $this->subjects[$key] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, Subject<Payload>> */
|
||||
public function getSubjects(): array {
|
||||
return $this->subjects;
|
||||
}
|
||||
|
||||
public function addSubjectTransformer(SubjectTransformer $transformer): void {
|
||||
$this->subjectTransformers[] = $transformer;
|
||||
}
|
||||
|
||||
public function getSubjectTransformers(): array {
|
||||
return $this->subjectTransformers;
|
||||
}
|
||||
|
||||
public function getField(string $key): ?Field {
|
||||
return $this->getFields()[$key] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, Field> */
|
||||
public function getFields(): array {
|
||||
// add fields lazily (on the first call)
|
||||
if ($this->fields === null) {
|
||||
$this->fields = [];
|
||||
foreach ($this->subjects as $subject) {
|
||||
foreach ($subject->getFields() as $field) {
|
||||
$this->addField($field);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->fields ?? [];
|
||||
}
|
||||
|
||||
public function addFilter(Filter $filter): void {
|
||||
$fieldType = $filter->getFieldType();
|
||||
if (isset($this->filters[$fieldType])) {
|
||||
throw new \Exception(); // TODO
|
||||
}
|
||||
$this->filters[$fieldType] = $filter;
|
||||
}
|
||||
|
||||
public function getFilter(string $fieldType): ?Filter {
|
||||
return $this->filters[$fieldType] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, Filter> */
|
||||
public function getFilters(): array {
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function addStep(Step $step): void {
|
||||
if ($step instanceof Trigger) {
|
||||
$this->addTrigger($step);
|
||||
} elseif ($step instanceof Action) {
|
||||
$this->addAction($step);
|
||||
}
|
||||
|
||||
// TODO: allow adding any other step implementations?
|
||||
}
|
||||
|
||||
public function getStep(string $key): ?Step {
|
||||
return $this->steps[$key] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, Step> */
|
||||
public function getSteps(): array {
|
||||
return $this->steps;
|
||||
}
|
||||
|
||||
public function addTrigger(Trigger $trigger): void {
|
||||
$key = $trigger->getKey();
|
||||
if (isset($this->steps[$key]) || isset($this->triggers[$key])) {
|
||||
throw new \Exception(); // TODO
|
||||
}
|
||||
$this->steps[$key] = $trigger;
|
||||
$this->triggers[$key] = $trigger;
|
||||
}
|
||||
|
||||
public function getTrigger(string $key): ?Trigger {
|
||||
return $this->triggers[$key] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, Trigger> */
|
||||
public function getTriggers(): array {
|
||||
return $this->triggers;
|
||||
}
|
||||
|
||||
public function addAction(Action $action): void {
|
||||
$key = $action->getKey();
|
||||
if (isset($this->steps[$key]) || isset($this->actions[$key])) {
|
||||
throw new \Exception(); // TODO
|
||||
}
|
||||
$this->steps[$key] = $action;
|
||||
$this->actions[$key] = $action;
|
||||
}
|
||||
|
||||
public function getAction(string $key): ?Action {
|
||||
return $this->actions[$key] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, Action> */
|
||||
public function getActions(): array {
|
||||
return $this->actions;
|
||||
}
|
||||
|
||||
public function addContextFactory(string $key, callable $factory): void {
|
||||
$this->contextFactories[$key] = $factory;
|
||||
}
|
||||
|
||||
/** @return callable[] */
|
||||
public function getContextFactories(): array {
|
||||
return $this->contextFactories;
|
||||
}
|
||||
|
||||
public function onBeforeAutomationSave(callable $callback, int $priority = 10): void {
|
||||
$this->wordPress->addAction(Hooks::AUTOMATION_BEFORE_SAVE, $callback, $priority);
|
||||
}
|
||||
|
||||
public function onBeforeAutomationStepSave(callable $callback, string $key = null, int $priority = 10): void {
|
||||
$keyPart = $key ? "/key=$key" : '';
|
||||
$this->wordPress->addAction(Hooks::AUTOMATION_STEP_BEFORE_SAVE . $keyPart, $callback, $priority, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used only internally. Fields are added lazily from subjects.
|
||||
*/
|
||||
private function addField(Field $field): void {
|
||||
$key = $field->getKey();
|
||||
if (isset($this->fields[$key])) {
|
||||
throw new \Exception(); // TODO
|
||||
}
|
||||
$this->fields[$key] = $field;
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Storage;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\AutomationRunLog;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\InvalidStateException;
|
||||
|
||||
class AutomationRunLogStorage {
|
||||
/** @var string */
|
||||
private $table;
|
||||
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
$this->table = $wpdb->prefix . 'mailpoet_automation_run_logs';
|
||||
}
|
||||
|
||||
public function createAutomationRunLog(AutomationRunLog $automationRunLog): int {
|
||||
global $wpdb;
|
||||
$result = $wpdb->insert($this->table, $automationRunLog->toArray());
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
return $wpdb->insert_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
|
||||
public function updateAutomationRunLog(AutomationRunLog $automationRunLog): void {
|
||||
global $wpdb;
|
||||
$result = $wpdb->update($this->table, $automationRunLog->toArray(), ['id' => $automationRunLog->getId()]);
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
public function getAutomationRunStatisticsForAutomationInTimeFrame(int $automationId, string $status, \DateTimeImmutable $after, \DateTimeImmutable $before, int $versionId = null): array {
|
||||
global $wpdb;
|
||||
$andWhere = $versionId ? 'AND run.version_id = %d' : '';
|
||||
$results = $wpdb->get_results(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The number of replacements is dynamic.
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT COUNT(log.id) AS count, log.step_id
|
||||
FROM %i AS log
|
||||
JOIN %i AS run ON log.automation_run_id = run.id
|
||||
WHERE run.automation_id = %d AND log.status = %s AND run.created_at BETWEEN %s AND %s
|
||||
' . $andWhere . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The condition uses placeholders. */ '
|
||||
GROUP BY log.step_id
|
||||
',
|
||||
array_merge(
|
||||
[
|
||||
$this->table,
|
||||
$wpdb->prefix . 'mailpoet_automation_runs',
|
||||
$automationId,
|
||||
$status,
|
||||
$after->format('Y-m-d H:i:s'),
|
||||
$before->format('Y-m-d H:i:s'),
|
||||
],
|
||||
$versionId ? [$versionId] : []
|
||||
)
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
return is_array($results) ? $results : [];
|
||||
}
|
||||
|
||||
public function getAutomationRunLog(int $id): ?AutomationRunLog {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->get_row(
|
||||
$wpdb->prepare('SELECT * FROM %i WHERE id = %d', $this->table, $id),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$data = (array)$result;
|
||||
return AutomationRunLog::fromArray($data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAutomationRunLogByRunAndStepId(int $runId, string $stepId): ?AutomationRunLog {
|
||||
global $wpdb;
|
||||
$result = $wpdb->get_row(
|
||||
$wpdb->prepare('SELECT * FROM %i WHERE automation_run_id = %d AND step_id = %s', $this->table, $runId, $stepId),
|
||||
ARRAY_A
|
||||
);
|
||||
return $result ? AutomationRunLog::fromArray((array)$result) : null;
|
||||
}
|
||||
|
||||
/** @return AutomationRunLog[] */
|
||||
public function getLogsForAutomationRun(int $automationRunId): array {
|
||||
global $wpdb;
|
||||
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT *
|
||||
FROM %i
|
||||
WHERE automation_run_id = %d
|
||||
ORDER BY id ASC
|
||||
',
|
||||
$this->table,
|
||||
$automationRunId
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if (!is_array($results)) {
|
||||
throw InvalidStateException::create();
|
||||
}
|
||||
|
||||
if ($results) {
|
||||
return array_map(function($data) {
|
||||
/** @var array $data - for PHPStan because it conflicts with expected callable(mixed): mixed)|null */
|
||||
return AutomationRunLog::fromArray($data);
|
||||
}, $results);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function truncate(): void {
|
||||
global $wpdb;
|
||||
$wpdb->query($wpdb->prepare('TRUNCATE %i', $this->table));
|
||||
}
|
||||
|
||||
private function throwDatabaseError(): void {
|
||||
global $wpdb;
|
||||
throw Exceptions::databaseError($wpdb->last_error); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Storage;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\AutomationRun;
|
||||
use MailPoet\Automation\Engine\Data\Subject;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
|
||||
class AutomationRunStorage {
|
||||
/** @var string */
|
||||
private $table;
|
||||
|
||||
/** @var string */
|
||||
private $subjectTable;
|
||||
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
$this->table = $wpdb->prefix . 'mailpoet_automation_runs';
|
||||
$this->subjectTable = $wpdb->prefix . 'mailpoet_automation_run_subjects';
|
||||
}
|
||||
|
||||
public function createAutomationRun(AutomationRun $automationRun): int {
|
||||
global $wpdb;
|
||||
$automationTableData = $automationRun->toArray();
|
||||
$subjectTableData = $automationTableData['subjects'];
|
||||
unset($automationTableData['subjects']);
|
||||
$result = $wpdb->insert($this->table, $automationTableData);
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
$automationRunId = $wpdb->insert_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
|
||||
if (!$subjectTableData) {
|
||||
//We allow for AutomationRuns with no subjects.
|
||||
return $automationRunId;
|
||||
}
|
||||
|
||||
|
||||
$values = [];
|
||||
foreach ($subjectTableData as $entry) {
|
||||
$values[] = $wpdb->prepare('(%d,%s,%s,%s)', $automationRunId, $entry['key'], $entry['args'], $entry['hash']);
|
||||
}
|
||||
|
||||
$result = $wpdb->query(
|
||||
$wpdb->prepare('INSERT INTO %i (`automation_run_id`, `key`, `args`, `hash`) VALUES ', $this->subjectTable) . implode(',', $values)
|
||||
);
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
|
||||
return $automationRunId;
|
||||
}
|
||||
|
||||
public function getAutomationRun(int $id): ?AutomationRun {
|
||||
global $wpdb;
|
||||
|
||||
$data = $wpdb->get_row(
|
||||
$wpdb->prepare('SELECT * FROM %i WHERE id = %d', $this->table, $id),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if (!is_array($data) || !$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$subjects = $wpdb->get_results(
|
||||
$wpdb->prepare('SELECT * FROM %i WHERE automation_run_id = %d', $this->subjectTable, $id),
|
||||
ARRAY_A
|
||||
);
|
||||
$data['subjects'] = is_array($subjects) ? $subjects : [];
|
||||
return AutomationRun::fromArray((array)$data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Automation $automation
|
||||
* @return AutomationRun[]
|
||||
*/
|
||||
public function getAutomationRunsForAutomation(Automation $automation): array {
|
||||
global $wpdb;
|
||||
|
||||
$automationRuns = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
'SELECT * FROM %i WHERE automation_id = %d ORDER BY id',
|
||||
$this->table,
|
||||
$automation->getId()
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
if (!is_array($automationRuns) || !$automationRuns) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$automationRunIds = array_column($automationRuns, 'id');
|
||||
$subjects = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT *
|
||||
FROM %i
|
||||
WHERE automation_run_id IN (' . implode(',', array_fill(0, count($automationRunIds), '%s')) . ')
|
||||
ORDER BY automation_run_id, id
|
||||
',
|
||||
array_merge(
|
||||
[$this->subjectTable],
|
||||
$automationRunIds,
|
||||
)
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map(
|
||||
function($runData) use ($subjects): AutomationRun {
|
||||
/** @var array $runData - PHPStan expects as array_map first parameter (callable(mixed): mixed)|null */
|
||||
$runData['subjects'] = array_values(array_filter(
|
||||
is_array($subjects) ? $subjects : [],
|
||||
function($subjectData) use ($runData): bool {
|
||||
/** @var array $subjectData - PHPStan expects as array_map first parameter (callable(mixed): mixed)|null */
|
||||
return (int)$subjectData['automation_run_id'] === (int)$runData['id'];
|
||||
}
|
||||
));
|
||||
return AutomationRun::fromArray($runData);
|
||||
},
|
||||
$automationRuns
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Automation $automation
|
||||
* @return int
|
||||
*/
|
||||
public function getCountByAutomationAndSubject(Automation $automation, Subject $subject): int {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT count(DISTINCT runs.id) AS count FROM %i AS runs
|
||||
JOIN %i AS subjects ON runs.id = subjects.automation_run_id
|
||||
WHERE runs.automation_id = %d
|
||||
AND subjects.hash = %s
|
||||
',
|
||||
$this->table,
|
||||
$this->subjectTable,
|
||||
$automation->getId(),
|
||||
$subject->getHash()
|
||||
)
|
||||
);
|
||||
|
||||
return $result ? (int)current($result) : 0;
|
||||
}
|
||||
|
||||
public function getCountForAutomation(Automation $automation, string ...$status): int {
|
||||
global $wpdb;
|
||||
|
||||
if (!count($status)) {
|
||||
$result = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
'SELECT COUNT(id) as count FROM %i WHERE automation_id = %d',
|
||||
$this->table,
|
||||
$automation->getId()
|
||||
)
|
||||
);
|
||||
return $result ? (int)current($result) : 0;
|
||||
}
|
||||
|
||||
$result = $wpdb->get_col(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The number of replacements is dynamic.
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT COUNT(id) as count
|
||||
FROM %i
|
||||
WHERE automation_id = %d
|
||||
AND status IN (' . implode(',', array_fill(0, count($status), '%s')) . ')
|
||||
',
|
||||
$this->table,
|
||||
$automation->getId(),
|
||||
...$status
|
||||
)
|
||||
);
|
||||
return $result ? (int)current($result) : 0;
|
||||
}
|
||||
|
||||
public function updateStatus(int $id, string $status): void {
|
||||
global $wpdb;
|
||||
$result = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
UPDATE %i
|
||||
SET status = %s, updated_at = current_timestamp()
|
||||
WHERE id = %d
|
||||
',
|
||||
$this->table,
|
||||
$status,
|
||||
$id
|
||||
)
|
||||
);
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
public function updateNextStep(int $id, ?string $nextStepId): void {
|
||||
global $wpdb;
|
||||
$result = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
UPDATE %i
|
||||
SET next_step_id = %s, updated_at = current_timestamp()
|
||||
WHERE id = %d
|
||||
',
|
||||
$this->table,
|
||||
$nextStepId,
|
||||
$id
|
||||
)
|
||||
);
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
public function getAutomationStepStatisticForTimeFrame(int $automationId, string $status, \DateTimeImmutable $after, \DateTimeImmutable $before, int $versionId = null): array {
|
||||
global $wpdb;
|
||||
$andWhere = $versionId ? 'AND version_id = %d' : '';
|
||||
$result = $wpdb->get_results(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The number of replacements is dynamic.
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT COUNT(id) AS count, next_step_id
|
||||
FROM %i AS log
|
||||
WHERE automation_id = %d AND status = %s AND created_at BETWEEN %s AND %s
|
||||
' . $andWhere . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The condition uses placeholders. */ '
|
||||
GROUP BY next_step_id
|
||||
',
|
||||
array_merge(
|
||||
[
|
||||
$this->table,
|
||||
$automationId,
|
||||
$status,
|
||||
$after->format('Y-m-d H:i:s'),
|
||||
$before->format('Y-m-d H:i:s'),
|
||||
],
|
||||
$versionId ? [$versionId] : []
|
||||
)
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
return is_array($result) ? $result : [];
|
||||
}
|
||||
|
||||
public function truncate(): void {
|
||||
global $wpdb;
|
||||
$wpdb->query($wpdb->prepare('TRUNCATE %i', $this->table));
|
||||
$wpdb->query($wpdb->prepare('TRUNCATE %i', $this->subjectTable));
|
||||
}
|
||||
|
||||
private function throwDatabaseError(): void {
|
||||
global $wpdb;
|
||||
throw Exceptions::databaseError($wpdb->last_error); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Storage;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\AutomationRun;
|
||||
use MailPoet\Automation\Engine\Data\AutomationStatistics;
|
||||
|
||||
class AutomationStatisticsStorage {
|
||||
/** @var string */
|
||||
private $table;
|
||||
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
$this->table = $wpdb->prefix . 'mailpoet_automation_runs';
|
||||
}
|
||||
|
||||
/** @return AutomationStatistics[] */
|
||||
public function getAutomationStatisticsForAutomations(Automation ...$automations): array {
|
||||
if (empty($automations)) {
|
||||
return [];
|
||||
}
|
||||
$automationIds = array_map(
|
||||
function(Automation $automation): int {
|
||||
return $automation->getId();
|
||||
},
|
||||
$automations
|
||||
);
|
||||
|
||||
$data = $this->getStatistics($automationIds);
|
||||
$statistics = [];
|
||||
foreach ($automationIds as $id) {
|
||||
$statistics[$id] = new AutomationStatistics(
|
||||
$id,
|
||||
(int)($data[$id]['total'] ?? 0),
|
||||
(int)($data[$id]['running'] ?? 0)
|
||||
);
|
||||
}
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
public function getAutomationStats(int $automationId, int $versionId = null, \DateTimeImmutable $after = null, \DateTimeImmutable $before = null): AutomationStatistics {
|
||||
$data = $this->getStatistics([$automationId], $versionId, $after, $before);
|
||||
return new AutomationStatistics(
|
||||
$automationId,
|
||||
(int)($data[$automationId]['total'] ?? 0),
|
||||
(int)($data[$automationId]['running'] ?? 0),
|
||||
$versionId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $automationIds
|
||||
* @return array<int, array{id: int, total: int, running: int}>
|
||||
*/
|
||||
private function getStatistics(array $automationIds, int $versionId = null, \DateTimeImmutable $after = null, \DateTimeImmutable $before = null): array {
|
||||
global $wpdb;
|
||||
$totalSubquery = $this->getStatsQuery($automationIds, $versionId, $after, $before);
|
||||
$runningSubquery = $this->getStatsQuery($automationIds, $versionId, $after, $before, AutomationRun::STATUS_RUNNING);
|
||||
|
||||
$results = (array)$wpdb->get_results(
|
||||
'
|
||||
SELECT t.id, t.count AS total, r.count AS running
|
||||
FROM (' . $totalSubquery /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The subquery was already prepared. */ . ') t
|
||||
LEFT JOIN (' . $runningSubquery /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The subquery was already prepared. */ . ') r ON t.id = r.id
|
||||
',
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
/** @var array{id: int, total: int, running: int} $results */
|
||||
return array_combine(array_column($results, 'id'), $results) ?: [];
|
||||
}
|
||||
|
||||
private function getStatsQuery(array $automationIds, int $versionId = null, \DateTimeImmutable $after = null, \DateTimeImmutable $before = null, string $status = null): string {
|
||||
global $wpdb;
|
||||
|
||||
$versionCondition = $versionId ? 'AND version_id = %d' : '';
|
||||
$statusCondition = $status ? 'AND status = %s' : '';
|
||||
$dateCondition = $after !== null && $before !== null ? 'AND created_at BETWEEN %s AND %s' : '';
|
||||
|
||||
$coditions = "$versionCondition $statusCondition $dateCondition";
|
||||
$query = $wpdb->prepare(
|
||||
'
|
||||
SELECT automation_id AS id, COUNT(*) AS count
|
||||
FROM %i
|
||||
WHERE automation_id IN (' . implode(',', array_fill(0, count($automationIds), '%d')) . ')
|
||||
' . $coditions . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The conditions use placeholders. */ '
|
||||
GROUP BY automation_id
|
||||
',
|
||||
array_merge(
|
||||
[$this->table],
|
||||
$automationIds,
|
||||
$versionId ? [$versionId] : [],
|
||||
$status ? [$status] : [],
|
||||
$after !== null && $before !== null ? [$after->format('Y-m-d H:i:s'), $before->format('Y-m-d H:i:s')] : []
|
||||
)
|
||||
);
|
||||
return strval($query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Storage;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Data\Subject;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Integration\Trigger;
|
||||
|
||||
/**
|
||||
* @phpstan-type VersionDate array{id: int, created_at: \DateTimeImmutable}
|
||||
*/
|
||||
class AutomationStorage {
|
||||
/** @var string */
|
||||
private $automationsTable;
|
||||
|
||||
/** @var string */
|
||||
private $versionsTable;
|
||||
|
||||
/** @var string */
|
||||
private $triggersTable;
|
||||
|
||||
/** @var string */
|
||||
private $runsTable;
|
||||
|
||||
/** @var string */
|
||||
private $subjectsTable;
|
||||
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
$this->automationsTable = $wpdb->prefix . 'mailpoet_automations';
|
||||
$this->versionsTable = $wpdb->prefix . 'mailpoet_automation_versions';
|
||||
$this->triggersTable = $wpdb->prefix . 'mailpoet_automation_triggers';
|
||||
$this->runsTable = $wpdb->prefix . 'mailpoet_automation_runs';
|
||||
$this->subjectsTable = $wpdb->prefix . 'mailpoet_automation_run_subjects';
|
||||
}
|
||||
|
||||
public function createAutomation(Automation $automation): int {
|
||||
global $wpdb;
|
||||
$automationHeaderData = $this->getAutomationHeaderData($automation);
|
||||
unset($automationHeaderData['id']);
|
||||
$result = $wpdb->insert($this->automationsTable, $automationHeaderData);
|
||||
if (!$result) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
$id = $wpdb->insert_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$this->insertAutomationVersion($id, $automation);
|
||||
$this->insertAutomationTriggers($id, $automation);
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function updateAutomation(Automation $automation): void {
|
||||
global $wpdb;
|
||||
$oldRecord = $this->getAutomation($automation->getId());
|
||||
if ($oldRecord && $oldRecord->equals($automation)) {
|
||||
return;
|
||||
}
|
||||
$result = $wpdb->update($this->automationsTable, $this->getAutomationHeaderData($automation), ['id' => $automation->getId()]);
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
$this->insertAutomationVersion($automation->getId(), $automation);
|
||||
$this->insertAutomationTriggers($automation->getId(), $automation);
|
||||
}
|
||||
|
||||
public function getAutomationVersionDates(int $automationId): array {
|
||||
global $wpdb;
|
||||
|
||||
$data = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
'SELECT id, created_at FROM %i WHERE automation_id = %d ORDER BY id DESC',
|
||||
$this->versionsTable,
|
||||
$automationId
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return is_array($data) ? array_map(
|
||||
function($row): array {
|
||||
/** @var array{id: string, created_at: string} $row */
|
||||
return [
|
||||
'id' => absint($row['id']),
|
||||
'created_at' => new \DateTimeImmutable($row['created_at']),
|
||||
];
|
||||
},
|
||||
$data
|
||||
) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $versionIds
|
||||
* @return Automation[]
|
||||
*/
|
||||
public function getAutomationWithDifferentVersions(array $versionIds): array {
|
||||
global $wpdb;
|
||||
if (!$versionIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$versionIds = array_map('intval', $versionIds);
|
||||
$data = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT a.*, v.id AS version_id, v.steps
|
||||
FROM %i as a, %i as v
|
||||
WHERE v.automation_id = a.id AND v.id IN (' . implode(',', array_fill(0, count($versionIds), '%d')) . ')
|
||||
ORDER BY v.id DESC
|
||||
',
|
||||
array_merge(
|
||||
[
|
||||
$this->automationsTable,
|
||||
$this->versionsTable,
|
||||
],
|
||||
$versionIds
|
||||
)
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
return is_array($data) ? array_map(
|
||||
function($row): Automation {
|
||||
return Automation::fromArray((array)$row);
|
||||
},
|
||||
$data
|
||||
) : [];
|
||||
}
|
||||
|
||||
public function getAutomation(int $automationId, int $versionId = null): ?Automation {
|
||||
global $wpdb;
|
||||
|
||||
if ($versionId) {
|
||||
$automations = $this->getAutomationWithDifferentVersions([$versionId]);
|
||||
return $automations ? $automations[0] : null;
|
||||
}
|
||||
|
||||
$data = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT a.*, v.id AS version_id, v.steps
|
||||
FROM %i as a, %i as v
|
||||
WHERE v.automation_id = a.id AND a.id = %d
|
||||
ORDER BY v.id DESC
|
||||
LIMIT 1
|
||||
',
|
||||
$this->automationsTable,
|
||||
$this->versionsTable,
|
||||
$automationId
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
return $data ? Automation::fromArray((array)$data) : null;
|
||||
}
|
||||
|
||||
/** @return Automation[] */
|
||||
public function getAutomations(array $status = null): array {
|
||||
global $wpdb;
|
||||
|
||||
$statusFilter = $status ? 'AND a.status IN (' . implode(',', array_fill(0, count($status), '%s')) . ')' : '';
|
||||
$data = $wpdb->get_results(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The number of replacements is dynamic.
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT a.*, v.id AS version_id, v.steps
|
||||
FROM %i AS a
|
||||
INNER JOIN %i as v ON (v.automation_id = a.id)
|
||||
WHERE v.id = (
|
||||
SELECT MAX(id) FROM %i WHERE automation_id = v.automation_id
|
||||
)
|
||||
' . $statusFilter . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The condition uses placeholders. */ '
|
||||
ORDER BY a.id DESC
|
||||
',
|
||||
$this->automationsTable,
|
||||
$this->versionsTable,
|
||||
$this->versionsTable,
|
||||
...($status ?? []),
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
return array_map(function ($automationData) {
|
||||
/** @var array $automationData - for PHPStan because it conflicts with expected callable(mixed): mixed)|null */
|
||||
return Automation::fromArray($automationData);
|
||||
}, (array)$data);
|
||||
}
|
||||
|
||||
/** @return int[] */
|
||||
public function getAutomationIdsBySubject(Subject $subject, array $runStatus = null, int $inTheLastSeconds = null): array {
|
||||
global $wpdb;
|
||||
|
||||
$statusFilter = $runStatus ? 'AND r.status IN (' . implode(',', array_fill(0, count($runStatus), '%s')) . ')' : '';
|
||||
$inTheLastFilter = isset($inTheLastSeconds) ? 'AND r.created_at > DATE_SUB(NOW(), INTERVAL %d SECOND)' : '';
|
||||
|
||||
$result = $wpdb->get_col(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The number of replacements is dynamic.
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT DISTINCT a.id
|
||||
FROM %i a
|
||||
INNER JOIN %i r ON r.automation_id = a.id
|
||||
INNER JOIN %i s ON s.automation_run_id = r.id
|
||||
WHERE s.hash = %s
|
||||
' . $statusFilter . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The condition uses placeholders. */ '
|
||||
' . $inTheLastFilter . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The condition uses placeholders. */ '
|
||||
ORDER BY a.id DESC
|
||||
',
|
||||
array_merge(
|
||||
[
|
||||
$this->automationsTable,
|
||||
$this->runsTable,
|
||||
$this->subjectsTable,
|
||||
$subject->getHash(),
|
||||
],
|
||||
$runStatus ?? [],
|
||||
$inTheLastSeconds ? [$inTheLastSeconds] : []
|
||||
)
|
||||
)
|
||||
);
|
||||
return array_map('intval', $result);
|
||||
}
|
||||
|
||||
public function getAutomationCount(): int {
|
||||
global $wpdb;
|
||||
return (int)$wpdb->get_var(
|
||||
$wpdb->prepare('SELECT COUNT(*) FROM %i', $this->automationsTable)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getActiveTriggerKeys(): array {
|
||||
global $wpdb;
|
||||
return $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT DISTINCT t.trigger_key
|
||||
FROM %i AS a
|
||||
JOIN %i as t
|
||||
WHERE a.status = %s AND a.id = t.automation_id
|
||||
ORDER BY trigger_key DESC
|
||||
',
|
||||
$this->automationsTable,
|
||||
$this->triggersTable,
|
||||
Automation::STATUS_ACTIVE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return Automation[] */
|
||||
public function getActiveAutomationsByTrigger(Trigger $trigger): array {
|
||||
return $this->getActiveAutomationsByTriggerKey($trigger->getKey());
|
||||
}
|
||||
|
||||
public function getActiveAutomationsByTriggerKey(string $triggerKey): array {
|
||||
global $wpdb;
|
||||
$data = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT a.*, v.id AS version_id, v.steps
|
||||
FROM %i AS a
|
||||
INNER JOIN %i as t ON (t.automation_id = a.id)
|
||||
INNER JOIN %i as v ON (v.automation_id = a.id)
|
||||
WHERE a.status = %s
|
||||
AND t.trigger_key = %s
|
||||
AND v.id = (
|
||||
SELECT MAX(id) FROM %i WHERE automation_id = v.automation_id
|
||||
)
|
||||
',
|
||||
$this->automationsTable,
|
||||
$this->triggersTable,
|
||||
$this->versionsTable,
|
||||
Automation::STATUS_ACTIVE,
|
||||
$triggerKey,
|
||||
$this->versionsTable
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
return array_map(function ($automationData) {
|
||||
/** @var array $automationData - for PHPStan because it conflicts with expected callable(mixed): mixed)|null */
|
||||
return Automation::fromArray($automationData);
|
||||
}, (array)$data);
|
||||
}
|
||||
|
||||
public function getCountOfActiveByTriggerKeysAndAction(array $triggerKeys, string $actionKey): int {
|
||||
global $wpdb;
|
||||
return (int)$wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT COUNT(*)
|
||||
FROM %i AS a
|
||||
INNER JOIN %i as t ON (t.automation_id = a.id) AND t.trigger_key IN (' . implode(',', array_fill(0, count($triggerKeys), '%s')) . ')
|
||||
INNER JOIN %i as v ON v.id = (SELECT MAX(id) FROM %i WHERE automation_id = a.id)
|
||||
WHERE a.status = %s
|
||||
AND v.steps LIKE %s
|
||||
',
|
||||
array_merge(
|
||||
[
|
||||
$this->automationsTable,
|
||||
$this->triggersTable,
|
||||
],
|
||||
$triggerKeys,
|
||||
[
|
||||
$this->versionsTable,
|
||||
$this->versionsTable,
|
||||
Automation::STATUS_ACTIVE,
|
||||
'%"' . $wpdb->esc_like($actionKey) . '"%',
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function deleteAutomation(Automation $automation): void {
|
||||
global $wpdb;
|
||||
$automationId = $automation->getId();
|
||||
$logsDeleted = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
DELETE FROM %i
|
||||
WHERE automation_run_id IN (
|
||||
SELECT id
|
||||
FROM %i
|
||||
WHERE automation_id = %d
|
||||
)
|
||||
',
|
||||
$wpdb->prefix . 'mailpoet_automation_run_logs',
|
||||
$this->runsTable,
|
||||
$automationId
|
||||
)
|
||||
);
|
||||
if ($logsDeleted === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
|
||||
$runsDeleted = $wpdb->delete($this->runsTable, ['automation_id' => $automationId]);
|
||||
if ($runsDeleted === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
|
||||
$versionsDeleted = $wpdb->delete($this->versionsTable, ['automation_id' => $automationId]);
|
||||
if ($versionsDeleted === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
|
||||
$triggersDeleted = $wpdb->delete($this->triggersTable, ['automation_id' => $automationId]);
|
||||
if ($triggersDeleted === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
|
||||
$automationDeleted = $wpdb->delete($this->automationsTable, ['id' => $automationId]);
|
||||
if ($automationDeleted === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
public function truncate(): void {
|
||||
global $wpdb;
|
||||
$result = $wpdb->query($wpdb->prepare('TRUNCATE %i', $this->automationsTable));
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
|
||||
$result = $wpdb->query($wpdb->prepare('TRUNCATE %i', $this->versionsTable));
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
|
||||
$result = $wpdb->query($wpdb->prepare('TRUNCATE %i', $this->triggersTable));
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
public function getNameColumnLength(): int {
|
||||
global $wpdb;
|
||||
$nameColumnLengthInfo = $wpdb->get_col_length($this->automationsTable, 'name');
|
||||
return is_array($nameColumnLengthInfo)
|
||||
? $nameColumnLengthInfo['length'] ?? 255
|
||||
: 255;
|
||||
}
|
||||
|
||||
private function getAutomationHeaderData(Automation $automation): array {
|
||||
$automationHeader = $automation->toArray();
|
||||
unset($automationHeader['steps']);
|
||||
return $automationHeader;
|
||||
}
|
||||
|
||||
private function insertAutomationVersion(int $automationId, Automation $automation): void {
|
||||
global $wpdb;
|
||||
$dateString = (new DateTimeImmutable())->format(DateTimeImmutable::W3C);
|
||||
$data = [
|
||||
'automation_id' => $automationId,
|
||||
'steps' => $automation->toArray()['steps'],
|
||||
'created_at' => $dateString,
|
||||
'updated_at' => $dateString,
|
||||
];
|
||||
$result = $wpdb->insert($this->versionsTable, $data);
|
||||
if (!$result) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
private function insertAutomationTriggers(int $automationId, Automation $automation): void {
|
||||
global $wpdb;
|
||||
$triggerKeys = [];
|
||||
foreach ($automation->getSteps() as $step) {
|
||||
if ($step->getType() === Step::TYPE_TRIGGER) {
|
||||
$triggerKeys[] = $step->getKey();
|
||||
}
|
||||
}
|
||||
|
||||
// insert/update
|
||||
if ($triggerKeys) {
|
||||
$result = $wpdb->query(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The number of replacements is dynamic.
|
||||
$wpdb->prepare(
|
||||
'INSERT IGNORE INTO %i (automation_id, trigger_key) VALUES ' . implode(',', array_fill(0, count($triggerKeys), '(%d, %s)')),
|
||||
array_merge(
|
||||
[$this->triggersTable],
|
||||
...array_map(function (string $key) use ($automationId) {
|
||||
return [$automationId, $key];
|
||||
}, $triggerKeys)
|
||||
)
|
||||
)
|
||||
);
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
// delete
|
||||
if ($triggerKeys) {
|
||||
$result = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
'DELETE FROM %i WHERE automation_id = %d AND trigger_key NOT IN (' . implode(',', array_fill(0, count($triggerKeys), '%s')) . ')',
|
||||
array_merge(
|
||||
[
|
||||
$this->triggersTable,
|
||||
$automationId,
|
||||
],
|
||||
$triggerKeys
|
||||
),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$result = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
'DELETE FROM %i WHERE automation_id = %d',
|
||||
$this->triggersTable,
|
||||
$automationId
|
||||
)
|
||||
);
|
||||
}
|
||||
if ($result === false) {
|
||||
$this->throwDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
private function throwDatabaseError(): void {
|
||||
global $wpdb;
|
||||
throw Exceptions::databaseError($wpdb->last_error); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Templates;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Filter;
|
||||
use MailPoet\Automation\Engine\Data\FilterGroup;
|
||||
use MailPoet\Automation\Engine\Data\Filters;
|
||||
use MailPoet\Automation\Engine\Data\NextStep;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Integration\Trigger;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class AutomationBuilder {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array<
|
||||
* array{
|
||||
* key: string,
|
||||
* args?: array<string, mixed>,
|
||||
* filters?: array{
|
||||
* operator: 'and' | 'or',
|
||||
* groups: array{
|
||||
* operator: 'and' | 'or',
|
||||
* filters: array{
|
||||
* field: string,
|
||||
* condition: string,
|
||||
* value: mixed,
|
||||
* }[],
|
||||
* }[],
|
||||
* },
|
||||
* }
|
||||
* > $sequence
|
||||
* @param array<string, mixed> $meta
|
||||
* @return Automation
|
||||
*/
|
||||
public function createFromSequence(string $name, array $sequence, array $meta = []): Automation {
|
||||
$steps = [];
|
||||
$nextSteps = [];
|
||||
foreach (array_reverse($sequence) as $data) {
|
||||
$stepKey = $data['key'];
|
||||
$automationStep = $this->registry->getStep($stepKey);
|
||||
if (!$automationStep) {
|
||||
continue;
|
||||
}
|
||||
$args = array_merge($this->getDefaultArgs($automationStep->getArgsSchema()), $data['args'] ?? []);
|
||||
$filters = isset($data['filters']) ? $this->getFilters($data['filters']) : null;
|
||||
$step = new Step(
|
||||
$this->uniqueId(),
|
||||
in_array(Trigger::class, (array)class_implements($automationStep)) ? Step::TYPE_TRIGGER : Step::TYPE_ACTION,
|
||||
$stepKey,
|
||||
$args,
|
||||
$nextSteps,
|
||||
$filters
|
||||
);
|
||||
$nextSteps = [new NextStep($step->getId())];
|
||||
$steps[$step->getId()] = $step;
|
||||
}
|
||||
$steps['root'] = new Step('root', 'root', 'core:root', [], $nextSteps);
|
||||
$steps = array_reverse($steps);
|
||||
$automation = new Automation(
|
||||
$name,
|
||||
$steps,
|
||||
wp_get_current_user()
|
||||
);
|
||||
foreach ($meta as $key => $value) {
|
||||
$automation->setMeta($key, $value);
|
||||
}
|
||||
return $automation;
|
||||
}
|
||||
|
||||
private function uniqueId(): string {
|
||||
return Security::generateRandomString(16);
|
||||
}
|
||||
|
||||
private function getDefaultArgs(ObjectSchema $argsSchema): array {
|
||||
$args = [];
|
||||
foreach ($argsSchema->toArray()['properties'] ?? [] as $name => $schema) {
|
||||
if (array_key_exists('default', $schema)) {
|
||||
$args[$name] = $schema['default'];
|
||||
}
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* operator: 'and' | 'or',
|
||||
* groups: array{
|
||||
* operator: 'and' | 'or',
|
||||
* filters: array{
|
||||
* field: string,
|
||||
* condition: string,
|
||||
* value: mixed,
|
||||
* params?: array<string, mixed>,
|
||||
* }[],
|
||||
* }[],
|
||||
* } $filters
|
||||
* @return Filters
|
||||
*/
|
||||
private function getFilters(array $filters): Filters {
|
||||
$groups = [];
|
||||
foreach ($filters['groups'] as $group) {
|
||||
$groups[] = new FilterGroup(
|
||||
$this->uniqueId(),
|
||||
$group['operator'],
|
||||
array_map(
|
||||
function (array $filter): Filter {
|
||||
$field = $this->registry->getField($filter['field']);
|
||||
if (!$field) {
|
||||
throw new InvalidStateException(sprintf("Field with key '%s' not found", $filter['field']));
|
||||
}
|
||||
return new Filter(
|
||||
$this->uniqueId(),
|
||||
$field->getType(),
|
||||
$filter['field'],
|
||||
$filter['condition'],
|
||||
array_merge(['value' => $filter['value']], isset($filter['params']) ? ['params' => $filter['params']] : []),
|
||||
);
|
||||
},
|
||||
$group['filters']
|
||||
)
|
||||
);
|
||||
}
|
||||
return new Filters($filters['operator'], $groups);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Utils;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
|
||||
class Json {
|
||||
public static function encode(array $value): string {
|
||||
$json = json_encode((object)$value, JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION);
|
||||
$error = json_last_error();
|
||||
if ($error || $json === false) {
|
||||
throw new InvalidStateException(json_last_error_msg(), (string)$error);
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
|
||||
public static function decode(string $json): array {
|
||||
$value = json_decode($json, true);
|
||||
$error = json_last_error();
|
||||
if ($error) {
|
||||
throw new InvalidStateException(json_last_error_msg(), (string)$error);
|
||||
}
|
||||
if (!is_array($value)) {
|
||||
throw Exceptions::jsonNotObject($json);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationGraph;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
|
||||
class AutomationNode {
|
||||
/** @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;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationGraph;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
|
||||
interface AutomationNodeVisitor {
|
||||
public function initialize(Automation $automation): void;
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void;
|
||||
|
||||
public function complete(Automation $automation): void;
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationGraph;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use Generator;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException;
|
||||
|
||||
class AutomationWalker {
|
||||
/** @param AutomationNodeVisitor[] $visitors */
|
||||
public function walk(Automation $automation, array $visitors = []): void {
|
||||
$steps = $automation->getSteps();
|
||||
$root = $steps['root'] ?? null;
|
||||
if (!$root) {
|
||||
throw Exceptions::automationStructureNotValid(__("Automation must contain a 'root' step", 'mailpoet'), 'no-root');
|
||||
}
|
||||
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->initialize($automation);
|
||||
}
|
||||
|
||||
foreach ($this->walkStepsDepthFirstPreOrder($steps, $root) as $record) {
|
||||
[$step, $parents] = $record;
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->visitNode($automation, new AutomationNode($step, array_values($parents)));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->complete($automation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string|int, Step> $steps
|
||||
* @return Generator<array{0: Step, 1: array<string|int, Step>}>
|
||||
*/
|
||||
private function walkStepsDepthFirstPreOrder(array $steps, Step $root): Generator {
|
||||
/** @var array{0: Step, 1: array<string|int, 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();
|
||||
if (!$nextStepId) {
|
||||
continue; // empty edge
|
||||
}
|
||||
$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::automationStructureNotValid(
|
||||
// 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
|
||||
),
|
||||
'step-not-found'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class AtLeastOneTriggerRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'at-least-one-trigger';
|
||||
|
||||
/** @var bool */
|
||||
private $triggerFound = false;
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
$this->triggerFound = false;
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
if ($node->getStep()->getType() === Step::TYPE_TRIGGER) {
|
||||
$this->triggerFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
if (!$automation->needsFullValidation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->triggerFound) {
|
||||
return;
|
||||
}
|
||||
throw Exceptions::automationStructureNotValid(__('There must be at least one trigger in the automation.', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class ConsistentStepMapRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'consistent-step-map';
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
foreach ($automation->getSteps() as $id => $step) {
|
||||
if ((string)$id !== $step->getId()) {
|
||||
// translators: %1$s is the ID of the step, %2$s is its index in the steps object.
|
||||
throw Exceptions::automationStructureNotValid(
|
||||
sprintf(__("Step with ID '%1\$s' stored under a mismatched index '%2\$s'.", 'mailpoet'), $step->getId(), $id),
|
||||
self::RULE_ID
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class NoCycleRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'no-cycle';
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
$parents = $node->getParents();
|
||||
$parentIdsMap = array_combine(
|
||||
array_map(function (Step $parent) {
|
||||
return $parent->getId();
|
||||
}, $node->getParents()),
|
||||
$parents
|
||||
) ?: [];
|
||||
|
||||
foreach ($step->getNextStepIds() as $nextStepId) {
|
||||
if ($nextStepId === $step->getId() || isset($parentIdsMap[$nextStepId])) {
|
||||
throw Exceptions::automationStructureNotValid(__('Cycle found in automation graph', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class NoDuplicateEdgesRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'no-duplicate-edges';
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$visitedNextStepIdsMap = [];
|
||||
foreach ($node->getStep()->getNextStepIds() as $nextStepId) {
|
||||
if (isset($visitedNextStepIdsMap[$nextStepId])) {
|
||||
throw Exceptions::automationStructureNotValid(__('Duplicate next step definition found', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
$visitedNextStepIdsMap[$nextStepId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class NoJoinRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'no-join';
|
||||
|
||||
/** @var array<string|int, Step[]> */
|
||||
private $directParentMap = [];
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
$this->directParentMap = [];
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
foreach ($step->getNextStepIds() as $nextStepId) {
|
||||
$this->directParentMap[$nextStepId] = array_merge($this->directParentMap[$nextStepId] ?? [], [$step]);
|
||||
}
|
||||
|
||||
if (count($this->directParentMap[$step->getId()] ?? []) > 1) {
|
||||
throw Exceptions::automationStructureNotValid(__('Path join found in automation graph', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class NoUnreachableStepsRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'no-unreachable-steps';
|
||||
|
||||
/** @var AutomationNode[] */
|
||||
private $visitedNodes = [];
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
$this->visitedNodes = [];
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$this->visitedNodes[$node->getStep()->getId()] = $node;
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
if (count($this->visitedNodes) !== count($automation->getSteps())) {
|
||||
throw Exceptions::automationStructureNotValid(__('Unreachable steps found in automation graph', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class TriggerNeedsToBeFollowedByActionRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'trigger-needs-to-be-followed-by-action';
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
if (!$automation->needsFullValidation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$step = $node->getStep();
|
||||
if ($step->getType() !== Step::TYPE_TRIGGER) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nextStepIds = $step->getNextStepIds();
|
||||
if (!count($nextStepIds)) {
|
||||
throw Exceptions::automationStructureNotValid(__('A trigger needs to be followed by an action.', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
|
||||
foreach ($nextStepIds as $nextStepsId) {
|
||||
$step = $automation->getStep($nextStepsId);
|
||||
if ($step && $step->getType() === Step::TYPE_ACTION) {
|
||||
continue;
|
||||
}
|
||||
throw Exceptions::automationStructureNotValid(__('A trigger needs to be followed by an action.', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class TriggersUnderRootRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'triggers-under-root';
|
||||
|
||||
/** @var array<string, Step> $triggersMap */
|
||||
private $triggersMap = [];
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
$this->triggersMap = [];
|
||||
foreach ($automation->getSteps() as $step) {
|
||||
if ($step->getType() === 'trigger') {
|
||||
$this->triggersMap[$step->getId()] = $step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
if ($step->getType() === Step::TYPE_ROOT) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($step->getNextStepIds() as $nextStepId) {
|
||||
if (isset($this->triggersMap[$nextStepId])) {
|
||||
throw Exceptions::automationStructureNotValid(__('Trigger must be a direct descendant of automation root', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class UnknownStepRule implements AutomationNodeVisitor {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
/** @var Automation|null|false */
|
||||
private $cachedExistingAutomation = false;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry,
|
||||
AutomationStorage $automationStorage
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
$this->automationStorage = $automationStorage;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
$this->cachedExistingAutomation = false;
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
$registryStep = $this->registry->getStep($step->getKey());
|
||||
|
||||
// step not registered (e.g. plugin was deactivated) - allow saving it only if it hasn't changed
|
||||
if (!$registryStep) {
|
||||
$currentAutomation = $this->getCurrentAutomation($automation);
|
||||
$currentStep = $currentAutomation ? ($currentAutomation->getSteps()[$step->getId()] ?? null) : null;
|
||||
if (!$currentStep || $step->toArray() !== $currentStep->toArray()) {
|
||||
throw Exceptions::automationStepModifiedWhenUnknown($step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
|
||||
private function getCurrentAutomation(Automation $automation): ?Automation {
|
||||
try {
|
||||
$id = $automation->getId();
|
||||
if ($this->cachedExistingAutomation === false) {
|
||||
$this->cachedExistingAutomation = $this->automationStorage->getAutomation($id);
|
||||
}
|
||||
} catch (InvalidStateException $e) {
|
||||
// for new automations, no automation ID is set
|
||||
$this->cachedExistingAutomation = null;
|
||||
}
|
||||
return $this->cachedExistingAutomation;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
use MailPoet\Validator\ValidationException;
|
||||
use MailPoet\Validator\Validator;
|
||||
|
||||
class ValidStepArgsRule implements AutomationNodeVisitor {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
/** @var Validator */
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry,
|
||||
Validator $validator
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
$registryStep = $this->registry->getStep($step->getKey());
|
||||
if (!$registryStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = $registryStep->getArgsSchema();
|
||||
$properties = $schema->toArray()['properties'] ?? null;
|
||||
if (!$properties) {
|
||||
$this->validator->validate($schema, $step->getArgs());
|
||||
return;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
foreach ($properties as $property => $propertySchema) {
|
||||
$schemaToValidate = array_merge(
|
||||
$schema->toArray(),
|
||||
['properties' => [$property => $propertySchema]]
|
||||
);
|
||||
try {
|
||||
$this->validator->validateSchemaArray(
|
||||
$schemaToValidate,
|
||||
$step->getArgs(),
|
||||
$property
|
||||
);
|
||||
} catch (ValidationException $e) {
|
||||
$errors[$property] = $e->getWpError()->get_error_code();
|
||||
}
|
||||
}
|
||||
if ($errors) {
|
||||
$throwable = ValidationException::create();
|
||||
foreach ($errors as $errorKey => $errorMsg) {
|
||||
$throwable->withError((string)$errorKey, (string)$errorMsg);
|
||||
}
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Control\SubjectTransformerHandler;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Filter;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
use MailPoet\Validator\ValidationException;
|
||||
use MailPoet\Validator\Validator;
|
||||
|
||||
class ValidStepFiltersRule implements AutomationNodeVisitor {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
/** @var SubjectTransformerHandler */
|
||||
private $subjectTransformerHandler;
|
||||
|
||||
/** @var Validator */
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry,
|
||||
SubjectTransformerHandler $subjectTransformerHandler,
|
||||
Validator $validator
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
$this->subjectTransformerHandler = $subjectTransformerHandler;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$filters = $node->getStep()->getFilters();
|
||||
$groups = $filters ? $filters->getGroups() : [];
|
||||
$errors = [];
|
||||
foreach ($groups as $group) {
|
||||
foreach ($group->getFilters() as $filter) {
|
||||
$registryFilter = $this->registry->getFilter($filter->getFieldType());
|
||||
if (!$registryFilter) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$this->validator->validate($registryFilter->getArgsSchema($filter->getCondition()), $filter->getArgs());
|
||||
} catch (ValidationException $e) {
|
||||
$errors[$filter->getId()] = $e->getWpError()->get_error_code();
|
||||
continue;
|
||||
}
|
||||
|
||||
// ensure that the field is available with the provided subjects
|
||||
$subjectKeys = $this->subjectTransformerHandler->getSubjectKeysForAutomation($automation);
|
||||
$filterSubject = $this->getFilterSubject($filter);
|
||||
if (!$filterSubject) {
|
||||
$errors[$filter->getId()] = __('Field not found', 'mailpoet');
|
||||
} elseif (!in_array($filterSubject->getKey(), $subjectKeys, true)) {
|
||||
// translators: %s is the name of a subject (data structure) that provides the field
|
||||
$errors[$filter->getId()] = sprintf(__('A trigger that provides %s is required', 'mailpoet'), $filterSubject->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
$throwable = ValidationException::create()->withMessage('invalid-automation-filters');
|
||||
foreach ($errors as $errorKey => $errorMsg) {
|
||||
$throwable->withError((string)$errorKey, (string)$errorMsg);
|
||||
}
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
|
||||
/** @return Subject<Payload> */
|
||||
private function getFilterSubject(Filter $filter): ?Subject {
|
||||
foreach ($this->registry->getSubjects() as $subject) {
|
||||
foreach ($subject->getFields() as $field) {
|
||||
if ($field->getKey() === $filter->getFieldKey()) {
|
||||
return $subject;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Control\SubjectTransformerHandler;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class ValidStepOrderRule implements AutomationNodeVisitor {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
/** @var SubjectTransformerHandler */
|
||||
private $subjectTransformerHandler;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry,
|
||||
SubjectTransformerHandler $subjectTransformerHandler
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
$this->subjectTransformerHandler = $subjectTransformerHandler;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
$registryStep = $this->registry->getStep($step->getKey());
|
||||
if (!$registryStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
// triggers don't require any subjects (they provide them)
|
||||
if ($step->getType() === Step::TYPE_TRIGGER) {
|
||||
return;
|
||||
}
|
||||
|
||||
$requiredSubjectKeys = $registryStep->getSubjectKeys();
|
||||
if (!$requiredSubjectKeys) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subjectKeys = $this->subjectTransformerHandler->getSubjectKeysForAutomation($automation);
|
||||
$missingSubjectKeys = array_diff($requiredSubjectKeys, $subjectKeys);
|
||||
if (count($missingSubjectKeys) > 0) {
|
||||
throw Exceptions::missingRequiredSubjects($step, $missingSubjectKeys);
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
use MailPoet\Validator\ValidationException;
|
||||
use Throwable;
|
||||
|
||||
class ValidStepRule implements AutomationNodeVisitor {
|
||||
/** @var AutomationNodeVisitor[] */
|
||||
private $rules;
|
||||
|
||||
/** @var array<string, array{step_id: string, fields: array<string,string>}> */
|
||||
private $errors = [];
|
||||
|
||||
/** @param AutomationNodeVisitor[] $rules */
|
||||
public function __construct(
|
||||
array $rules
|
||||
) {
|
||||
$this->rules = $rules;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
if (!$automation->needsFullValidation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->rules as $rule) {
|
||||
$rule->initialize($automation);
|
||||
}
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
if (!$automation->needsFullValidation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->rules as $rule) {
|
||||
$stepId = $node->getStep()->getId();
|
||||
try {
|
||||
$rule->visitNode($automation, $node);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
if (!isset($this->errors[$stepId])) {
|
||||
$this->errors[$stepId] = ['step_id' => $stepId, 'message' => $e->getMessage(), 'fields' => [], 'filters' => []];
|
||||
}
|
||||
$this->errors[$stepId]['fields'] = array_merge(
|
||||
$this->mapErrorCodesToErrorMessages($e->getErrors()),
|
||||
$this->errors[$stepId]['fields']
|
||||
);
|
||||
} catch (ValidationException $e) {
|
||||
if (!isset($this->errors[$stepId])) {
|
||||
$this->errors[$stepId] = ['step_id' => $stepId, 'message' => $e->getMessage(), 'fields' => [], 'filters' => []];
|
||||
}
|
||||
|
||||
$key = $rule instanceof ValidStepFiltersRule ? 'filters' : 'fields';
|
||||
/** @phpstan-ignore-next-line - PHPStan detects inconsistency in merged array */
|
||||
$this->errors[$stepId][$key] = array_merge(
|
||||
$this->mapErrorCodesToErrorMessages($e->getErrors()),
|
||||
$this->errors[$stepId][$key]
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
if (!isset($this->errors[$stepId])) {
|
||||
$this->errors[$stepId] = ['step_id' => $stepId, 'message' => __('Unknown error.', 'mailpoet'), 'fields' => [], 'filters' => []];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function mapErrorCodesToErrorMessages(array $errorCodes): array {
|
||||
|
||||
return array_map(
|
||||
function(string $errorCode): string {
|
||||
switch ($errorCode) {
|
||||
case "rest_property_required":
|
||||
return __('This is a required field.', 'mailpoet');
|
||||
case "rest_additional_properties_forbidden":
|
||||
case "rest_too_few_properties":
|
||||
case "rest_too_many_properties":
|
||||
return "";
|
||||
case "rest_invalid_type":
|
||||
case "rest_invalid_multiple":
|
||||
case "rest_not_in_enum":
|
||||
return __('This field is not well formed.', 'mailpoet');
|
||||
case "rest_too_few_items":
|
||||
return __('Please add more items.', 'mailpoet');
|
||||
case "rest_too_many_items":
|
||||
return __('Please remove some items.', 'mailpoet');
|
||||
case "rest_duplicate_items":
|
||||
return __('Please remove duplicate items.', 'mailpoet');
|
||||
case "rest_out_of_bounds":
|
||||
return __('This value is out of bounds.', 'mailpoet');
|
||||
case "rest_too_short":
|
||||
return __('This value is not long enough.', 'mailpoet');
|
||||
case "rest_too_long":
|
||||
return __('This value is too long.', 'mailpoet');
|
||||
case "rest_invalid_pattern":
|
||||
return __('This value is not well formed.', 'mailpoet');
|
||||
case "rest_no_matching_schema":
|
||||
return __('This value does not match the expected format.', 'mailpoet');
|
||||
case "rest_one_of_multiple_matches":
|
||||
return __('This value is not matching the correct times.', 'mailpoet');
|
||||
case "rest_invalid_hex_color":
|
||||
return __('This value is not a hex formatted color.', 'mailpoet');
|
||||
case "rest_invalid_date":
|
||||
return __('This value is not a date.', 'mailpoet');
|
||||
case "rest_invalid_email":
|
||||
return __('This value is not an email address.', 'mailpoet');
|
||||
case "rest_invalid_ip":
|
||||
return __('This value is not an IP address.', 'mailpoet');
|
||||
case "rest_invalid_uuid":
|
||||
return __('This value is not an UUID.', 'mailpoet');
|
||||
default:
|
||||
return $errorCode;
|
||||
}
|
||||
},
|
||||
$errorCodes
|
||||
);
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
if (!$automation->needsFullValidation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->rules as $rule) {
|
||||
$rule->complete($automation);
|
||||
}
|
||||
|
||||
if ($this->errors) {
|
||||
throw Exceptions::automationNotValid(__('Some steps are not valid', 'mailpoet'), $this->errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Data\StepValidationArgs;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class ValidStepValidationRule implements AutomationNodeVisitor {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
$registryStep = $this->registry->getStep($step->getKey());
|
||||
if (!$registryStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subjects = $this->collectSubjects($automation, $node->getParents());
|
||||
$args = new StepValidationArgs($automation, $step, $subjects);
|
||||
$registryStep->validate($args);
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Step[] $parents
|
||||
* @return Subject<Payload>[]
|
||||
*/
|
||||
private function collectSubjects(Automation $automation, array $parents): array {
|
||||
$triggers = array_filter($parents, function (Step $step) {
|
||||
return $step->getType() === Step::TYPE_TRIGGER;
|
||||
});
|
||||
|
||||
$subjectKeys = [];
|
||||
foreach ($triggers as $trigger) {
|
||||
$registryTrigger = $this->registry->getTrigger($trigger->getKey());
|
||||
if (!$registryTrigger) {
|
||||
throw Exceptions::automationTriggerNotFound($automation->getId(), $trigger->getKey());
|
||||
}
|
||||
$subjectKeys = array_merge($subjectKeys, $registryTrigger->getSubjectKeys());
|
||||
}
|
||||
|
||||
$subjects = [];
|
||||
foreach (array_unique($subjectKeys) as $key) {
|
||||
$subject = $this->registry->getSubject($key);
|
||||
if (!$subject) {
|
||||
throw Exceptions::subjectNotFound($key);
|
||||
}
|
||||
$subjects[] = $subject;
|
||||
}
|
||||
return $subjects;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user