| 1 |
<?php |
| 2 |
|
| 3 |
namespace AcyMailing\Helpers; |
| 4 |
|
| 5 |
use AcyMailing\Classes\HistoryClass; |
| 6 |
use AcyMailing\Classes\ScenarioClass; |
| 7 |
use AcyMailing\Classes\ScenarioHistoryLineClass; |
| 8 |
use AcyMailing\Classes\ScenarioProcessClass; |
| 9 |
use AcyMailing\Classes\ScenarioStepClass; |
| 10 |
use AcyMailing\Classes\ScenarioQueueClass; |
| 11 |
use AcyMailing\Classes\UserClass; |
| 12 |
use AcyMailing\Core\AcymObject; |
| 13 |
|
| 14 |
class ScenarioHelper extends AcymObject |
| 15 |
{ |
| 16 |
private ScenarioClass $scenarioClass; |
| 17 |
private ScenarioStepClass $scenarioStepClass; |
| 18 |
private ScenarioQueueClass $scenarioQueueClass; |
| 19 |
private ScenarioHistoryLineClass $scenarioHistoryLineClass; |
| 20 |
private ScenarioProcessClass $scenarioProcessClass; |
| 21 |
|
| 22 |
public function __construct() |
| 23 |
{ |
| 24 |
parent::__construct(); |
| 25 |
$this->scenarioClass = new ScenarioClass(); |
| 26 |
$this->scenarioStepClass = new ScenarioStepClass(); |
| 27 |
$this->scenarioQueueClass = new ScenarioQueueClass(); |
| 28 |
$this->scenarioHistoryLineClass = new ScenarioHistoryLineClass(); |
| 29 |
$this->scenarioProcessClass = new ScenarioProcessClass(); |
| 30 |
} |
| 31 |
|
| 32 |
public function trigger(string $trigger, array $options): void |
| 33 |
{ |
| 34 |
if (!acym_level(ACYM_ENTERPRISE)) { |
| 35 |
return; |
| 36 |
} |
| 37 |
|
| 38 |
if (empty($trigger)) { |
| 39 |
acym_logError('ScenarioHelper::trigger - Missing trigger for options: '.json_encode($options)); |
| 40 |
|
| 41 |
return; |
| 42 |
} |
| 43 |
|
| 44 |
if (empty($options['userId'])) { |
| 45 |
acym_logError('ScenarioHelper::trigger - Missing userId, trigger: '.$trigger); |
| 46 |
|
| 47 |
return; |
| 48 |
} |
| 49 |
|
| 50 |
$userId = intval($options['userId']); |
| 51 |
|
| 52 |
$userClass = new UserClass(); |
| 53 |
if (empty($userClass->getOneById($userId))) { |
| 54 |
acym_logError('ScenarioHelper::trigger - User not found, userId: '.$userId); |
| 55 |
|
| 56 |
return; |
| 57 |
} |
| 58 |
|
| 59 |
$scenarios = $this->scenarioClass->getAllActiveByTriggers([$trigger]); |
| 60 |
|
| 61 |
foreach ($scenarios as $scenario) { |
| 62 |
if ($scenario->trigger_once) { |
| 63 |
if ($this->scenarioProcessClass->hasUserProcess($scenario->id, $userId)) { |
| 64 |
continue; |
| 65 |
} |
| 66 |
} |
| 67 |
|
| 68 |
// For the trigger TimeAutomationTriggers, this way we know if we are triggering a scenario or not |
| 69 |
$scenario->is_scenario = true; |
| 70 |
$scenario->triggers = [$scenario->trigger => 1]; |
| 71 |
$execute = false; |
| 72 |
$options = $this->formatOptions($scenario->trigger_params, 'trigger'); |
| 73 |
$options['userId'] = $userId; |
| 74 |
$options['time'] = time(); |
| 75 |
acym_trigger('onAcymExecuteTrigger', [&$scenario, &$execute, &$options]); |
| 76 |
|
| 77 |
if (!$execute) { |
| 78 |
continue; |
| 79 |
} |
| 80 |
|
| 81 |
$firstStep = $this->scenarioStepClass->getFirstStepByScenarioId($scenario->id); |
| 82 |
|
| 83 |
if (empty($firstStep)) { |
| 84 |
acym_logError('ScenarioHelper::trigger - No first step found for scenario '.$scenario->id); |
| 85 |
continue; |
| 86 |
} |
| 87 |
|
| 88 |
$scenarioProcess = new \stdClass(); |
| 89 |
$scenarioProcess->scenario_id = $scenario->id; |
| 90 |
$scenarioProcess->user_id = $userId; |
| 91 |
$scenarioProcess->start_at = acym_date('now', 'Y-m-d H:i:s', false); |
| 92 |
$scenarioProcess->id = $this->scenarioProcessClass->save($scenarioProcess); |
| 93 |
|
| 94 |
$this->createHistory($scenarioProcess->id, 'trigger', 'success'); |
| 95 |
|
| 96 |
$this->handleStep($firstStep, $userId, $scenarioProcess->id); |
| 97 |
} |
| 98 |
} |
| 99 |
|
| 100 |
private function handleStep(object $step, int $userId, int $scenarioProcessId): void |
| 101 |
{ |
| 102 |
switch ($step->type) { |
| 103 |
case ScenarioClass::TYPE_DELAY: |
| 104 |
$this->handleDelay($step, $userId, $scenarioProcessId); |
| 105 |
break; |
| 106 |
case ScenarioClass::TYPE_CONDITION: |
| 107 |
$this->handleCondition($step, $userId, $scenarioProcessId); |
| 108 |
break; |
| 109 |
case ScenarioClass::TYPE_ACTION: |
| 110 |
$this->handleAction($step, $userId, $scenarioProcessId); |
| 111 |
break; |
| 112 |
} |
| 113 |
} |
| 114 |
|
| 115 |
private function handleDelay(object $step, int $userId, int $scenarioProcessId): void |
| 116 |
{ |
| 117 |
$nextStep = $this->scenarioStepClass->getStepByPreviousStepId($step->id); |
| 118 |
|
| 119 |
if (empty($nextStep)) { |
| 120 |
$this->ending($scenarioProcessId); |
| 121 |
|
| 122 |
return; |
| 123 |
} |
| 124 |
|
| 125 |
$scenarioQueue = new \stdClass(); |
| 126 |
$scenarioQueue->scenario_process_id = $scenarioProcessId; |
| 127 |
$scenarioQueue->step_id = $nextStep->id; |
| 128 |
$timestampNextAction = time() + $this->calculateDelaySecondsToAdd($step->params); |
| 129 |
$scenarioQueue->execution_date = acym_date($timestampNextAction, 'Y-m-d H:i:s', false); |
| 130 |
|
| 131 |
$this->scenarioQueueClass->save($scenarioQueue); |
| 132 |
$this->scenarioQueueClass->deleteByStepIds([$step->id]); |
| 133 |
|
| 134 |
$this->createHistory($scenarioProcessId, ScenarioClass::TYPE_DELAY, 'success', $step->id, $step->params); |
| 135 |
} |
| 136 |
|
| 137 |
private function calculateDelaySecondsToAdd(array $stepDelayParams): int |
| 138 |
{ |
| 139 |
if (empty($stepDelayParams['delay']) || empty($stepDelayParams['unit'])) { |
| 140 |
return 0; |
| 141 |
} |
| 142 |
|
| 143 |
return $stepDelayParams['delay'] * $stepDelayParams['unit']; |
| 144 |
} |
| 145 |
|
| 146 |
private function handleCondition(object $step, int $userId, int $scenarioProcessId): void |
| 147 |
{ |
| 148 |
$conditionNotValidCount = 0; |
| 149 |
|
| 150 |
$query = new AutomationHelper(); |
| 151 |
$query->where = ['user.id = '.$userId]; |
| 152 |
|
| 153 |
$conditionOptions = $this->formatOptions($step->params['option'], $step->type); |
| 154 |
// Unused variable |
| 155 |
$number = 0; |
| 156 |
acym_trigger('onAcymProcessCondition_'.$step->params['condition'], [&$query, &$conditionOptions, &$number, &$conditionNotValidCount]); |
| 157 |
$this->scenarioQueueClass->deleteByStepIds([$step->id]); |
| 158 |
|
| 159 |
$conditionValid = $conditionNotValidCount === 0; |
| 160 |
|
| 161 |
$historyOptions = array_merge($conditionOptions, ['scenario_condition' => $step->params['condition']]); |
| 162 |
$this->createHistory($scenarioProcessId, ScenarioClass::TYPE_CONDITION, 'Condition is '.($conditionValid ? 'valid' : 'not valid'), $step->id, $historyOptions); |
| 163 |
|
| 164 |
$nextStep = $this->scenarioStepClass->getStepByPreviousConditionId($step->id, $conditionValid); |
| 165 |
|
| 166 |
if (empty($nextStep)) { |
| 167 |
$this->ending($scenarioProcessId); |
| 168 |
|
| 169 |
return; |
| 170 |
} |
| 171 |
|
| 172 |
$this->handleStep($nextStep, $userId, $scenarioProcessId); |
| 173 |
} |
| 174 |
|
| 175 |
private function formatOptions(array $options, string $type): array |
| 176 |
{ |
| 177 |
$formattedOptions = []; |
| 178 |
|
| 179 |
switch ($type) { |
| 180 |
case ScenarioClass::TYPE_CONDITION: |
| 181 |
$regex = '/acym_condition\[conditions\]\[__numor__\]\[__numand__\]\[(.+)\]\[(.+)\]/'; |
| 182 |
break; |
| 183 |
case ScenarioClass::TYPE_ACTION: |
| 184 |
$regex = '/acym_action\[actions\]\[__and__\]\[(.+)\]\[(.+)\]/'; |
| 185 |
break; |
| 186 |
case 'trigger': |
| 187 |
$regex = '/\[triggers\]\[user\]\[(.+)\]\[(.+)\]/'; |
| 188 |
break; |
| 189 |
default: |
| 190 |
$regex = ''; |
| 191 |
} |
| 192 |
|
| 193 |
if (empty($regex)) { |
| 194 |
return $formattedOptions; |
| 195 |
} |
| 196 |
|
| 197 |
foreach ($options as $key => $option) { |
| 198 |
$formattedKey = preg_replace($regex, '$2', $key); |
| 199 |
if ($formattedKey === $key) { |
| 200 |
continue; |
| 201 |
} |
| 202 |
$formattedOptions[$formattedKey] = $option; |
| 203 |
} |
| 204 |
|
| 205 |
return $formattedOptions; |
| 206 |
} |
| 207 |
|
| 208 |
private function handleAction(object $step, int $userId, int $scenarioProcessId): void |
| 209 |
{ |
| 210 |
$scenarioClass = new ScenarioClass(); |
| 211 |
$markUnsubscribed = $scenarioClass->checkUserMarkedUnsubscribed($userId, $scenarioProcessId); |
| 212 |
if ($markUnsubscribed && $step->params['action'] === 'acy_send_email') { |
| 213 |
$historyClass = new HistoryClass(); |
| 214 |
$historyClass->insert($userId, 'unsubscribed', [acym_translation('ACYM_SCENARIO_USER_UNSUBSCRIBED')]); |
| 215 |
|
| 216 |
return; |
| 217 |
} |
| 218 |
|
| 219 |
$query = new AutomationHelper(); |
| 220 |
$query->where = ['user.id = '.$userId]; |
| 221 |
|
| 222 |
$actionOptions = $this->formatOptions($step->params['option'], $step->type); |
| 223 |
$log = ''; |
| 224 |
$report = ''; |
| 225 |
try { |
| 226 |
$report = acym_trigger('onAcymProcessAction_'.$step->params['action'], [&$query, &$actionOptions]); |
| 227 |
$report = is_array($report) ? implode(', ', $report) : $report; |
| 228 |
} catch (\Exception $e) { |
| 229 |
$log = $e->getMessage(); |
| 230 |
} |
| 231 |
|
| 232 |
$this->scenarioQueueClass->deleteByStepIds([$step->id]); |
| 233 |
|
| 234 |
$historyOptions = array_merge($actionOptions, ['scenario_action' => $step->params['action']]); |
| 235 |
$this->createHistory($scenarioProcessId, ScenarioClass::TYPE_CONDITION, 'Action completed: '.$report, $step->id, $historyOptions, $log); |
| 236 |
|
| 237 |
$nextStep = $this->scenarioStepClass->getStepByPreviousStepId($step->id); |
| 238 |
|
| 239 |
if (empty($nextStep)) { |
| 240 |
$this->ending($scenarioProcessId); |
| 241 |
|
| 242 |
return; |
| 243 |
} |
| 244 |
|
| 245 |
$this->handleStep($nextStep, $userId, $scenarioProcessId); |
| 246 |
} |
| 247 |
|
| 248 |
private function ending(int $scenarioProcessId): void |
| 249 |
{ |
| 250 |
$this->createHistory($scenarioProcessId, 'ending', acym_translation('ACYM_SCENARIO_ENDED')); |
| 251 |
|
| 252 |
$this->scenarioProcessClass->endProcess($scenarioProcessId); |
| 253 |
} |
| 254 |
|
| 255 |
public function executeAvailableSteps(): void |
| 256 |
{ |
| 257 |
$dateNowUtc = acym_date('now', 'Y-m-d H:i:s', false); |
| 258 |
|
| 259 |
$steps = $this->scenarioStepClass->getAvailableStepsByDate($dateNowUtc); |
| 260 |
|
| 261 |
if (empty($steps)) { |
| 262 |
return; |
| 263 |
} |
| 264 |
|
| 265 |
foreach ($steps as $step) { |
| 266 |
$this->handleStep($step, $step->user_id, $step->scenario_process_id); |
| 267 |
} |
| 268 |
} |
| 269 |
|
| 270 |
public function triggerTimeScenarios(): void |
| 271 |
{ |
| 272 |
$userStatusTriggers = []; |
| 273 |
acym_trigger('onAcymDefineUserStatusCheckTriggers', [&$userStatusTriggers]); |
| 274 |
|
| 275 |
if (empty($userStatusTriggers)) { |
| 276 |
return; |
| 277 |
} |
| 278 |
|
| 279 |
$scenarioClass = new ScenarioClass(); |
| 280 |
$scenarios = $scenarioClass->getAllActiveByTriggers($userStatusTriggers); |
| 281 |
|
| 282 |
if (empty($scenarios)) { |
| 283 |
return; |
| 284 |
} |
| 285 |
|
| 286 |
foreach ($scenarios as $scenario) { |
| 287 |
$nextExecution = $scenario->trigger_params['next_execution'] ?? null; |
| 288 |
$scenario->next_execution = $nextExecution; |
| 289 |
|
| 290 |
if (!empty($nextExecution) && $nextExecution > time()) { |
| 291 |
continue; |
| 292 |
} |
| 293 |
|
| 294 |
$firstStep = $this->scenarioStepClass->getFirstStepByScenarioId($scenario->id); |
| 295 |
if (empty($firstStep)) { |
| 296 |
acym_logError('ScenarioHelper::triggerTimeScenarios - No first step found for scenario '.$scenario->id); |
| 297 |
continue; |
| 298 |
} |
| 299 |
|
| 300 |
$execute = false; |
| 301 |
$options = $this->formatOptions($scenario->trigger_params['option'] ?? [], 'trigger'); |
| 302 |
$options['time'] = time(); |
| 303 |
|
| 304 |
$scenario->is_scenario = true; |
| 305 |
$scenario->triggers = [$scenario->trigger => $options]; |
| 306 |
|
| 307 |
// Get the users matching the trigger |
| 308 |
acym_trigger('onAcymExecuteTrigger', [&$scenario, &$execute, &$options]); |
| 309 |
|
| 310 |
if ($nextExecution !== $scenario->next_execution) { |
| 311 |
$scenario->trigger_params['next_execution'] = $scenario->next_execution; |
| 312 |
$scenarioClass->save($scenario); |
| 313 |
} |
| 314 |
|
| 315 |
if (empty($options['userIds'])) { |
| 316 |
continue; |
| 317 |
} |
| 318 |
|
| 319 |
foreach ($options['userIds'] as $userId) { |
| 320 |
if ($scenario->trigger_once && $this->scenarioProcessClass->hasUserProcess($scenario->id, $userId)) { |
| 321 |
continue; |
| 322 |
} |
| 323 |
|
| 324 |
$scenarioProcess = new \stdClass(); |
| 325 |
$scenarioProcess->scenario_id = $scenario->id; |
| 326 |
$scenarioProcess->user_id = $userId; |
| 327 |
$scenarioProcess->start_at = acym_date('now', 'Y-m-d H:i:s', false); |
| 328 |
$scenarioProcess->id = $this->scenarioProcessClass->save($scenarioProcess); |
| 329 |
|
| 330 |
$this->createHistory($scenarioProcess->id, 'trigger', 'success'); |
| 331 |
|
| 332 |
$this->handleStep($firstStep, $userId, $scenarioProcess->id); |
| 333 |
} |
| 334 |
} |
| 335 |
} |
| 336 |
|
| 337 |
private function createHistory(int $scenarioProcessId, string $type, string $result, string $scenarioStepId = '', array $params = [], string $log = ''): void |
| 338 |
{ |
| 339 |
$scenarioHistoryLine = new \stdClass(); |
| 340 |
$scenarioHistoryLine->scenario_process_id = $scenarioProcessId; |
| 341 |
$scenarioHistoryLine->date = acym_date('now', 'Y-m-d H:i:s', false); |
| 342 |
$scenarioHistoryLine->type = $type; |
| 343 |
//TODO maybe add something more specific |
| 344 |
$scenarioHistoryLine->result = $result; |
| 345 |
|
| 346 |
if (!empty($scenarioStepId)) { |
| 347 |
$scenarioHistoryLine->scenario_step_id = $scenarioStepId; |
| 348 |
} |
| 349 |
|
| 350 |
if (!empty($params)) { |
| 351 |
$scenarioHistoryLine->params = $params; |
| 352 |
} |
| 353 |
|
| 354 |
if (!empty($log)) { |
| 355 |
$scenarioHistoryLine->log = $log; |
| 356 |
} |
| 357 |
|
| 358 |
$this->scenarioHistoryLineClass->save($scenarioHistoryLine); |
| 359 |
} |
| 360 |
|
| 361 |
public function getConditionValue(object $step): string |
| 362 |
{ |
| 363 |
return $step->params['condition']; |
| 364 |
} |
| 365 |
|
| 366 |
public function getActionValue(object $step): string |
| 367 |
{ |
| 368 |
return $step->params['action']; |
| 369 |
} |
| 370 |
} |
| 371 |
|