Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Model;
|
||||
|
||||
use Mautic\LeadBundle\Entity\DoNotContact as DNC;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\DoNotContact;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ChannelActionModel
|
||||
{
|
||||
public function __construct(
|
||||
private LeadModel $contactModel,
|
||||
private DoNotContact $doNotContact,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update channels and frequency rules.
|
||||
*/
|
||||
public function update(array $contactIds, array $subscribedChannels): void
|
||||
{
|
||||
$contacts = $this->contactModel->getLeadsByIds($contactIds);
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
if (!$this->contactModel->canEditContact($contact)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->addChannels($contact, $subscribedChannels);
|
||||
$this->removeChannels($contact, $subscribedChannels);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add contact's channels.
|
||||
* Only resubscribe if the contact did not opt out themselves.
|
||||
*/
|
||||
private function addChannels(Lead $contact, array $subscribedChannels): void
|
||||
{
|
||||
$contactChannels = $this->contactModel->getContactChannels($contact);
|
||||
|
||||
foreach ($subscribedChannels as $subscribedChannel) {
|
||||
if (!array_key_exists($subscribedChannel, $contactChannels)) {
|
||||
$contactable = $this->doNotContact->isContactable($contact, $subscribedChannel);
|
||||
if (DNC::UNSUBSCRIBED !== $contactable) {
|
||||
$this->doNotContact->removeDncForContact($contact->getId(), $subscribedChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove contact's channels.
|
||||
*/
|
||||
private function removeChannels(Lead $contact, array $subscribedChannels): void
|
||||
{
|
||||
$allChannels = $this->contactModel->getPreferenceChannels();
|
||||
$dncChannels = array_diff($allChannels, $subscribedChannels);
|
||||
|
||||
foreach ($dncChannels as $channel) {
|
||||
$this->doNotContact->addDncForContact(
|
||||
$contact->getId(),
|
||||
$channel,
|
||||
DNC::MANUAL,
|
||||
$this->translator->trans('mautic.lead.event.donotcontact_manual')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Model;
|
||||
|
||||
use Mautic\LeadBundle\Entity\FrequencyRule;
|
||||
use Mautic\LeadBundle\Entity\FrequencyRuleRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
|
||||
class FrequencyActionModel
|
||||
{
|
||||
public function __construct(
|
||||
private LeadModel $contactModel,
|
||||
private FrequencyRuleRepository $frequencyRuleRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update channels.
|
||||
*
|
||||
* @param string $preferredChannel
|
||||
*/
|
||||
public function update(array $contactIds, array $params, $preferredChannel): void
|
||||
{
|
||||
$contacts = $this->contactModel->getLeadsByIds($contactIds);
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
if (!$this->contactModel->canEditContact($contact)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->updateFrequencyRules($contact, $params, $preferredChannel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $preferredChannel
|
||||
*/
|
||||
private function updateFrequencyRules(Lead $contact, array $params, $preferredChannel): void
|
||||
{
|
||||
$frequencyRules = $contact->getFrequencyRules()->toArray();
|
||||
$channels = $this->contactModel->getPreferenceChannels();
|
||||
|
||||
foreach ($channels as $channel) {
|
||||
if (is_null($preferredChannel)) {
|
||||
$preferredChannel = $channel;
|
||||
}
|
||||
|
||||
$frequencyRule = $frequencyRules[$channel] ?? new FrequencyRule();
|
||||
$frequencyRule->setChannel($channel);
|
||||
$frequencyRule->setLead($contact);
|
||||
|
||||
if (!$frequencyRule->getDateAdded()) {
|
||||
$frequencyRule->setDateAdded(new \DateTime());
|
||||
}
|
||||
|
||||
if (!empty($params['frequency_number_'.$channel]) && !empty($params['frequency_time_'.$channel])) {
|
||||
$frequencyRule->setFrequencyNumber($params['frequency_number_'.$channel]);
|
||||
$frequencyRule->setFrequencyTime($params['frequency_time_'.$channel]);
|
||||
} else {
|
||||
$frequencyRule->setFrequencyNumber(null);
|
||||
$frequencyRule->setFrequencyTime(null);
|
||||
}
|
||||
|
||||
if (!empty($params['contact_pause_start_date_'.$channel])) {
|
||||
$frequencyRule->setPauseFromDate(new \DateTime($params['contact_pause_start_date_'.$channel]));
|
||||
}
|
||||
|
||||
if (!empty($params['contact_pause_end_date_'.$channel])) {
|
||||
$frequencyRule->setPauseToDate(new \DateTime($params['contact_pause_end_date_'.$channel]));
|
||||
}
|
||||
|
||||
$frequencyRule->setPreferredChannel($preferredChannel === $channel);
|
||||
|
||||
$contact->addFrequencyRule($frequencyRule);
|
||||
|
||||
$this->frequencyRuleRepository->saveEntity($frequencyRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Entity\Message;
|
||||
use Mautic\ChannelBundle\Entity\MessageRepository;
|
||||
use Mautic\ChannelBundle\Event\MessageEvent;
|
||||
use Mautic\ChannelBundle\Form\Type\MessageType;
|
||||
use Mautic\ChannelBundle\Helper\ChannelListHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\AjaxLookupModelInterface;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends FormModel<Message>
|
||||
*
|
||||
* @implements AjaxLookupModelInterface<Message>
|
||||
*/
|
||||
class MessageModel extends FormModel implements AjaxLookupModelInterface, GlobalSearchInterface
|
||||
{
|
||||
public const CHANNEL_FEATURE = 'marketing_messages';
|
||||
|
||||
protected static $channels;
|
||||
|
||||
public function __construct(
|
||||
protected ChannelListHelper $channelListHelper,
|
||||
protected CampaignModel $campaignModel,
|
||||
EntityManager $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Message $entity
|
||||
* @param bool $unlock
|
||||
*/
|
||||
public function saveEntity($entity, $unlock = true): void
|
||||
{
|
||||
$isNew = $entity->isNew();
|
||||
|
||||
parent::saveEntity($entity, $unlock);
|
||||
|
||||
if (!$isNew) {
|
||||
// Update the channels
|
||||
$channels = $entity->getChannels();
|
||||
foreach ($channels as $channel) {
|
||||
$channel->setMessage($entity);
|
||||
}
|
||||
$this->getRepository()->saveEntities($channels);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'channel:messages';
|
||||
}
|
||||
|
||||
public function getRepository(): ?MessageRepository
|
||||
{
|
||||
return $this->em->getRepository(Message::class);
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?Message
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Message();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
* @param array $options
|
||||
*
|
||||
* @return \Symfony\Component\Form\FormInterface<mixed>
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(MessageType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getChannels()
|
||||
{
|
||||
if (!self::$channels) {
|
||||
$channels = $this->channelListHelper->getFeatureChannels(self::CHANNEL_FEATURE);
|
||||
|
||||
// Validate channel configs
|
||||
foreach ($channels as $channel => $config) {
|
||||
if (!isset($config['lookupFormType']) && !isset($config['propertiesFormType'])) {
|
||||
throw new \InvalidArgumentException('lookupFormType and/or propertiesFormType are required for channel '.$channel);
|
||||
}
|
||||
|
||||
$label = match (true) {
|
||||
$this->translator->hasId('mautic.channel.'.$channel) => $this->translator->trans('mautic.channel.'.$channel),
|
||||
$this->translator->hasId('mautic.'.$channel) => $this->translator->trans('mautic.'.$channel),
|
||||
$this->translator->hasId('mautic.'.$channel.'.'.$channel) => $this->translator->trans('mautic.'.$channel.'.'.$channel),
|
||||
default => ucfirst($channel),
|
||||
};
|
||||
$config['label'] = $label;
|
||||
|
||||
$channels[$channel] = $config;
|
||||
}
|
||||
|
||||
self::$channels = $channels;
|
||||
}
|
||||
|
||||
return self::$channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filter
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param array $options
|
||||
*/
|
||||
public function getLookupResults($type, $filter = '', $limit = 10, $start = 0, $options = []): array
|
||||
{
|
||||
$results = [];
|
||||
switch ($type) {
|
||||
case 'channel.message':
|
||||
$entities = $this->getRepository()->getMessageList(
|
||||
$filter,
|
||||
$limit,
|
||||
$start
|
||||
);
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$results[] = [
|
||||
'label' => $entity['name'],
|
||||
'value' => $entity['id'],
|
||||
];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getMessageChannels($messageId): array
|
||||
{
|
||||
return $this->getRepository()->getMessageChannels($messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getChannelMessageByChannelId($channelId)
|
||||
{
|
||||
return $this->getRepository()->getChannelMessageByChannelId($channelId);
|
||||
}
|
||||
|
||||
public function getLeadStatsPost($messageId, $dateFrom = null, $dateTo = null, $channel = null): array
|
||||
{
|
||||
$eventLog = $this->campaignModel->getCampaignLeadEventLogRepository();
|
||||
|
||||
return $eventLog->getChartQuery(
|
||||
[
|
||||
'type' => 'message.send',
|
||||
'dateFrom' => $dateFrom,
|
||||
'dateTo' => $dateTo,
|
||||
'channel' => 'channel.message',
|
||||
'channelId' => $messageId,
|
||||
'logChannel' => $channel,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMarketingMessagesEventLogs($messageId, $dateFrom = null, $dateTo = null)
|
||||
{
|
||||
$eventLog = $this->campaignModel->getCampaignLeadEventLogRepository();
|
||||
|
||||
return $eventLog->getEventLogs(['type' => 'message.send', 'dateFrom' => $dateFrom, 'dateTo' => $dateTo, 'channel' => 'message', 'channelId' => $messageId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channel name from the database.
|
||||
*
|
||||
* @template T of object
|
||||
*
|
||||
* @param int $id
|
||||
* @param class-string<T> $entityName
|
||||
* @param string $nameColumn
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getChannelName($id, $entityName, $nameColumn = 'name')
|
||||
{
|
||||
if (!$id || !$entityName || !$nameColumn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$repo = $this->em->getRepository($entityName);
|
||||
$qb = $repo->createQueryBuilder('e')
|
||||
->select('e.'.$nameColumn)
|
||||
->where('e.id = :id')
|
||||
->setParameter('id', (int) $id);
|
||||
$result = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
return $result[$nameColumn] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Message) {
|
||||
throw new MethodNotAllowedHttpException(['Message']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = ChannelEvents::MESSAGE_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = ChannelEvents::MESSAGE_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = ChannelEvents::MESSAGE_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = ChannelEvents::MESSAGE_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new MessageEvent($entity, $isNew);
|
||||
}
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Mautic\ChannelBundle\Event\MessageQueueBatchProcessEvent;
|
||||
use Mautic\ChannelBundle\Event\MessageQueueEvent;
|
||||
use Mautic\ChannelBundle\Event\MessageQueueProcessEvent;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends FormModel<MessageQueue>
|
||||
*/
|
||||
class MessageQueueModel extends FormModel
|
||||
{
|
||||
/**
|
||||
* @var string A default message reschedule interval
|
||||
*/
|
||||
public const DEFAULT_RESCHEDULE_INTERVAL = 'PT15M';
|
||||
|
||||
public function __construct(
|
||||
protected LeadModel $leadModel,
|
||||
protected CompanyModel $companyModel,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EntityManagerInterface $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\ChannelBundle\Entity\MessageQueueRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(MessageQueue::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $attempts
|
||||
* @param int $priority
|
||||
* @param mixed $messageQueue
|
||||
* @param string $statTableName
|
||||
* @param string $statContactColumn
|
||||
* @param string $statSentColumn
|
||||
*/
|
||||
public function processFrequencyRules(
|
||||
array &$leads,
|
||||
$channel,
|
||||
$channelId,
|
||||
$campaignEventId = null,
|
||||
$attempts = 3,
|
||||
$priority = MessageQueue::PRIORITY_NORMAL,
|
||||
$messageQueue = null,
|
||||
$statTableName = 'email_stats',
|
||||
$statContactColumn = 'lead_id',
|
||||
$statSentColumn = 'date_sent',
|
||||
): array {
|
||||
$leadIds = array_keys($leads);
|
||||
$leadIds = array_combine($leadIds, $leadIds);
|
||||
|
||||
/** @var \Mautic\LeadBundle\Entity\FrequencyRuleRepository $frequencyRulesRepo */
|
||||
$frequencyRulesRepo = $this->em->getRepository(\Mautic\LeadBundle\Entity\FrequencyRule::class);
|
||||
$defaultFrequencyNumber = $this->coreParametersHelper->get($channel.'_frequency_number');
|
||||
$defaultFrequencyTime = $this->coreParametersHelper->get($channel.'_frequency_time');
|
||||
|
||||
$dontSendTo = $frequencyRulesRepo->getAppliedFrequencyRules(
|
||||
$channel,
|
||||
$leadIds,
|
||||
$defaultFrequencyNumber,
|
||||
$defaultFrequencyTime,
|
||||
$statTableName,
|
||||
$statContactColumn,
|
||||
$statSentColumn
|
||||
);
|
||||
|
||||
$queuedContacts = [];
|
||||
foreach ($dontSendTo as $frequencyRuleMet) {
|
||||
// We only deal with date intervals here (no time intervals) so it's safe to use 'P'
|
||||
$scheduleInterval = new \DateInterval('P1'.substr($frequencyRuleMet['frequency_time'], 0, 1));
|
||||
if ($messageQueue && isset($messageQueue[$frequencyRuleMet['lead_id']])) {
|
||||
$this->reschedule($messageQueue[$frequencyRuleMet['lead_id']], $scheduleInterval);
|
||||
} else {
|
||||
// Queue this message to be processed by frequency and priority
|
||||
$this->queue(
|
||||
[$leads[$frequencyRuleMet['lead_id']]],
|
||||
$channel,
|
||||
$channelId,
|
||||
$scheduleInterval,
|
||||
$attempts,
|
||||
$priority,
|
||||
$campaignEventId
|
||||
);
|
||||
}
|
||||
$queuedContacts[$frequencyRuleMet['lead_id']] = $frequencyRuleMet['lead_id'];
|
||||
unset($leads[$frequencyRuleMet['lead_id']]);
|
||||
}
|
||||
|
||||
return $queuedContacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds messages to the queue.
|
||||
*
|
||||
* @param array $leads
|
||||
* @param string $channel
|
||||
* @param int $channelId
|
||||
* @param int $maxAttempts
|
||||
* @param int $priority
|
||||
* @param int|null $campaignEventId
|
||||
* @param array $options
|
||||
*/
|
||||
public function queue(
|
||||
$leads,
|
||||
$channel,
|
||||
$channelId,
|
||||
\DateInterval $scheduledInterval,
|
||||
$maxAttempts = 1,
|
||||
$priority = 1,
|
||||
$campaignEventId = null,
|
||||
$options = [],
|
||||
): bool {
|
||||
$messageQueues = [];
|
||||
|
||||
$scheduledDate = (new \DateTime())->add($scheduledInterval);
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$leadId = (is_array($lead)) ? $lead['id'] : $lead->getId();
|
||||
if (!empty($this->getRepository()->findMessage($channel, $channelId, $leadId))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$messageQueue = new MessageQueue();
|
||||
if ($campaignEventId) {
|
||||
$messageQueue->setEvent($this->em->getReference(\Mautic\CampaignBundle\Entity\Event::class, $campaignEventId));
|
||||
}
|
||||
$messageQueue->setChannel($channel);
|
||||
$messageQueue->setChannelId($channelId);
|
||||
$messageQueue->setDatePublished(new \DateTime());
|
||||
$messageQueue->setMaxAttempts($maxAttempts);
|
||||
$messageQueue->setLead(
|
||||
($lead instanceof Lead) ? $lead : $this->em->getReference(Lead::class, $leadId)
|
||||
);
|
||||
$messageQueue->setPriority($priority);
|
||||
$messageQueue->setScheduledDate($scheduledDate);
|
||||
$messageQueue->setOptions($options);
|
||||
|
||||
$messageQueues[] = $messageQueue;
|
||||
}
|
||||
|
||||
if ($messageQueues) {
|
||||
$this->saveEntities($messageQueues);
|
||||
$messageQueueRepository = $this->getRepository();
|
||||
$messageQueueRepository->detachEntities($messageQueues);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sendMessages($channel = null, $channelId = null, int $limit = 50): int
|
||||
{
|
||||
// Note when the process started for batch purposes
|
||||
$processStarted = new \DateTime();
|
||||
$counter = 0;
|
||||
|
||||
foreach ($this->getRepository()->getQueuedMessages($limit, $processStarted, $channel, $channelId) as $queue) {
|
||||
$counter += $this->processMessageQueue($queue);
|
||||
$event = $queue->getEvent();
|
||||
|
||||
if ($event) {
|
||||
$this->em->detach($event);
|
||||
}
|
||||
$this->em->detach($queue);
|
||||
}
|
||||
|
||||
return $counter;
|
||||
}
|
||||
|
||||
public function processMessageQueue($queue): int
|
||||
{
|
||||
if (!is_array($queue)) {
|
||||
if (!$queue instanceof MessageQueue) {
|
||||
throw new \InvalidArgumentException('$queue must be an instance of '.MessageQueue::class);
|
||||
}
|
||||
|
||||
$queue = [$queue->getId() => $queue];
|
||||
}
|
||||
|
||||
$counter = 0;
|
||||
$contacts = [];
|
||||
$byChannel = [];
|
||||
|
||||
// Lead entities will not have profile fields populated due to the custom field use - therefore to optimize resources,
|
||||
// get a list of leads to fetch details all at once along with company details for dynamic email content, etc
|
||||
/** @var MessageQueue $message */
|
||||
foreach ($queue as $message) {
|
||||
if ($message->getLead()) {
|
||||
$contacts[$message->getId()] = $message->getLead()->getId();
|
||||
}
|
||||
}
|
||||
if (!empty($contacts)) {
|
||||
$contactData = $this->leadModel->getRepository()->getContacts($contacts);
|
||||
foreach ($contacts as $messageId => $contactId) {
|
||||
$queue[$messageId]->getLead()->setFields($contactData[$contactId]);
|
||||
}
|
||||
}
|
||||
// Group queue by channel and channel ID - this make it possible for processing listeners to batch process such as
|
||||
// sending emails in batches to 3rd party transactional services via HTTP APIs
|
||||
foreach ($queue as $key => $message) {
|
||||
if (MessageQueue::STATUS_SENT == $message->getStatus()) {
|
||||
unset($queue[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$messageChannel = $message->getChannel();
|
||||
$messageChannelId = $message->getChannelId();
|
||||
if (!$messageChannelId) {
|
||||
$messageChannelId = 0;
|
||||
}
|
||||
|
||||
if (!isset($byChannel[$messageChannel])) {
|
||||
$byChannel[$messageChannel] = [];
|
||||
}
|
||||
if (!isset($byChannel[$messageChannel][$messageChannelId])) {
|
||||
$byChannel[$messageChannel][$messageChannelId] = [];
|
||||
}
|
||||
|
||||
$byChannel[$messageChannel][$messageChannelId][] = $message;
|
||||
}
|
||||
|
||||
// First try to batch process each channel
|
||||
foreach ($byChannel as $messageChannel => $channelMessages) {
|
||||
foreach ($channelMessages as $messageChannelId => $messages) {
|
||||
$event = new MessageQueueBatchProcessEvent($messages, $messageChannel, $messageChannelId);
|
||||
$ignore = null;
|
||||
$this->dispatchEvent('process_batch_message_queue', $ignore, false, $event);
|
||||
}
|
||||
}
|
||||
unset($byChannel);
|
||||
|
||||
// Now check to see if the message was processed by the listener and if not
|
||||
// send it through a single process event listener
|
||||
foreach ($queue as $message) {
|
||||
if (!$message->isProcessed()) {
|
||||
$event = new MessageQueueProcessEvent($message);
|
||||
$this->dispatchEvent('process_message_queue', $message, false, $event);
|
||||
}
|
||||
|
||||
if ($message->isSuccess()) {
|
||||
++$counter;
|
||||
$message->setSuccess();
|
||||
$message->setLastAttempt(new \DateTime());
|
||||
$message->setDateSent(new \DateTime());
|
||||
$message->setStatus(MessageQueue::STATUS_SENT);
|
||||
} elseif ($message->isFailed()) {
|
||||
// Failure such as email delivery issue or something so retry in a short time
|
||||
$this->reschedule($message, new \DateInterval(self::DEFAULT_RESCHEDULE_INTERVAL));
|
||||
} // otherwise assume the listener did something such as rescheduling the message
|
||||
}
|
||||
|
||||
// add listener
|
||||
$this->saveEntities($queue);
|
||||
|
||||
return $counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $persist
|
||||
*/
|
||||
public function reschedule($message, \DateInterval $rescheduleInterval, $leadId = null, $channel = null, $channelId = null, $persist = false): void
|
||||
{
|
||||
if (!$message instanceof MessageQueue && $leadId && $channel && $channelId) {
|
||||
$message = $this->getRepository()->findMessage($channel, $channelId, $leadId);
|
||||
$persist = true;
|
||||
}
|
||||
|
||||
if (!$message) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message->setAttempts($message->getAttempts() + 1);
|
||||
$message->setLastAttempt(new \DateTime());
|
||||
$rescheduleTo = clone $message->getScheduledDate();
|
||||
|
||||
$rescheduleTo->add($rescheduleInterval);
|
||||
$message->setScheduledDate($rescheduleTo);
|
||||
$message->setStatus(MessageQueue::STATUS_RESCHEDULED);
|
||||
|
||||
if ($persist) {
|
||||
$this->saveEntity($message);
|
||||
}
|
||||
|
||||
// Mark as processed for listeners
|
||||
$message->setProcessed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $channelIds
|
||||
*/
|
||||
public function getQueuedChannelCount($channel, $channelIds = []): int
|
||||
{
|
||||
return $this->getRepository()->getQueuedChannelCount($channel, $channelIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?object $entity
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
switch ($action) {
|
||||
case 'process_message_queue':
|
||||
$name = ChannelEvents::PROCESS_MESSAGE_QUEUE;
|
||||
break;
|
||||
case 'process_batch_message_queue':
|
||||
$name = ChannelEvents::PROCESS_MESSAGE_QUEUE_BATCH;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = ChannelEvents::MESSAGE_QUEUED;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new MessageQueueEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->em);
|
||||
}
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user