diff --git a/DependencyInjection/Compiler/OverrideServiceCompilerPass.php b/DependencyInjection/Compiler/OverrideServiceCompilerPass.php index f854ac7..e763295 100644 --- a/DependencyInjection/Compiler/OverrideServiceCompilerPass.php +++ b/DependencyInjection/Compiler/OverrideServiceCompilerPass.php @@ -1,7 +1,7 @@ getDefinition('mautic.campaign.model.event'); - $definition->setClass('MauticPlugin\ThirdSetMauticTimingBundle\Model\EventModel'); + + $schedulerDefinition = $container->getDefinition('mautic.campaign.scheduler'); + if($schedulerDefinition != null) { + // Mautic >= v2.14.0 + $schedulerDefinition + ->setClass('MauticPlugin\ThirdSetMauticTimingBundle\Executioner\Scheduler\EventScheduler') + ->addArgument(new Reference('plugin.thirdset.timing.timing_helper')); + + $eventExecutionerDefinition = $container->getDefinition('mautic.campaign.event_executioner'); + $eventExecutionerDefinition->setClass('MauticPlugin\ThirdSetMauticTimingBundle\Executioner\EventExecutioner'); + + $kickoffExecutionerDefinition = $container->getDefinition('mautic.campaign.executioner.kickoff'); + $kickoffExecutionerDefinition->setClass('MauticPlugin\ThirdSetMauticTimingBundle\Executioner\KickoffExecutioner'); + + $realtimeExecutionerDefinition = $container->getDefinition('mautic.campaign.executioner.realtime'); + $realtimeExecutionerDefinition->setClass('MauticPlugin\ThirdSetMauticTimingBundle\Executioner\RealTimeExecutioner'); + + $scheduledExecutionerDefinition = $container->getDefinition('mautic.campaign.executioner.scheduled'); + $scheduledExecutionerDefinition->setClass('MauticPlugin\ThirdSetMauticTimingBundle\Executioner\ScheduledExecutioner'); + } else { + // Mautic < v2.14.0 + $definition = $container->getDefinition('mautic.campaign.model.event'); + $definition->setClass('MauticPlugin\ThirdSetMauticTimingBundle\Model\EventModel'); + } } } diff --git a/Executioner/EventExecutioner.php b/Executioner/EventExecutioner.php new file mode 100644 index 0000000..f373bc6 --- /dev/null +++ b/Executioner/EventExecutioner.php @@ -0,0 +1,149 @@ +scheduler = $scheduler; + } + + /** + * The parent class executes events for a group of contacts. Here we rewrite + * the method to loop contacts individually so that we can make scheduling + * decisions on a contact by contact basis. + * @param ArrayCollection $events + * @param ArrayCollection $contacts + * @param Counter|null $childrenCounter + * @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) + { + // Quit if ther are no contacts. + if (!$contacts->count()) { + return; + } + + // Loop contacts and process them one at a time. + foreach ($contacts as $contact) { + + // Set the current contact on our custom scheduler. + $this->scheduler->setCurrentContact($contact); + + // Call the parent method with just our current contact. + parent::executeEventsForContacts( + $events, + new ArrayCollection(array($contact)), + $childrenCounter, + $isInactive + ); + } + } + +} diff --git a/Executioner/KickoffExecutioner.php b/Executioner/KickoffExecutioner.php new file mode 100644 index 0000000..e8e2241 --- /dev/null +++ b/Executioner/KickoffExecutioner.php @@ -0,0 +1,283 @@ +logger = $logger; + $this->kickoffContactFinder = $kickoffContactFinder; + $this->translator = $translator; + $this->executioner = $executioner; + $this->scheduler = $scheduler; + } + + /** + * @param Campaign $campaign + * @param ContactLimiter $limiter + * @param OutputInterface|null $output + * + * @return Counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws NotSchedulableException + */ + public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInterface $output = null) + { + $this->campaign = $campaign; + $this->limiter = $limiter; + $this->output = ($output) ? $output : new NullOutput(); + $this->counter = new Counter(); + + try { + $this->prepareForExecution(); + $this->executeOrScheduleEvent(); + } catch (NoContactsFoundException $exception) { + $this->logger->debug('CAMPAIGN: No more contacts to process'); + } catch (NoEventsFoundException $exception) { + $this->logger->debug('CAMPAIGN: No events to process'); + } finally { + if ($this->progressBar) { + $this->progressBar->finish(); + $this->output->writeln("\n"); + } + } + + return $this->counter; + } + + /** + * @throws NoEventsFoundException + */ + private function prepareForExecution() + { + $this->logger->debug('CAMPAIGN: Triggering kickoff events'); + + $this->progressBar = null; + $this->batchCounter = 0; + + $this->rootEvents = $this->campaign->getRootEvents(); + $totalRootEvents = $this->rootEvents->count(); + $this->logger->debug('CAMPAIGN: Processing the following events: '.implode(', ', $this->rootEvents->getKeys())); + + $totalContacts = $this->kickoffContactFinder->getContactCount($this->campaign->getId(), $this->rootEvents->getKeys(), $this->limiter); + $totalKickoffEvents = $totalRootEvents * $totalContacts; + + $this->output->writeln( + $this->translator->trans( + 'mautic.campaign.trigger.event_count', + [ + '%events%' => $totalKickoffEvents, + '%batch%' => $this->limiter->getBatchLimit(), + ] + ) + ); + + if (!$totalKickoffEvents) { + throw new NoEventsFoundException(); + } + + $this->progressBar = ProgressBarHelper::init($this->output, $totalKickoffEvents); + $this->progressBar->start(); + } + + /** + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws NoContactsFoundException + * @throws NotSchedulableException + */ + private function executeOrScheduleEvent() + { + // Use the same timestamp across all contacts processed + $now = new \DateTime(); + $this->counter->advanceEventCount($this->rootEvents->count()); + + // Loop over contacts until the entire campaign is executed + $contacts = $this->kickoffContactFinder->getContacts($this->campaign->getId(), $this->limiter); + while ($contacts->count()) { + $batchMinContactId = max($contacts->getKeys()) + 1; + $rootEvents = clone $this->rootEvents; + + /** @var Event $event */ + foreach ($rootEvents as $key => $event) { + $this->progressBar->advance($contacts->count()); + $this->counter->advanceEvaluated($contacts->count()); + + // Loop contacts and process them one at a time. + foreach ($contacts as $contact) { + $this->scheduler->setCurrentContact($contact); + + // Check if the event should be scheduled (let the schedulers do the debug logging) + $executionDate = $this->scheduler->getExecutionDateTime($event, $now); + $this->logger->debug( + 'CAMPAIGN: Event ID# '.$event->getId(). + ' to be executed on '.$executionDate->format('Y-m-d H:i:s'). + ' compared to '.$now->format('Y-m-d H:i:s') + ); + + if ($this->scheduler->shouldSchedule($executionDate, $now)) { + $this->counter->advanceTotalScheduled(1); + $this->scheduler->schedule($event, $executionDate, new ArrayCollection(array($contact))); + + $rootEvents->remove($key); + + continue; + } + } + } + + if ($rootEvents->count()) { + // Execute the events for the batch of contacts + $this->executioner->executeEventsForContacts($rootEvents, $contacts, $this->counter); + } + + $this->kickoffContactFinder->clear(); + + if ($this->limiter->getContactId()) { + // No use making another call + break; + } + + $this->logger->debug('CAMPAIGN: Fetching the next batch of kickoff contacts starting with contact ID '.$batchMinContactId); + $this->limiter->setBatchMinContactId($batchMinContactId); + + // Get the next batch + $contacts = $this->kickoffContactFinder->getContacts($this->campaign->getId(), $this->limiter); + } + } +} diff --git a/Executioner/RealTimeExecutioner.php b/Executioner/RealTimeExecutioner.php new file mode 100644 index 0000000..948a26d --- /dev/null +++ b/Executioner/RealTimeExecutioner.php @@ -0,0 +1,328 @@ +logger = $logger; + $this->leadModel = $leadModel; + $this->eventRepository = $eventRepository; + $this->executioner = $executioner; + $this->decisionExecutioner = $decisionExecutioner; + $this->collector = $collector; + $this->scheduler = $scheduler; + $this->contactTracker = $contactTracker; + } + + /** + * @param string $type + * @param mixed $passthrough + * @param string|null $channel + * @param int|null $channelId + * + * @return Responses + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + */ + public function execute($type, $passthrough = null, $channel = null, $channelId = null) + { + $this->responses = new Responses(); + $now = new \DateTime(); + + $this->logger->debug('CAMPAIGN: Campaign triggered for event type '.$type.'('.$channel.' / '.$channelId.')'); + + // Kept for BC support although not sure we need this + defined('MAUTIC_CAMPAIGN_NOT_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_NOT_SYSTEM_TRIGGERED', 1); + + try { + $this->fetchCurrentContact(); + } catch (CampaignNotExecutableException $exception) { + $this->logger->debug('CAMPAIGN: '.$exception->getMessage()); + + return $this->responses; + } + + try { + $this->fetchCampaignData($type); + } catch (CampaignNotExecutableException $exception) { + $this->logger->debug('CAMPAIGN: '.$exception->getMessage()); + + return $this->responses; + } + + /** @var Event $event */ + foreach ($this->events as $event) { + try { + $this->evaluateDecisionForContact($event, $passthrough, $channel, $channelId); + } catch (DecisionNotApplicableException $exception) { + $this->logger->debug('CAMPAIGN: Event ID '.$event->getId().' is not applicable ('.$exception->getMessage().')'); + + continue; + } + + $children = $event->getPositiveChildren(); + if (!$children->count()) { + $this->logger->debug('CAMPAIGN: Event ID '.$event->getId().' has no positive children'); + + continue; + } + + $this->executeAssociatedEvents($children, $now); + } + + // Save any changes to the contact done by the listeners + if ($this->contact->getChanges()) { + $this->leadModel->saveEntity($this->contact, false); + } + + return $this->responses; + } + + /** + * @param ArrayCollection $children + * @param \DateTime $now + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + */ + private function executeAssociatedEvents(ArrayCollection $children, \DateTime $now) + { + $children = clone $children; + + /** @var Event $child */ + foreach ($children as $key => $child) { + ////////////// ThirdSetMauticTimingBundle \\\\\\\\\\\\\\ + $this->scheduler->setCurrentContact($this->contact); + //////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + $executionDate = $this->scheduler->getExecutionDateTime($child, $now); + $this->logger->debug( + 'CAMPAIGN: Event ID# '.$child->getId(). + ' to be executed on '.$executionDate->format('Y-m-d H:i:s') + ); + + if ($this->scheduler->shouldSchedule($executionDate, $now)) { + $this->scheduler->scheduleForContact($child, $executionDate, $this->contact); + + $children->remove($key); + } + } + + if ($children->count()) { + $this->executioner->executeEventsForContact($children, $this->contact, $this->responses); + } + } + + /** + * @param Event $event + * @param mixed $passthrough + * @param string|null $channel + * @param int|null $channelId + * + * @throws DecisionNotApplicableException + * @throws Exception\CannotProcessEventException + */ + private function evaluateDecisionForContact(Event $event, $passthrough = null, $channel = null, $channelId = null) + { + $this->logger->debug('CAMPAIGN: Executing '.$event->getType().' ID '.$event->getId().' for contact ID '.$this->contact->getId()); + + if ($event->getEventType() !== Event::TYPE_DECISION) { + @trigger_error( + "{$event->getType()} is not assigned to a decision and no longer supported. ". + 'Check that you are executing RealTimeExecutioner::execute for an event registered as a decision.', + E_USER_DEPRECATED + ); + + throw new DecisionNotApplicableException("Event {$event->getId()} is not a decision."); + } + + // If channels do not match up, there's no need to go further + if ($channel && $event->getChannel() && $channel !== $event->getChannel()) { + throw new DecisionNotApplicableException("Channels, $channel and {$event->getChannel()}, do not match."); + } + + if ($channel && $channelId && $event->getChannelId() && $channelId !== $event->getChannelId()) { + throw new DecisionNotApplicableException("Channel IDs, $channelId and {$event->getChannelId()}, do not match for $channel."); + } + + /** @var DecisionAccessor $config */ + $config = $this->collector->getEventConfig($event); + $this->decisionExecutioner->evaluateForContact($config, $event, $this->contact, $passthrough, $channel, $channelId); + } + + /** + * @throws CampaignNotExecutableException + */ + private function fetchCurrentContact() + { + $this->contact = $this->contactTracker->getContact(); + if (!$this->contact instanceof Lead || !$this->contact->getId()) { + throw new CampaignNotExecutableException('Unidentifiable contact'); + } + + $this->logger->debug('CAMPAIGN: Current contact ID# '.$this->contact->getId()); + } + + /** + * @param $type + * + * @throws CampaignNotExecutableException + */ + private function fetchCampaignData($type) + { + if (!$this->events = $this->eventRepository->getContactPendingEvents($this->contact->getId(), $type)) { + throw new CampaignNotExecutableException('Contact does not have any applicable '.$type.' associations.'); + } + + // 2.14 BC break workaround - pre 2.14 had a bug that recorded channelId for decisions as 1 regardless of actually ID + // if channelIdField was an array and only one item was selected. That caused the channel ID check in evaluateDecisionForContact + // to fail resulting in the decision never being evaluated. Therefore we are going to self heal these decisions. + /** @var Event $event */ + foreach ($this->events as $event) { + if (1 === $event->getChannelId()) { + ChannelExtractor::setChannel($event, $event, $this->collector->getEventConfig($event)); + + $this->eventRepository->saveEntity($event); + } + } + + $this->logger->debug('CAMPAIGN: Found '.count($this->events).' events to analyze for contact ID '.$this->contact->getId()); + } +} diff --git a/Executioner/ScheduledExecutioner.php b/Executioner/ScheduledExecutioner.php new file mode 100644 index 0000000..b1448d5 --- /dev/null +++ b/Executioner/ScheduledExecutioner.php @@ -0,0 +1,436 @@ +repo = $repository; + $this->logger = $logger; + $this->translator = $translator; + $this->executioner = $executioner; + $this->scheduler = $scheduler; + $this->scheduledContactFinder = $scheduledContactFinder; + } + + /** + * @param Campaign $campaign + * @param ContactLimiter $limiter + * @param OutputInterface|null $output + * + * @return Counter|mixed + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + * @throws \Doctrine\ORM\Query\QueryException + */ + public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInterface $output = null) + { + $this->campaign = $campaign; + $this->limiter = $limiter; + $this->output = ($output) ? $output : new NullOutput(); + $this->counter = new Counter(); + + $this->logger->debug('CAMPAIGN: Triggering scheduled events'); + + try { + $this->prepareForExecution(); + $this->executeOrRecheduleEvent(); + } catch (NoEventsFoundException $exception) { + $this->logger->debug('CAMPAIGN: No events to process'); + } finally { + if ($this->progressBar) { + $this->progressBar->finish(); + $this->output->writeln("\n"); + } + } + + return $this->counter; + } + + /** + * @param array $logIds + * @param OutputInterface|null $output + * + * @return Counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + * @throws \Doctrine\ORM\Query\QueryException + */ + public function executeByIds(array $logIds, OutputInterface $output = null) + { + $this->output = ($output) ? $output : new NullOutput(); + $this->counter = new Counter(); + + if (!$logIds) { + return $this->counter; + } + + $logs = $this->repo->getScheduledByIds($logIds); + $totalLogsFound = $logs->count(); + $this->counter->advanceEvaluated($totalLogsFound); + + $this->logger->debug('CAMPAIGN: '.$logs->count().' events scheduled to execute.'); + $this->output->writeln( + $this->translator->trans( + 'mautic.campaign.trigger.event_count', + [ + '%events%' => $totalLogsFound, + '%batch%' => 'n/a', + ] + ) + ); + + if (!$logs->count()) { + return $this->counter; + } + + $this->progressBar = ProgressBarHelper::init($this->output, $totalLogsFound); + $this->progressBar->start(); + + $scheduledLogCount = $totalLogsFound - $logs->count(); + $this->progressBar->advance($scheduledLogCount); + + // Organize the logs by event ID + $organized = $this->organizeByEvent($logs); + $now = new \DateTime(); + foreach ($organized as $organizedLogs) { + $event = $organizedLogs->first()->getEvent(); + + // Validate that the schedule is still appropriate + $this->validateSchedule($event, $organizedLogs, $now, true); + + try { + // Hydrate contacts with custom field data + $this->scheduledContactFinder->hydrateContacts($organizedLogs); + + $this->executioner->executeLogs($event, $organizedLogs, $this->counter); + } catch (NoContactsFoundException $e) { + // All of the events were rescheduled + } + + $this->progressBar->advance($organizedLogs->count()); + } + + $this->progressBar->finish(); + + return $this->counter; + } + + /** + * @throws NoEventsFoundException + */ + private function prepareForExecution() + { + $this->progressBar = null; + $this->now = new \Datetime(); + + // Get counts by event + $scheduledEvents = $this->repo->getScheduledCounts($this->campaign->getId(), $this->now, $this->limiter); + $totalScheduledCount = array_sum($scheduledEvents); + $this->scheduledEvents = array_keys($scheduledEvents); + $this->logger->debug('CAMPAIGN: '.$totalScheduledCount.' events scheduled to execute.'); + + $this->output->writeln( + $this->translator->trans( + 'mautic.campaign.trigger.event_count', + [ + '%events%' => $totalScheduledCount, + '%batch%' => $this->limiter->getBatchLimit(), + ] + ) + ); + + if (!$totalScheduledCount) { + throw new NoEventsFoundException(); + } + + $this->progressBar = ProgressBarHelper::init($this->output, $totalScheduledCount); + $this->progressBar->start(); + } + + /** + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + * @throws \Doctrine\ORM\Query\QueryException + */ + private function executeOrRecheduleEvent() + { + // Use the same timestamp across all contacts processed + $now = new \DateTime(); + + foreach ($this->scheduledEvents as $eventId) { + $this->counter->advanceEventCount(); + + // Loop over contacts until the entire campaign is executed + $this->executeScheduled($eventId, $now); + } + } + + /** + * @param $eventId + * @param \DateTime $now + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + * @throws \Doctrine\ORM\Query\QueryException + */ + private function executeScheduled($eventId, \DateTime $now) + { + $logs = $this->repo->getScheduled($eventId, $this->now, $this->limiter); + while ($logs->count()) { + try { + $this->scheduledContactFinder->hydrateContacts($logs); + } catch (NoContactsFoundException $e) { + break; + } + + $event = $logs->first()->getEvent(); + $this->progressBar->advance($logs->count()); + $this->counter->advanceEvaluated($logs->count()); + + // Validate that the schedule is still appropriate + $this->validateSchedule($event, $logs, $now); + + // Execute if there are any that did not get rescheduled + $this->executioner->executeLogs($event, $logs, $this->counter); + + // Get next batch + $this->scheduledContactFinder->clear(); + $logs = $this->repo->getScheduled($eventId, $this->now, $this->limiter); + } + } + + /** + * @param Event $event + * @param ArrayCollection $logs + * @param \DateTime $now + * @param bool $scheduleTogether + * + * @throws Scheduler\Exception\NotSchedulableException + */ + private function validateSchedule(Event $event, ArrayCollection $logs, \DateTime $now, $scheduleTogether = false) + { + $toBeRescheduled = new ArrayCollection(); + $latestExecutionDate = $now; + + // Check if the event should be scheduled (let the schedulers do the debug logging) + /** @var LeadEventLog $log */ + foreach ($logs as $key => $log) { + ////////////// ThirdSetMauticTimingBundle \\\\\\\\\\\\\\ + $this->scheduler->setCurrentContact($log->getLead()); + //////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + $executionDate = $this->scheduler->getExecutionDateTime($event, $now, $log->getDateTriggered()); + $this->logger->debug( + 'CAMPAIGN: Log ID #'.$log->getID(). + ' to be executed on '.$executionDate->format('Y-m-d H:i:s'). + ' compared to '.$now->format('Y-m-d H:i:s') + ); + + if ($this->scheduler->shouldSchedule($executionDate, $now)) { + // The schedule has changed for this event since first scheduled + $this->counter->advanceTotalScheduled(); + if ($scheduleTogether) { + $toBeRescheduled->set($key, $log); + + if ($executionDate > $latestExecutionDate) { + $latestExecutionDate = $executionDate; + } + } else { + $this->scheduler->reschedule($log, $executionDate); + } + + $logs->remove($key); + + continue; + } + } + + if ($toBeRescheduled->count()) { + $this->scheduler->rescheduleLogs($toBeRescheduled, $latestExecutionDate); + } + } + + /** + * @param ArrayCollection $logs + * + * @return ArrayCollection[] + */ + private function organizeByEvent(ArrayCollection $logs) + { + $jumpTo = []; + $other = []; + + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + $event = $log->getEvent(); + $eventType = $event->getType(); + + if (CampaignActionJumpToEventSubscriber::EVENT_NAME === $eventType) { + if (!isset($jumpTo[$event->getId()])) { + $jumpTo[$event->getId()] = new ArrayCollection(); + } + + $jumpTo[$event->getId()]->set($log->getId(), $log); + } else { + if (!isset($other[$event->getId()])) { + $other[$event->getId()] = new ArrayCollection(); + } + + $other[$event->getId()]->set($log->getId(), $log); + } + } + + return array_merge($other, $jumpTo); + } +} diff --git a/Executioner/Scheduler/EventScheduler.php b/Executioner/Scheduler/EventScheduler.php new file mode 100644 index 0000000..a220bb1 --- /dev/null +++ b/Executioner/Scheduler/EventScheduler.php @@ -0,0 +1,166 @@ +timingHelper = $timingHelper; + } + + /** + * Overrides the standard getExecutionDateTime method to use our custom + * contact specific timing logic. + * + * @param Event $event The Campaign Event that is being processed. + * @param \DateTime|null $compareFromDateTime The date to compare from (this + * would typically be 'now'). + * @param \DateTime|null $comparedToDateTime The date/time to compare to. + * + * @return \DateTime + * + * @throws NotSchedulableException + */ + public function getExecutionDateTime( + Event $event, + \DateTime $compareFromDateTime = null, + \DateTime $comparedToDateTime = null + ) + { + // Get the executionDateTime from the parent method. + $executionDateTime = parent::getExecutionDateTime( + $event, + $compareFromDateTime, + $comparedToDateTime + ); + + // Now apply our extended timing rules. Note that contact may be null + // for certain executioners and that's okay. The only one that we are + // concerned with is the EventExecutioner and that one is being + // overridden to set the contact. + if($this->currentContact != null) { + $executionDateTime = $this->timingHelper->getExecutionDateTime( + $event, + $this->currentContact, + $executionDateTime + ); + } + + return $executionDateTime; + } + + /** + * Set the current contact (the contact that is currently being scheduled). + * + * @param \Mautic\LeadBundle\Entity\Lead $currentContact + * + * @return EventScheduler + */ + public function setCurrentContact(Lead $currentContact) + { + $this->currentContact = $currentContact; + + return $this; + } + + /** + * Get current contact (the contact that is currently being scheduled). + * + * @return \Mautic\LeadBundle\Entity\Lead + */ + public function getCurrentContact() + { + return $this->currentContact; + } + +} diff --git a/Helper/TimingHelper.php b/Helper/TimingHelper.php index a236a75..178c152 100644 --- a/Helper/TimingHelper.php +++ b/Helper/TimingHelper.php @@ -1,7 +1,8 @@ timingModel = $timingModel; } + /** + * Gets the executionDateTime with our extended timing rules applied. + * + * This is used by Mautic 2.14 and up. + * + * @param \Mautic\CampaignBundle\Entity\Event $event The Campaign Event to + * use for the evaluation. + * @param \Mautic\LeadBundle\Entity\Lead $contact The Contact to use for the + * evaluation. + * @param \DateTime $executionDateTime The executionDateTime that was + * determined by Mautic's standard timing rules. + * @param string $initNowStr A string for calulating 'now'. This can be used + * to find the nextRunDate based on some future now date. + * @return \DateTime Returns the executionDateTime. + */ + public function getExecutionDateTime( + Event $event, + Lead $contact, + \DateTime $executionDateTime, + $initNowStr = 'now' + ) + { + /* @var $timing \MauticPlugin\ThirdSetMauticTimingBundle\Entity\Timing */ + $timing = $this->timingModel->getById($event->getId()); + + // Get the nextRunDate (next time that the trigger can be run according + // to our extended timing rules). + $nextRunDate = $this->getNextRunDate( + $timing, + $contact, + $executionDateTime->format('Y-m-d H:i:s') + ); + + return $nextRunDate; + } + /** * Our custom checkEventTiming function. * @param array $eventData An array of event data. diff --git a/Tests/Helper/TimingHelperTest.php b/Tests/Helper/TimingHelperTest.php index 62b8710..e3232cc 100644 --- a/Tests/Helper/TimingHelperTest.php +++ b/Tests/Helper/TimingHelperTest.php @@ -1,7 +1,8 @@ getMockTiming( + $expression, + $useContactTimezone, + null + ); + + /* @var $event \Mautic\CampaignBundle\Entity\Event */ + $event = $this->getMockBuilder('\Mautic\CampaignBundle\Entity\Event') + ->disableOriginalConstructor() + ->getMock(); + + /* @var $timingHelper \MauticPlugin\ThirdSetMauticTimingBundle\Helper\TimingHelper */ + $timingHelper = $this->getTimingHelper($timing); + + /* @var $lead \Mautic\LeadBundle\Entity\Lead */ + $lead = $this->getMockLead($contactTimezone); + + // Call the function. + $executionDateTime = $timingHelper->getExecutionDateTime( + $event, + $lead, + $mauticExecutionDateTime, + $mockNow + ); + + //Assert that the expected DateTime is returned + $this->assertEquals($expected, $executionDateTime); + } /** * @testdox checkEventTiming correctly returns true for a due simple expression. @@ -599,7 +642,7 @@ private function getMockTiming( /** * Helper function that returns a mock lead for use by our tests. - * @param string $timezone A timezone as a string (ex: "America/New_York"_ + * @param string $timezone A timezone as a string (ex: "America/New_York") * @return Lead Returns a mock Lead. */ private function getMockLead($timezone = null)