![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/mautic.corals.io/app/bundles/CampaignBundle/Executioner/ |
<?php namespace Mautic\CampaignBundle\Executioner; use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\FailedLeadEventLog; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Exception\TypeNotFoundException; use Mautic\CampaignBundle\EventCollector\EventCollector; use Mautic\CampaignBundle\EventListener\CampaignActionJumpToEventSubscriber; use Mautic\CampaignBundle\Executioner\Event\ActionExecutioner; use Mautic\CampaignBundle\Executioner\Event\ConditionExecutioner; use Mautic\CampaignBundle\Executioner\Event\DecisionExecutioner; use Mautic\CampaignBundle\Executioner\Logger\EventLogger; use Mautic\CampaignBundle\Executioner\Result\Counter; use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; use Mautic\CampaignBundle\Executioner\Result\Responses; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\CampaignBundle\Helper\RemovedContactTracker; use Mautic\LeadBundle\Entity\Lead; use Psr\Log\LoggerInterface; class EventExecutioner { private ?Responses $responses = null; private \DateTimeInterface $executionDate; public function __construct( private EventCollector $collector, private EventLogger $eventLogger, private ActionExecutioner $actionExecutioner, private ConditionExecutioner $conditionExecutioner, private DecisionExecutioner $decisionExecutioner, private LoggerInterface $logger, private EventScheduler $scheduler, private RemovedContactTracker $removedContactTracker, ) { // Be sure that all events are compared using the exact same \DateTime $this->executionDate = new \DateTime(); } /** * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ public function executeForContact(Event $event, Lead $contact, Responses $responses = null, Counter $counter = null): void { if ($responses) { $this->responses = $responses; } $contacts = new ArrayCollection([$contact->getId() => $contact]); $this->executeForContacts($event, $contacts, $counter); } /** * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ public function executeEventsForContact(ArrayCollection $events, Lead $contact, Responses $responses = null, Counter $counter = null): void { if ($responses) { $this->responses = $responses; } $contacts = new ArrayCollection([$contact->getId() => $contact]); $this->executeEventsForContacts($events, $contacts, $counter); } /** * @param ArrayCollection<int,Lead> $contacts * @param bool $isInactiveEvent * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null, $isInactiveEvent = false): void { if (!$contacts->count()) { $this->logger->debug('CAMPAIGN: No contacts to process for event ID '.$event->getId()); return; } $config = $this->collector->getEventConfig($event); $logs = $this->eventLogger->fetchRotationAndGenerateLogsFromContacts($event, $config, $contacts, $isInactiveEvent); $this->executeLogs($event, $logs, $counter); } /** * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ public function executeLogs(Event $event, ArrayCollection $logs, Counter $counter = null): void { $this->logger->debug('CAMPAIGN: Executing '.$event->getType().' ID '.$event->getId()); if (!$logs->count()) { $this->logger->debug('CAMPAIGN: No logs to process for event ID '.$event->getId()); return; } $config = $this->collector->getEventConfig($event); if ($counter) { // Must pass $counter around rather than setting it as a class property as this class is used // circularly to process children of parent events thus counter must be kept track separately $counter->advanceExecuted($logs->count()); } switch ($event->getEventType()) { case Event::TYPE_ACTION: $evaluatedContacts = $this->actionExecutioner->execute($config, $logs); $this->persistLogs($logs); $this->executeConditionEventsForContacts($event, $evaluatedContacts->getPassed(), $counter); $this->executeActionEventsForContacts($event, $evaluatedContacts->getPassed(), $counter); break; case Event::TYPE_CONDITION: $evaluatedContacts = $this->conditionExecutioner->execute($config, $logs); $this->persistLogs($logs); $this->executeBranchedEventsForContacts($event, $evaluatedContacts, $counter); break; case Event::TYPE_DECISION: $evaluatedContacts = $this->decisionExecutioner->execute($config, $logs); $this->persistLogs($logs); $this->executePositivePathEventsForContacts($event, $evaluatedContacts->getPassed(), $counter); break; default: throw new TypeNotFoundException("{$event->getEventType()} is not a valid event type"); } } /** * @param bool $isInactive * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ public function executeEventsForContacts(ArrayCollection $events, ArrayCollection $contacts, Counter $childrenCounter = null, $isInactive = false): void { if (!$contacts->count()) { return; } // Schedule then return those that need to be immediately executed $executeThese = $this->scheduleEvents($events, $contacts, $childrenCounter, $isInactive); // Execute non jump-to events normally $otherEvents = $executeThese->filter(fn (Event $event): bool => CampaignActionJumpToEventSubscriber::EVENT_NAME !== $event->getType()); if ($otherEvents->count()) { foreach ($otherEvents as $event) { $this->executeForContacts($event, $contacts, $childrenCounter, $isInactive); } } // Now execute jump to events $jumpEvents = $executeThese->filter(fn (Event $event): bool => CampaignActionJumpToEventSubscriber::EVENT_NAME === $event->getType()); if ($jumpEvents->count()) { $jumpLogs = []; // Create logs for the jump to events before the rotation is incremented foreach ($jumpEvents as $key => $event) { $config = $this->collector->getEventConfig($event); $jumpLogs[$key] = $this->eventLogger->fetchRotationAndGenerateLogsFromContacts($event, $config, $contacts, $isInactive); } // Process the jump to events foreach ($jumpLogs as $key => $logs) { $this->executeLogs($jumpEvents->get($key), $logs, $childrenCounter); } } } /** * @param bool $isInactiveEvent */ public function recordLogsAsExecutedForEvent(Event $event, ArrayCollection $contacts, $isInactiveEvent = false): void { $config = $this->collector->getEventConfig($event); $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $isInactiveEvent); // Save updated log entries and clear from memory if (!$logs->isEmpty()) { $this->eventLogger->persistCollection($logs) ->clearCollection($logs); } } /** * @param bool $isInactiveEvent */ public function recordLogsAsFailedForEvent(Event $event, ArrayCollection $contacts, $reason, $isInactiveEvent = false): void { $config = $this->collector->getEventConfig($event); $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $isInactiveEvent); if (!$logs->isEmpty()) { foreach ($logs as $log) { $failedLog = new FailedLeadEventLog(); $failedLog->setLog($log) ->setReason($reason); } // Save updated log entries and clear from memory $this->eventLogger->persistCollection($logs) ->clearCollection($logs); } } /** * @param ArrayCollection|LeadEventLog[] $logs * @param string $error */ public function recordLogsWithError(ArrayCollection $logs, $error): void { foreach ($logs as $log) { $log->appendToMetadata( [ 'failed' => 1, 'reason' => $error, ] ); $log->setIsScheduled(false); } // Save updated log entries and clear from memory $this->eventLogger->persistCollection($logs) ->clearCollection($logs); } /** * @return \DateTimeInterface */ public function getExecutionDate() { return $this->executionDate; } /** * @param bool $isInactive * * @return ArrayCollection * * @throws Scheduler\Exception\NotSchedulableException */ private function scheduleEvents(ArrayCollection $events, ArrayCollection $contacts, Counter $childrenCounter = null, $isInactive = false) { $events = clone $events; foreach ($events as $key => $event) { // Ignore decisions if (Event::TYPE_DECISION == $event->getEventType()) { $this->logger->debug('CAMPAIGN: Ignoring child event ID '.$event->getId().' as a decision'); continue; } $executionDate = $this->scheduler->getExecutionDateTime($event, $this->executionDate); $this->logger->debug( 'CAMPAIGN: Event ID# '.$event->getId(). ' to be executed on '.$executionDate->format('Y-m-d H:i:s e') ); // Check if we need to schedule this if it is not an inactivity check if (!$isInactive && $this->scheduler->shouldScheduleEvent($event, $executionDate, $this->executionDate)) { if ($childrenCounter) { $childrenCounter->advanceTotalScheduled($contacts->count()); } $this->scheduler->schedule($event, $executionDate, $contacts, $isInactive); $events->remove($key); continue; } } return $events; } private function persistLogs(ArrayCollection $logs): void { if ($this->responses) { // Extract responses $this->responses->setFromLogs($logs); } $this->checkForRemovedContacts($logs); // Save updated log entries and clear from memory $this->eventLogger->persistCollection($logs) ->clearCollection($logs); } private function checkForRemovedContacts(ArrayCollection $logs): void { /** * @var int $key * @var LeadEventLog $log */ foreach ($logs as $key => $log) { // Use the deleted ID if the contact was removed by the delete contact action $contact = $log->getLead(); $contactId = (!empty($contact->deletedId)) ? $contact->deletedId : $contact->getId(); $campaignId = $log->getCampaign()->getId(); if ($this->removedContactTracker->wasContactRemoved($campaignId, $contactId)) { $this->logger->debug("CAMPAIGN: Contact ID# $contactId has been removed from campaign ID $campaignId"); $logs->remove($key); // Clear out removed contacts to prevent a memory leak $this->removedContactTracker->clearRemovedContact($campaignId, $contactId); } } } /** * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ private function executeActionEventsForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null): void { $childrenCounter = new Counter(); $actions = $event->getChildrenByEventType(Event::TYPE_ACTION); $childrenCounter->advanceEvaluated($actions->count()); $this->logger->debug('CAMPAIGN: Executing '.$actions->count().' actions under action ID '.$event->getId()); $this->executeEventsForContacts($actions, $contacts, $childrenCounter); if ($counter) { $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); } } /** * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ private function executeConditionEventsForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null): void { $childrenCounter = new Counter(); $conditions = $event->getChildrenByEventType(Event::TYPE_CONDITION); $childrenCounter->advanceEvaluated($conditions->count()); $this->logger->debug('CAMPAIGN: Evaluating '.$conditions->count().' conditions for action ID '.$event->getId()); $this->executeEventsForContacts($conditions, $contacts, $childrenCounter); if ($counter) { $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); } } /** * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ private function executeBranchedEventsForContacts(Event $event, EvaluatedContacts $contacts, Counter $counter = null): void { $childrenCounter = new Counter(); $this->executePositivePathEventsForContacts($event, $contacts->getPassed(), $childrenCounter); $this->executeNegativePathEventsForContacts($event, $contacts->getFailed(), $childrenCounter); if ($counter) { $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); } } /** * @param Counter|null $counter * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ private function executePositivePathEventsForContacts(Event $event, ArrayCollection $contacts, Counter $counter): void { if (!$contacts->count()) { return; } $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $contacts->getKeys()).' passed evaluation for event ID '.$event->getId()); $children = $event->getPositiveChildren(); $counter->advanceEvaluated($children->count()); $this->executeEventsForContacts($children, $contacts, $counter); } /** * @param Counter|null $counter * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ private function executeNegativePathEventsForContacts(Event $event, ArrayCollection $contacts, Counter $counter): void { if (!$contacts->count()) { return; } $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $contacts->getKeys()).' failed evaluation for event ID '.$event->getId()); $children = $event->getNegativeChildren(); $counter->advanceEvaluated($children->count()); $this->executeEventsForContacts($children, $contacts, $counter); } /** * @throws \Doctrine\DBAL\Exception * @throws \Doctrine\ORM\OptimisticLockException */ public function persistSummaries(): void { $this->eventLogger->getSummaryModel()->persistSummaries(); } }