Spaces:
No application file
No application file
namespace Mautic\CampaignBundle\Executioner\Scheduler\Mode; | |
use Doctrine\Common\Collections\ArrayCollection; | |
use Mautic\CampaignBundle\Entity\Event; | |
use Mautic\CampaignBundle\Entity\LeadEventLog; | |
use Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException; | |
use Mautic\CampaignBundle\Executioner\Scheduler\Mode\DAO\GroupExecutionDateDAO; | |
use Mautic\CoreBundle\Helper\CoreParametersHelper; | |
use Mautic\CoreBundle\Helper\DateTimeHelper; | |
use Mautic\LeadBundle\Entity\Lead; | |
use Psr\Log\LoggerInterface; | |
class Interval implements ScheduleModeInterface | |
{ | |
public const LOG_DATE_FORMAT = 'Y-m-d H:i:s T'; | |
private ?\DateTimeZone $defaultTimezone = null; | |
public function __construct( | |
private LoggerInterface $logger, | |
private CoreParametersHelper $coreParametersHelper | |
) { | |
} | |
/** | |
* @throws NotSchedulableException | |
*/ | |
public function getExecutionDateTime(Event $event, \DateTimeInterface $compareFromDateTime, \DateTimeInterface $comparedToDateTime): \DateTimeInterface | |
{ | |
$interval = $event->getTriggerInterval(); | |
$unit = $event->getTriggerIntervalUnit(); | |
try { | |
$this->logger->debug( | |
'CAMPAIGN: ('.$event->getId().') Adding interval of '.$interval.$unit.' to '.$comparedToDateTime->format(self::LOG_DATE_FORMAT) | |
); | |
/** @var \DateTime $comparedToDateTime */ | |
$comparedToDateTime->add((new DateTimeHelper())->buildInterval($interval, $unit)); | |
} catch (\Exception $exception) { | |
$this->logger->error('CAMPAIGN: Determining interval scheduled failed with "'.$exception->getMessage().'"'); | |
throw new NotSchedulableException($exception->getMessage()); | |
} | |
if ($comparedToDateTime > $compareFromDateTime) { | |
$this->logger->debug( | |
'CAMPAIGN: ('.$event->getId().') '.$comparedToDateTime->format(self::LOG_DATE_FORMAT).' is later than ' | |
.$compareFromDateTime->format(self::LOG_DATE_FORMAT).' and thus returning '.$comparedToDateTime->format(self::LOG_DATE_FORMAT) | |
); | |
// the event is to be scheduled based on the time interval | |
return $comparedToDateTime; | |
} | |
$this->logger->debug( | |
'CAMPAIGN: ('.$event->getId().') '.$comparedToDateTime->format(self::LOG_DATE_FORMAT).' is earlier than ' | |
.$compareFromDateTime->format(self::LOG_DATE_FORMAT).' and thus returning '.$compareFromDateTime->format(self::LOG_DATE_FORMAT) | |
); | |
return $compareFromDateTime; | |
} | |
/** | |
* @return \DateTimeInterface | |
* | |
* @throws NotSchedulableException | |
*/ | |
public function validateExecutionDateTime(LeadEventLog $log, \DateTimeInterface $compareFromDateTime) | |
{ | |
$event = $log->getEvent(); | |
$dateTriggered = clone $log->getDateTriggered(); | |
if (!$this->isContactSpecificExecutionDateRequired($event)) { | |
return $this->getExecutionDateTime($event, $compareFromDateTime, $dateTriggered); | |
} | |
$interval = $event->getTriggerInterval(); | |
$unit = $event->getTriggerIntervalUnit(); | |
if ($interval && $unit) { | |
/** @var \DateTime $dateTriggered */ | |
$dateTriggered->add((new DateTimeHelper())->buildInterval($interval, $unit)); | |
} | |
if ($dateTriggered < $compareFromDateTime) { | |
$this->logger->debug( | |
sprintf('CAMPAIGN: (%s) %s is earlier than %s and thus setting %s', $event->getId(), $dateTriggered->format(self::LOG_DATE_FORMAT), $compareFromDateTime->format(self::LOG_DATE_FORMAT), $compareFromDateTime->format(self::LOG_DATE_FORMAT)) | |
); | |
$dateTriggered = clone $compareFromDateTime; | |
} | |
$hour = $event->getTriggerHour(); | |
$startTime = $event->getTriggerRestrictedStartHour(); | |
$endTime = $event->getTriggerRestrictedStopHour(); | |
$dow = $event->getTriggerRestrictedDaysOfWeek(); | |
return $this->getGroupExecutionDateTime($event->getId(), $log->getLead(), $dateTriggered, $hour, $startTime, $endTime, $dow); | |
} | |
/** | |
* @return GroupExecutionDateDAO[] | |
*/ | |
public function groupContactsByDate(Event $event, ArrayCollection $contacts, \DateTimeInterface $executionDate, \DateTimeInterface $compareFromDateTime = null): array | |
{ | |
$groupedExecutionDates = []; | |
$hour = $event->getTriggerHour(); | |
$startTime = $event->getTriggerRestrictedStartHour(); | |
$endTime = $event->getTriggerRestrictedStopHour(); | |
$daysOfWeek = $event->getTriggerRestrictedDaysOfWeek(); | |
/** @var Lead $contact */ | |
foreach ($contacts as $contact) { | |
$groupExecutionDate = $this->getGroupExecutionDateTime( | |
$event->getId(), | |
$contact, | |
$executionDate, | |
$hour, | |
$startTime, | |
$endTime, | |
$daysOfWeek | |
); | |
if (!isset($groupedExecutionDates[$groupExecutionDate->getTimestamp()])) { | |
$groupedExecutionDates[$groupExecutionDate->getTimestamp()] = new GroupExecutionDateDAO($groupExecutionDate); | |
} | |
$groupedExecutionDates[$groupExecutionDate->getTimestamp()]->addContact($contact); | |
} | |
return $groupedExecutionDates; | |
} | |
/** | |
* Checks if an event has a relative time configured. | |
*/ | |
public function isContactSpecificExecutionDateRequired(Event $event): bool | |
{ | |
if (!$this->isTriggerModeInterval($event) || $this->isRestrictedToDailyScheduling($event) || $this->hasTimeRelatedRestrictions($event)) { | |
return false; | |
} | |
return true; | |
} | |
private function isTriggerModeInterval(Event $event): bool | |
{ | |
return Event::TRIGGER_MODE_INTERVAL === $event->getTriggerMode(); | |
} | |
private function isRestrictedToDailyScheduling(Event $event): bool | |
{ | |
return !in_array($event->getTriggerIntervalUnit(), ['i', 'h', 'd', 'm', 'y']) | |
&& empty($event->getTriggerRestrictedDaysOfWeek()); | |
} | |
private function hasTimeRelatedRestrictions(Event $event): bool | |
{ | |
return null === $event->getTriggerHour() | |
&& (null === $event->getTriggerRestrictedStartHour() || null === $event->getTriggerRestrictedStopHour()) | |
&& empty($event->getTriggerRestrictedDaysOfWeek()); | |
} | |
/** | |
* @return \DateTimeInterface | |
*/ | |
private function getGroupExecutionDateTime( | |
$eventId, | |
Lead $contact, | |
\DateTimeInterface $compareFromDateTime, | |
\DateTimeInterface $hour = null, | |
\DateTimeInterface $startTime = null, | |
\DateTimeInterface $endTime = null, | |
array $daysOfWeek = [] | |
) { | |
$this->logger->debug( | |
sprintf('CAMPAIGN: Comparing calculated executed time for event ID %s and contact ID %s with %s', $eventId, $contact->getId(), $compareFromDateTime->format('Y-m-d H:i:s e')) | |
); | |
if ($hour) { | |
$this->logger->debug( | |
sprintf('CAMPAIGN: Scheduling event ID %s for contact ID %s based on hour of %s', $eventId, $contact->getId(), $hour->format('H:i e')) | |
); | |
$groupDateTime = $this->getExecutionDateTimeFromHour($contact, $hour, $eventId, $compareFromDateTime); | |
} elseif ($startTime && $endTime) { | |
$this->logger->debug( | |
sprintf( | |
'CAMPAIGN: Scheduling event ID %s for contact ID %s based on hour range of %s to %s', | |
$eventId, | |
$contact->getId(), | |
$startTime->format('H:i e'), | |
$endTime->format('H:i e') | |
) | |
); | |
$groupDateTime = $this->getExecutionDateTimeBetweenHours($contact, $startTime, $endTime, $eventId, $compareFromDateTime); | |
} else { | |
$this->logger->debug( | |
sprintf('CAMPAIGN: Scheduling event ID %s for contact ID %s without hour restrictions.', $eventId, $contact->getId()) | |
); | |
$groupDateTime = clone $compareFromDateTime; | |
} | |
if ($daysOfWeek) { | |
$this->logger->debug( | |
sprintf( | |
'CAMPAIGN: Scheduling event ID %s for contact ID %s based on DOW restrictions of %s', | |
$eventId, | |
$contact->getId(), | |
implode(',', $daysOfWeek) | |
) | |
); | |
// Schedule for the next day of the week if applicable | |
while (!in_array((int) $groupDateTime->format('w'), $daysOfWeek)) { | |
/** @var \DateTime $groupDateTime */ | |
$groupDateTime->modify('+1 day'); | |
} | |
} | |
return $groupDateTime; | |
} | |
/** | |
* @return \DateTimeInterface | |
*/ | |
private function getExecutionDateTimeFromHour(Lead $contact, \DateTimeInterface $hour, $eventId, \DateTimeInterface $compareFromDateTime) | |
{ | |
/** @var \DateTime $groupHour */ | |
$groupHour = clone $hour; | |
/** @var \DateTime $groupExecutionDate */ | |
$groupExecutionDate = $this->getGroupExecutionDateWithTimeZone($contact, $eventId, $compareFromDateTime); | |
$groupExecutionDate->setTime((int) $groupExecutionDate->format('H'), (int) $groupExecutionDate->format('i')); | |
$testGroupHour = clone $groupExecutionDate; | |
$testGroupHour->setTime($groupHour->format('H'), $groupHour->format('i')); | |
if ($groupExecutionDate <= $testGroupHour) { | |
return $testGroupHour; | |
} else { | |
$groupExecutionDate->modify('+1 day')->setTime($groupHour->format('H'), $groupHour->format('i')); | |
} | |
return $groupExecutionDate; | |
} | |
/** | |
* @return \DateTimeInterface | |
*/ | |
private function getExecutionDateTimeBetweenHours( | |
Lead $contact, | |
\DateTimeInterface $startTime, | |
\DateTimeInterface $endTime, | |
$eventId, | |
\DateTimeInterface $compareFromDateTime | |
) { | |
/* @var \DateTime $startTime */ | |
$startTime = clone $startTime; | |
/* @var \DateTime $endTime */ | |
$endTime = clone $endTime; | |
if ($endTime < $startTime) { | |
// End time is after start time so switch them | |
$tempStartTime = clone $startTime; | |
$startTime = clone $endTime; | |
$endTime = clone $tempStartTime; | |
unset($tempStartTime); | |
} | |
/** @var \DateTime $groupExecutionDate */ | |
$groupExecutionDate = $this->getGroupExecutionDateWithTimeZone($contact, $eventId, $compareFromDateTime); | |
// Is the time between the start and end hours? | |
$testStartDateTime = clone $groupExecutionDate; | |
$testStartDateTime->setTime($startTime->format('H'), $startTime->format('i')); | |
$testStopDateTime = clone $groupExecutionDate; | |
$testStopDateTime->setTime($endTime->format('H'), $endTime->format('i')); | |
if ($groupExecutionDate < $testStartDateTime) { | |
// Too early so set it to the start date | |
return $testStartDateTime; | |
} | |
if ($groupExecutionDate >= $testStopDateTime) { | |
// Too late so try again tomorrow | |
$groupExecutionDate->modify('+1 day')->setTime((int) $startTime->format('H'), (int) $startTime->format('i')); | |
} | |
return $groupExecutionDate; | |
} | |
/** | |
* @return \DateTimeZone | |
*/ | |
private function getDefaultTimezone() | |
{ | |
if ($this->defaultTimezone) { | |
return $this->defaultTimezone; | |
} | |
$this->defaultTimezone = new \DateTimeZone( | |
$this->coreParametersHelper->get('default_timezone', 'UTC') | |
); | |
return $this->defaultTimezone; | |
} | |
private function getGroupExecutionDateWithTimeZone(Lead $contact, int $eventId, \DateTimeInterface $compareFromDateTime): \DateTimeInterface | |
{ | |
/** @var \DateTime $groupExecutionDate */ | |
$groupExecutionDate = clone $compareFromDateTime; | |
$contactTimezone = $this->getDefaultTimezone(); | |
// Set execution to UTC | |
if ($timezone = $contact->getTimezone()) { | |
try { | |
// Set the group's timezone to the contact's | |
$contactTimezone = new \DateTimeZone($timezone); | |
$this->logger->debug( | |
'CAMPAIGN: ('.$eventId.') Setting '.$timezone.' for contact '.$contact->getId() | |
); | |
} catch (\Exception) { | |
// Timezone is not recognized so use the default | |
$this->logger->debug( | |
'CAMPAIGN: ('.$eventId.') '.$timezone.' for contact '.$contact->getId().' is not recognized' | |
); | |
} | |
} | |
$groupExecutionDate->setTimezone($contactTimezone); | |
return $groupExecutionDate; | |
} | |
} | |