Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,896 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Model;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Event as Events;
|
||||
use Mautic\CampaignBundle\EventCollector\EventCollector;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CampaignBundle\Form\Type\CampaignType;
|
||||
use Mautic\CampaignBundle\Helper\ChannelExtractor;
|
||||
use Mautic\CampaignBundle\Membership\MembershipBuilder;
|
||||
use Mautic\CampaignBundle\Model\Exceptions\CampaignAlreadyUnpublishedException;
|
||||
use Mautic\CampaignBundle\Model\Exceptions\CampaignVersionMismatchedException;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel as CommonFormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\EmailBundle\Entity\StatRepository;
|
||||
use Mautic\FormBundle\Entity\Form;
|
||||
use Mautic\FormBundle\Model\FormModel;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonFormModel<Campaign>
|
||||
*/
|
||||
class CampaignModel extends CommonFormModel implements GlobalSearchInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected ListModel $leadListModel,
|
||||
protected FormModel $formModel,
|
||||
private EventCollector $eventCollector,
|
||||
private MembershipBuilder $membershipBuilder,
|
||||
private ContactTracker $contactTracker,
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\CampaignBundle\Entity\CampaignRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
$repo = $this->em->getRepository(Campaign::class);
|
||||
$repo->setCurrentUser($this->userHelper->getUser());
|
||||
|
||||
return $repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\CampaignBundle\Entity\EventRepository
|
||||
*/
|
||||
public function getEventRepository()
|
||||
{
|
||||
return $this->em->getRepository(Event::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\CampaignBundle\Entity\LeadRepository
|
||||
*/
|
||||
public function getCampaignLeadRepository()
|
||||
{
|
||||
return $this->em->getRepository(CampaignLead::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadEventLogRepository
|
||||
*/
|
||||
public function getCampaignLeadEventLogRepository()
|
||||
{
|
||||
return $this->em->getRepository(\Mautic\CampaignBundle\Entity\LeadEventLog::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'campaign:campaigns';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
* @param string|null $action
|
||||
* @param array $options
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof Campaign) {
|
||||
throw new MethodNotAllowedHttpException(['Campaign']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(CampaignType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific entity or generate a new one if id is empty.
|
||||
*/
|
||||
public function getEntity($id = null): ?Campaign
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Campaign();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an array of campaigns.
|
||||
*
|
||||
* @param int[] $campaignIds
|
||||
*
|
||||
* @return array<int,Campaign>
|
||||
*/
|
||||
public function deleteEntities($campaignIds): array
|
||||
{
|
||||
$entities = [];
|
||||
foreach ($campaignIds as $campaignId) {
|
||||
$campaign = $this->getEntity($campaignId);
|
||||
if ($campaign) {
|
||||
$entities[$campaignId] = $campaign;
|
||||
$this->deleteEntity($campaign);
|
||||
}
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
public function deleteEntity($entity): void
|
||||
{
|
||||
// Null all the event parents for this campaign to avoid database constraints
|
||||
$this->getEventRepository()->nullEventParents($entity->getId());
|
||||
$this->dispatchEvent('pre_delete', $entity);
|
||||
$this->getRepository()->setCampaignAsDeleted($entity->getId());
|
||||
|
||||
$this->dispatcher->dispatch(new Events\DeleteCampaign($entity), CampaignEvents::ON_CAMPAIGN_DELETE);
|
||||
}
|
||||
|
||||
public function deleteCampaign(Campaign $campaign): void
|
||||
{
|
||||
$campaign->deletedId = $campaign->getId();
|
||||
$this->getRepository()->deleteEntity($campaign);
|
||||
$this->dispatchEvent('post_delete', $campaign);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?\Symfony\Contracts\EventDispatcher\Event $event = null): ?\Symfony\Contracts\EventDispatcher\Event
|
||||
{
|
||||
if ($entity instanceof CampaignLead) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$entity instanceof Campaign) {
|
||||
throw new MethodNotAllowedHttpException(['Campaign']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = CampaignEvents::CAMPAIGN_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = CampaignEvents::CAMPAIGN_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = CampaignEvents::CAMPAIGN_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = CampaignEvents::CAMPAIGN_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new Events\CampaignEvent($entity, $isNew);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function setEvents(Campaign $entity, $sessionEvents, $sessionConnections, $deletedEvents): array
|
||||
{
|
||||
$existingEvents = $entity->getEvents()->toArray();
|
||||
$events = [];
|
||||
$hierarchy = [];
|
||||
|
||||
foreach ($sessionEvents as $properties) {
|
||||
$isNew = (!empty($properties['id']) && isset($existingEvents[$properties['id']])) ? false : true;
|
||||
$event = !$isNew ? $existingEvents[$properties['id']] : new Event();
|
||||
|
||||
foreach ($properties as $f => $v) {
|
||||
if ('id' == $f && str_starts_with($v, 'new')) {
|
||||
// set the temp ID used to be able to match up connections
|
||||
$event->setTempId($v);
|
||||
}
|
||||
|
||||
if (in_array($f, ['id', 'parent', 'campaign'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$func = 'set'.ucfirst($f);
|
||||
if (method_exists($event, $func)) {
|
||||
$event->$func($v);
|
||||
}
|
||||
}
|
||||
|
||||
ChannelExtractor::setChannel($event, $event, $this->eventCollector->getEventConfig($event));
|
||||
|
||||
$event->setCampaign($entity);
|
||||
$events[$properties['id']] = $event;
|
||||
}
|
||||
|
||||
foreach ($deletedEvents as $deleteMe) {
|
||||
if (isset($existingEvents[$deleteMe])) {
|
||||
// Remove child from parent
|
||||
$parent = $existingEvents[$deleteMe]->getParent();
|
||||
if ($parent) {
|
||||
$parent->removeChild($existingEvents[$deleteMe]);
|
||||
$existingEvents[$deleteMe]->removeParent();
|
||||
}
|
||||
|
||||
$entity->removeEvent($existingEvents[$deleteMe]);
|
||||
|
||||
unset($events[$deleteMe]);
|
||||
}
|
||||
}
|
||||
|
||||
$relationships = [];
|
||||
|
||||
if (isset($sessionConnections['connections'])) {
|
||||
foreach ($sessionConnections['connections'] as $connection) {
|
||||
$source = $connection['sourceId'];
|
||||
$target = $connection['targetId'];
|
||||
|
||||
if (in_array($source, ['lists', 'forms'])) {
|
||||
// Only concerned with events and not sources
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($connection['anchors']['source'])) {
|
||||
$sourceDecision = $connection['anchors']['source'];
|
||||
} else {
|
||||
$sourceDecision = (!empty($connection['anchors'][0])) ? $connection['anchors'][0]['endpoint'] : null;
|
||||
}
|
||||
|
||||
if ('leadsource' == $sourceDecision) {
|
||||
// Lead source connection that does not matter
|
||||
continue;
|
||||
}
|
||||
|
||||
$relationships[$target] = [
|
||||
'parent' => $source,
|
||||
'decision' => $sourceDecision,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Assign parent/child relationships
|
||||
foreach ($events as $id => $e) {
|
||||
if (isset($relationships[$id])) {
|
||||
// Has a parent
|
||||
$anchor = in_array($relationships[$id]['decision'], ['yes', 'no']) ? $relationships[$id]['decision'] : null;
|
||||
$events[$id]->setDecisionPath($anchor);
|
||||
|
||||
$parentId = $relationships[$id]['parent'];
|
||||
$events[$id]->setParent($events[$parentId]);
|
||||
|
||||
$hierarchy[$id] = $parentId;
|
||||
} elseif ($events[$id]->getParent()) {
|
||||
// No longer has a parent so null it out
|
||||
|
||||
// Remove decision so that it doesn't affect execution
|
||||
$events[$id]->setDecisionPath(null);
|
||||
|
||||
// Remove child from parent
|
||||
$parent = $events[$id]->getParent();
|
||||
$parent->removeChild($events[$id]);
|
||||
|
||||
// Remove parent from child
|
||||
$events[$id]->removeParent();
|
||||
$hierarchy[$id] = 'null';
|
||||
} else {
|
||||
// Is a parent
|
||||
$hierarchy[$id] = 'null';
|
||||
|
||||
// Remove decision so that it doesn't affect execution
|
||||
$events[$id]->setDecisionPath(null);
|
||||
}
|
||||
}
|
||||
|
||||
$entity->addEvents($events);
|
||||
|
||||
// set event order used when querying the events
|
||||
$this->buildOrder($hierarchy, $events, $entity);
|
||||
|
||||
uasort(
|
||||
$events,
|
||||
function ($a, $b): int {
|
||||
$aOrder = $a->getOrder();
|
||||
$bOrder = $b->getOrder();
|
||||
|
||||
return $aOrder <=> $bOrder;
|
||||
}
|
||||
);
|
||||
|
||||
// Persist events if campaign is being edited
|
||||
if ($entity->getId()) {
|
||||
$this->getEventRepository()->saveEntities($events);
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $persist
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function setCanvasSettings($entity, $settings, $persist = true, $events = null)
|
||||
{
|
||||
if (null === $events) {
|
||||
$events = $entity->getEvents();
|
||||
}
|
||||
|
||||
$tempIds = [];
|
||||
|
||||
foreach ($events as $e) {
|
||||
if ($e instanceof Event) {
|
||||
$tempIds[$e->getTempId()] = $e->getId();
|
||||
} else {
|
||||
$tempIds[$e['tempId']] = $e['id'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($settings['nodes'])) {
|
||||
$settings['nodes'] = [];
|
||||
}
|
||||
|
||||
foreach ($settings['nodes'] as &$node) {
|
||||
if (str_contains($node['id'], 'new')) {
|
||||
// Find the real one and update the node
|
||||
$node['id'] = str_replace($node['id'], $tempIds[$node['id']], $node['id']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($settings['connections'])) {
|
||||
$settings['connections'] = [];
|
||||
}
|
||||
|
||||
foreach ($settings['connections'] as &$connection) {
|
||||
// Check source
|
||||
if (str_contains($connection['sourceId'], 'new')) {
|
||||
// Find the real one and update the node
|
||||
$connection['sourceId'] = str_replace($connection['sourceId'], $tempIds[$connection['sourceId']], $connection['sourceId']);
|
||||
}
|
||||
|
||||
// Check target
|
||||
if (str_contains($connection['targetId'], 'new')) {
|
||||
// Find the real one and update the node
|
||||
$connection['targetId'] = str_replace($connection['targetId'], $tempIds[$connection['targetId']], $connection['targetId']);
|
||||
}
|
||||
|
||||
// Rebuild anchors
|
||||
if (!isset($connection['anchors']['source'])) {
|
||||
$anchors = [];
|
||||
foreach ($connection['anchors'] as $k => $anchor) {
|
||||
$type = (0 === $k) ? 'source' : 'target';
|
||||
$anchors[$type] = $anchor['endpoint'];
|
||||
}
|
||||
|
||||
$connection['anchors'] = $anchors;
|
||||
}
|
||||
}
|
||||
|
||||
$entity->setCanvasSettings($settings);
|
||||
|
||||
if ($persist) {
|
||||
$this->getRepository()->saveEntity($entity);
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of sources for a campaign.
|
||||
*/
|
||||
public function getLeadSources($campaign): array
|
||||
{
|
||||
$campaignId = ($campaign instanceof Campaign) ? $campaign->getId() : $campaign;
|
||||
|
||||
$sources = [];
|
||||
|
||||
// Lead lists
|
||||
$sources['lists'] = $this->getRepository()->getCampaignListSources($campaignId);
|
||||
|
||||
// Forms
|
||||
$sources['forms'] = $this->getRepository()->getCampaignFormSources($campaignId);
|
||||
|
||||
return $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add and/or delete lead sources from a campaign.
|
||||
*/
|
||||
public function setLeadSources(Campaign $entity, $addedSources, $deletedSources): void
|
||||
{
|
||||
foreach ($addedSources as $type => $sources) {
|
||||
foreach ($sources as $id => $label) {
|
||||
switch ($type) {
|
||||
case 'lists':
|
||||
$entity->addList($this->em->getReference(\Mautic\LeadBundle\Entity\LeadList::class, $id));
|
||||
break;
|
||||
case 'forms':
|
||||
$entity->addForm($this->em->getReference(Form::class, $id));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($deletedSources as $type => $sources) {
|
||||
foreach ($sources as $id => $label) {
|
||||
switch ($type) {
|
||||
case 'lists':
|
||||
$entity->removeList($this->em->getReference(\Mautic\LeadBundle\Entity\LeadList::class, $id));
|
||||
break;
|
||||
case 'forms':
|
||||
$entity->removeForm($this->em->getReference(Form::class, $id));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of source choices.
|
||||
*
|
||||
* @param string $sourceType
|
||||
* @param bool $globalOnly
|
||||
*/
|
||||
public function getSourceLists($sourceType = null, $globalOnly = false): array
|
||||
{
|
||||
$choices = [];
|
||||
switch ($sourceType) {
|
||||
case 'lists':
|
||||
case null:
|
||||
$choices['lists'] = [];
|
||||
$lists = $globalOnly ? $this->leadListModel->getGlobalLists() : $this->leadListModel->getUserLists();
|
||||
|
||||
if ($lists) {
|
||||
foreach ($lists as $list) {
|
||||
$choices['lists'][$list['id']] = $list['name'];
|
||||
}
|
||||
}
|
||||
|
||||
// no break
|
||||
case 'forms':
|
||||
case null:
|
||||
$choices['forms'] = [];
|
||||
$viewOther = $this->security->isGranted('form:forms:viewother');
|
||||
$repo = $this->formModel->getRepository();
|
||||
$repo->setCurrentUser($this->userHelper->getUser());
|
||||
|
||||
$forms = $repo->getFormList('', 0, 0, $viewOther, 'campaign');
|
||||
|
||||
foreach ($forms as $form) {
|
||||
$choices['forms'][$form['id']] = $form['name'];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($choices as &$typeChoices) {
|
||||
asort($typeChoices);
|
||||
}
|
||||
|
||||
return (null == $sourceType) ? $choices : $choices[$sourceType];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $form
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCampaignsByForm($form)
|
||||
{
|
||||
$formId = ($form instanceof Form) ? $form->getId() : $form;
|
||||
|
||||
return $this->getRepository()->findByFormId($formId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the campaigns a specific lead is part of.
|
||||
*
|
||||
* @param bool $forList
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLeadCampaigns(?Lead $lead = null, $forList = false)
|
||||
{
|
||||
static $campaigns = [];
|
||||
|
||||
if (null === $lead) {
|
||||
$lead = $this->contactTracker->getContact();
|
||||
}
|
||||
|
||||
if (!isset($campaigns[$lead->getId()])) {
|
||||
$repo = $this->getRepository();
|
||||
$leadId = $lead->getId();
|
||||
// get the campaigns the lead is currently part of
|
||||
$campaigns[$leadId] = $repo->getPublishedCampaigns(
|
||||
null,
|
||||
$lead->getId(),
|
||||
$forList,
|
||||
$this->security->isGranted($this->getPermissionBase().':viewother')
|
||||
);
|
||||
}
|
||||
|
||||
return $campaigns[$lead->getId()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of published campaigns.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPublishedCampaigns(bool $forList = false)
|
||||
{
|
||||
static $campaigns = [];
|
||||
|
||||
if (empty($campaigns)) {
|
||||
$campaigns = $this->getRepository()->getPublishedCampaigns(
|
||||
null,
|
||||
null,
|
||||
$forList,
|
||||
$this->security->isGranted($this->getPermissionBase().':viewother')
|
||||
);
|
||||
}
|
||||
|
||||
return $campaigns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a campaign lead, logs the error if saving fails.
|
||||
*/
|
||||
public function saveCampaignLead(CampaignLead $campaignLead): bool
|
||||
{
|
||||
try {
|
||||
$this->getCampaignLeadRepository()->saveEntity($campaignLead);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->log('error', $exception->getMessage(), ['exception' => $exception]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of leads in a campaign.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLeadDetails($campaign, $leads = null)
|
||||
{
|
||||
$campaignId = ($campaign instanceof Campaign) ? $campaign->getId() : $campaign;
|
||||
|
||||
if ($leads instanceof PersistentCollection) {
|
||||
$leads = array_keys($leads->toArray());
|
||||
}
|
||||
|
||||
return $this->em->getRepository(CampaignLead::class)->getLeadDetails($campaignId, $leads);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get leads for a campaign. If $event is passed in, only leads who have not triggered the event are returned.
|
||||
*
|
||||
* @param Campaign $campaign
|
||||
* @param array $event
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCampaignLeads($campaign, $event = null)
|
||||
{
|
||||
$campaignId = ($campaign instanceof Campaign) ? $campaign->getId() : $campaign;
|
||||
$eventId = (is_array($event) && isset($event['id'])) ? $event['id'] : $event;
|
||||
|
||||
return $this->em->getRepository(CampaignLead::class)->getLeads($campaignId, $eventId);
|
||||
}
|
||||
|
||||
public function getCampaignListIds($id): array
|
||||
{
|
||||
return $this->getRepository()->getCampaignListIds((int) $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line chart data of leads added to campaigns.
|
||||
*
|
||||
* @param string $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
|
||||
* @param string $dateFormat
|
||||
* @param array $filter
|
||||
* @param bool $canViewOthers
|
||||
*/
|
||||
public function getLeadsAddedLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
|
||||
{
|
||||
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
|
||||
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
$q = $query->prepareTimeDataQuery('campaign_leads', 'date_added', $filter);
|
||||
|
||||
if (!$canViewOthers) {
|
||||
$q->join('t', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'c.id = c.campaign_id')
|
||||
->andWhere('c.created_by = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
$data = $query->loadAndBuildTimeData($q);
|
||||
$chart->setDataset($this->translator->trans('mautic.campaign.campaign.leads'), $data);
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line chart data of hits.
|
||||
*
|
||||
* @param string|null $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
|
||||
* @param string $dateFormat
|
||||
* @param array $filter
|
||||
*/
|
||||
public function getCampaignMetricsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = []): array
|
||||
{
|
||||
$events = [];
|
||||
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
|
||||
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
|
||||
$contacts = $query->fetchTimeData('campaign_leads', 'date_added', $filter);
|
||||
$chart->setDataset($this->translator->trans('mautic.campaign.campaign.leads'), $contacts);
|
||||
|
||||
if (isset($filter['campaign_id'])) {
|
||||
$rawEvents = $this->getEventRepository()->getCampaignEvents($filter['campaign_id']);
|
||||
|
||||
// Group events by type
|
||||
foreach ($rawEvents as $event) {
|
||||
if (isset($events[$event['type']])) {
|
||||
$events[$event['type']][] = $event['id'];
|
||||
} else {
|
||||
$events[$event['type']] = [$event['id']];
|
||||
}
|
||||
}
|
||||
|
||||
if ($events) {
|
||||
foreach ($events as $type => $eventIds) {
|
||||
$filter['event_id'] = $eventIds;
|
||||
|
||||
if ($this->coreParametersHelper->get('campaign_use_summary')) {
|
||||
$q = $query->prepareTimeDataQuery('campaign_summary', 'date_triggered', $filter, 'triggered_count + non_action_path_taken_count', 'sum');
|
||||
$rawData = $q->executeQuery()->fetchAllAssociative();
|
||||
} else {
|
||||
// Exclude failed events
|
||||
$failedSq = $this->em->getConnection()->createQueryBuilder();
|
||||
$failedSq->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_failed_log', 'fe')
|
||||
->where(
|
||||
$failedSq->expr()->eq('fe.log_id', 't.id')
|
||||
);
|
||||
$filter['failed_events'] = [
|
||||
'subquery' => sprintf('NOT EXISTS (%s)', $failedSq->getSQL()),
|
||||
];
|
||||
|
||||
$q = $query->prepareTimeDataQuery('campaign_lead_event_log', 'date_triggered', $filter);
|
||||
$rawData = $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
if (!empty($rawData)) {
|
||||
$triggers = $query->completeTimeData($rawData);
|
||||
$chart->setDataset($this->translator->trans('mautic.campaign.'.$type), $triggers);
|
||||
}
|
||||
}
|
||||
unset($filter['event_id']);
|
||||
}
|
||||
}
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Campaign $entity
|
||||
* @param string $root
|
||||
* @param int $order
|
||||
*/
|
||||
protected function buildOrder($hierarchy, &$events, $entity, $root = 'null', $order = 1)
|
||||
{
|
||||
$count = count($hierarchy);
|
||||
if (1 === $count && 'null' === array_unique(array_values($hierarchy))[0]) {
|
||||
// no parents so leave order as is
|
||||
|
||||
return;
|
||||
} else {
|
||||
foreach ($hierarchy as $eventId => $parent) {
|
||||
if ($parent == $root || 1 === $count) {
|
||||
$events[$eventId]->setOrder($order);
|
||||
unset($hierarchy[$eventId]);
|
||||
if (count($hierarchy)) {
|
||||
$this->buildOrder($hierarchy, $events, $entity, $eventId, $order + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $limit
|
||||
* @param bool $maxLeads
|
||||
*/
|
||||
public function rebuildCampaignLeads(Campaign $campaign, $limit = 1000, $maxLeads = false, ?OutputInterface $output = null): int
|
||||
{
|
||||
$contactLimiter = new ContactLimiter($limit);
|
||||
|
||||
return $this->membershipBuilder->build($campaign, $contactLimiter, $maxLeads, $output);
|
||||
}
|
||||
|
||||
public function getCampaignIdsWithDependenciesOnSegment($segmentId): array
|
||||
{
|
||||
$entities = $this->getRepository()->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'l.id',
|
||||
'expr' => 'eq',
|
||||
'value' => $segmentId,
|
||||
],
|
||||
],
|
||||
],
|
||||
'joinLists' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$ids = [];
|
||||
foreach ($entities as $entity) {
|
||||
$ids[] = $entity->getId();
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getCampaignIdsWithDependenciesOnEmail(int $emailId): array
|
||||
{
|
||||
return $this->getRepository()->getCampaignIdsWithDependenciesOnEmail($emailId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getCampaignIdsWithDependenciesOnTagName(string $tagName): array
|
||||
{
|
||||
$entities = $this->getEventRepository()->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'e.type',
|
||||
'expr' => 'IN',
|
||||
'value' => ['lead.changetags', 'lead.tags'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$dependents = [];
|
||||
/** @var Event $entity */
|
||||
foreach ($entities as $entity) {
|
||||
$type = $entity->getType();
|
||||
$properties = $entity->getProperties();
|
||||
if ('lead.changetags' === $type) {
|
||||
$eventTags = array_merge([], $properties['add_tags'], $properties['remove_tags']);
|
||||
}
|
||||
if ('lead.tags' === $type) {
|
||||
$eventTags = $properties['tags'];
|
||||
}
|
||||
if (in_array($tagName, $eventTags)) {
|
||||
$dependents[] = $entity->getCampaign()->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($dependents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, array<string, int|string>>>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getCountryStats(Campaign $entity, \DateTimeImmutable $dateFrom, \DateTimeImmutable $dateTo): array
|
||||
{
|
||||
/** @var StatRepository $statRepo */
|
||||
$statRepo = $this->em->getRepository(Stat::class);
|
||||
$results['contacts'] = $this->getCampaignMembersGroupByCountry($entity, $dateFrom, $dateTo);
|
||||
|
||||
if ($entity->isEmailCampaign()) {
|
||||
$eventsEmailsSend = $entity->getEmailSendEvents();
|
||||
$eventsIds = $eventsEmailsSend->getKeys();
|
||||
$emailIds = [];
|
||||
|
||||
foreach ($eventsEmailsSend as $event) {
|
||||
$emailIds[] = $event->getChannelId();
|
||||
}
|
||||
|
||||
$emailStats = $statRepo->getStatsSummaryByCountry($dateFrom, $dateTo, $emailIds, 'campaign', $eventsIds);
|
||||
$results['read_count'] = $results['clicked_through_count'] = [];
|
||||
|
||||
foreach ($emailStats as $e) {
|
||||
$results['read_count'][] = array_intersect_key($e, array_flip(['country', 'read_count']));
|
||||
$results['clicked_through_count'][] = array_intersect_key($e, array_flip(['country', 'clicked_through_count']));
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get leads in a campaign grouped by country.
|
||||
*
|
||||
* @return array{}|array<int, array<string, string|null>>
|
||||
*/
|
||||
public function getCampaignMembersGroupByCountry(Campaign $campaign, \DateTimeImmutable $dateFromObject, \DateTimeImmutable $dateToObject): array
|
||||
{
|
||||
return $this->em->getRepository(CampaignLead::class)->getCampaignMembersGroupByCountry($campaign, $dateFromObject, $dateToObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CampaignAlreadyUnpublishedException
|
||||
* @throws CampaignVersionMismatchedException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function transactionalCampaignUnPublish(Campaign $campaign): void
|
||||
{
|
||||
$this->em->beginTransaction();
|
||||
$result = $this->getRepository()->getCampaignPublishAndVersionData($campaign->getId());
|
||||
|
||||
if (!(int) $result['is_published']) {
|
||||
$this->em->commit();
|
||||
throw new CampaignAlreadyUnpublishedException('Campaign is unpublished!');
|
||||
}
|
||||
|
||||
if ((int) $result['version'] !== $campaign->getVersion()) {
|
||||
$this->em->commit();
|
||||
throw new CampaignVersionMismatchedException('Version do not match!');
|
||||
}
|
||||
|
||||
$campaign->setIsPublished(false);
|
||||
$campaign->markForVersionIncrement();
|
||||
$this->saveEntity($campaign);
|
||||
$this->em->commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\AbstractCommonModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractCommonModel<LeadEventLog>
|
||||
*/
|
||||
class EventLogModel extends AbstractCommonModel
|
||||
{
|
||||
public function __construct(
|
||||
protected EventModel $eventModel,
|
||||
protected CampaignModel $campaignModel,
|
||||
protected IpLookupHelper $ipLookupHelper,
|
||||
protected EventScheduler $eventScheduler,
|
||||
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);
|
||||
}
|
||||
|
||||
public function getRepository(): LeadEventLogRepository
|
||||
{
|
||||
return $this->em->getRepository(LeadEventLog::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'campaign:campaigns';
|
||||
}
|
||||
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
/** @var LeadEventLog[] $logs */
|
||||
$logs = parent::getEntities($args);
|
||||
|
||||
if (!empty($args['campaign_id']) && !empty($args['contact_id'])) {
|
||||
/** @var Event[] $events */
|
||||
$events = $this->eventModel->getEntities(
|
||||
[
|
||||
'campaign_id' => $args['campaign_id'],
|
||||
'ignore_children' => true,
|
||||
'index_by' => 'id',
|
||||
'ignore_paginator' => true,
|
||||
]
|
||||
);
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$event = $log->getEvent()->getId();
|
||||
$events[$event]->addContactLog($log);
|
||||
}
|
||||
|
||||
return array_values($events);
|
||||
}
|
||||
|
||||
return $logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|mixed[]
|
||||
*/
|
||||
public function updateContactEvent(Event $event, Lead $contact, array $parameters): string|array
|
||||
{
|
||||
$campaign = $event->getCampaign();
|
||||
|
||||
// Check that contact is part of the campaign
|
||||
$membership = $campaign->getContactMembership($contact);
|
||||
if (0 === count($membership)) {
|
||||
return $this->translator->trans(
|
||||
'mautic.campaign.error.contact_not_in_campaign',
|
||||
['%campaign%' => $campaign->getId(), '%contact%' => $contact->getId()],
|
||||
'flashes'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var \Mautic\CampaignBundle\Entity\Lead $m */
|
||||
foreach ($membership as $m) {
|
||||
if ($m->getManuallyRemoved()) {
|
||||
return $this->translator->trans(
|
||||
'mautic.campaign.error.contact_not_in_campaign',
|
||||
['%campaign%' => $campaign->getId(), '%contact%' => $contact->getId()],
|
||||
'flashes'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that contact has not executed the event already
|
||||
$logs = $event->getContactLog($contact);
|
||||
$created = false;
|
||||
if (count($logs)) {
|
||||
$log = $logs[0];
|
||||
if ($log->getDateTriggered()) {
|
||||
return $this->translator->trans(
|
||||
'mautic.campaign.error.event_already_executed',
|
||||
[
|
||||
'%campaign%' => $campaign->getId(),
|
||||
'%event%' => $event->getId(),
|
||||
'%contact%' => $contact->getId(),
|
||||
'%dateTriggered%' => $log->getDateTriggered()->format(\DateTimeInterface::ATOM),
|
||||
],
|
||||
'flashes'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!isset($parameters['triggerDate']) && !isset($parameters['dateTriggered'])) {
|
||||
return $this->translator->trans(
|
||||
'mautic.campaign.error.event_must_be_scheduled',
|
||||
[
|
||||
'%campaign%' => $campaign->getId(),
|
||||
'%event%' => $event->getId(),
|
||||
'%contact%' => $contact->getId(),
|
||||
],
|
||||
'flashes'
|
||||
);
|
||||
}
|
||||
|
||||
$log = (new LeadEventLog())
|
||||
->setLead($contact)
|
||||
->setEvent($event);
|
||||
$created = true;
|
||||
}
|
||||
|
||||
foreach ($parameters as $property => $value) {
|
||||
switch ($property) {
|
||||
case 'dateTriggered':
|
||||
$log->setDateTriggered(
|
||||
new \DateTime($value)
|
||||
);
|
||||
break;
|
||||
case 'triggerDate':
|
||||
if (Event::TYPE_DECISION === $event->getEventType()) {
|
||||
return $this->translator->trans(
|
||||
'mautic.campaign.error.decision_cannot_be_scheduled',
|
||||
[
|
||||
'%campaign%' => $campaign->getId(),
|
||||
'%event%' => $event->getId(),
|
||||
'%contact%' => $contact->getId(),
|
||||
],
|
||||
'flashes'
|
||||
);
|
||||
}
|
||||
$log->setTriggerDate(
|
||||
new \DateTime($value)
|
||||
);
|
||||
break;
|
||||
case 'ipAddress':
|
||||
if (!defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED')) {
|
||||
$log->setIpAddress(
|
||||
$this->ipLookupHelper->getIpAddress($value)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'metadata':
|
||||
$metadata = $log->getMetadata();
|
||||
if (is_array($value)) {
|
||||
$newMetadata = $value;
|
||||
} elseif ($jsonDecoded = json_decode($value, true)) {
|
||||
$newMetadata = $jsonDecoded;
|
||||
} else {
|
||||
$newMetadata = (array) $value;
|
||||
}
|
||||
|
||||
$newMetadata = InputHelper::cleanArray($newMetadata);
|
||||
$log->setMetadata(array_merge($metadata, $newMetadata));
|
||||
break;
|
||||
case 'nonActionPathTaken':
|
||||
$log->setNonActionPathTaken((bool) $value);
|
||||
break;
|
||||
case 'channel':
|
||||
$log->setChannel(InputHelper::clean($value));
|
||||
break;
|
||||
case 'channelId':
|
||||
$log->setChannel(intval($value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->saveEntity($log);
|
||||
|
||||
return [$log, $created];
|
||||
}
|
||||
|
||||
public function saveEntity(LeadEventLog $entity): void
|
||||
{
|
||||
$triggerDate = $entity->getTriggerDate();
|
||||
if (null === $triggerDate) {
|
||||
// Reschedule for now
|
||||
$triggerDate = new \DateTime();
|
||||
}
|
||||
|
||||
$this->eventScheduler->reschedule($entity, $triggerDate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Model;
|
||||
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Event\DeleteEvent;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
|
||||
/**
|
||||
* @extends FormModel<Event>
|
||||
*/
|
||||
class EventModel extends FormModel
|
||||
{
|
||||
/**
|
||||
* @return \Mautic\CampaignBundle\Entity\EventRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(Event::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\CampaignBundle\Entity\CampaignRepository
|
||||
*/
|
||||
public function getCampaignRepository()
|
||||
{
|
||||
return $this->em->getRepository(Campaign::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadEventLogRepository
|
||||
*/
|
||||
public function getLeadEventLogRepository()
|
||||
{
|
||||
return $this->em->getRepository(LeadEventLog::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'campaign:campaigns';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific entity or generate a new one if id is empty.
|
||||
*/
|
||||
public function getEntity($id = null): ?Event
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Event();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
public function deleteEvents($currentEvents, $deletedEvents): void
|
||||
{
|
||||
$deletedKeys = [];
|
||||
foreach ($deletedEvents as $k => $deleteMe) {
|
||||
if ($deleteMe instanceof Event) {
|
||||
$deleteMe = $deleteMe->getId();
|
||||
}
|
||||
|
||||
if (str_starts_with($deleteMe, 'new')) {
|
||||
unset($deletedEvents[$k]);
|
||||
}
|
||||
|
||||
if (isset($currentEvents[$deleteMe])) {
|
||||
unset($deletedEvents[$k]);
|
||||
}
|
||||
|
||||
if (isset($deletedEvents[$k])) {
|
||||
$deletedKeys[] = $deleteMe;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($deletedEvents)) {
|
||||
// wipe out any references to these events to prevent restraint violations
|
||||
$this->getRepository()->nullEventRelationships($deletedKeys);
|
||||
$this->getRepository()->setEventsAsDeleted($deletedEvents);
|
||||
$this->dispatcher->dispatch(new DeleteEvent($deletedKeys), CampaignEvents::ON_EVENT_DELETE);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteEventsByCampaignId(int $campaignId): void
|
||||
{
|
||||
$eventIds = $this->getRepository()->getCampaignEventIds($campaignId);
|
||||
$this->deleteEventsByEventIds($eventIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $eventIds
|
||||
*/
|
||||
public function deleteEventsByEventIds(array $eventIds): void
|
||||
{
|
||||
$this->getRepository()->deleteEvents($eventIds);
|
||||
$this->dispatcher->dispatch(new DeleteEvent($eventIds), CampaignEvents::ON_AFTER_EVENTS_DELETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line chart data of campaign events.
|
||||
*
|
||||
* @param string $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
|
||||
* @param string $dateFormat
|
||||
* @param array $filter
|
||||
* @param bool $canViewOthers
|
||||
*/
|
||||
public function getEventLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
|
||||
{
|
||||
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
|
||||
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
$q = $query->prepareTimeDataQuery('campaign_lead_event_log', 'date_triggered', $filter);
|
||||
|
||||
if (!$canViewOthers) {
|
||||
$q->join('t', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'c.id = t.campaign_id')
|
||||
->andWhere('c.created_by = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
$data = $query->loadAndBuildTimeData($q);
|
||||
$chart->setDataset($this->translator->trans('mautic.campaign.triggered.events'), $data);
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Model\Exceptions;
|
||||
|
||||
class CampaignAlreadyUnpublishedException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Model\Exceptions;
|
||||
|
||||
class CampaignVersionMismatchedException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Model;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Entity\Summary;
|
||||
use Mautic\CampaignBundle\Entity\SummaryRepository;
|
||||
use Mautic\CoreBundle\Helper\ProgressBarHelper;
|
||||
use Mautic\CoreBundle\Model\AbstractCommonModel;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractCommonModel<Summary>
|
||||
*/
|
||||
class SummaryModel extends AbstractCommonModel
|
||||
{
|
||||
private array $logData = [];
|
||||
|
||||
/**
|
||||
* Collapse Event Log entities into insert/update queries for the campaign summary.
|
||||
*
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
*/
|
||||
public function updateSummary(iterable $logs): void
|
||||
{
|
||||
$now = new \DateTime();
|
||||
|
||||
/** @var LeadEventLog $log */
|
||||
foreach ($logs as $log) {
|
||||
if (!$log->getDateTriggered()) {
|
||||
// This shouldn't normally happen but it's possible to have a log without a date triggered
|
||||
// as it is a nullable field and it can be created without date triggered for example via API.
|
||||
continue;
|
||||
}
|
||||
|
||||
$timestamp = $log->getDateTriggered()->getTimestamp();
|
||||
$timestamp -= ($timestamp % 3600);
|
||||
$dateFrom = $now->setTimestamp($timestamp);
|
||||
$dateTo = (clone $dateFrom)->modify('+1 hour -1 second');
|
||||
|
||||
$campaign = $log->getCampaign();
|
||||
$event = $log->getEvent();
|
||||
$key = $campaign->getId().'.'.$event->getId().'.'.$timestamp;
|
||||
|
||||
$this->logData[$key] = [
|
||||
'campaignId' => $campaign->getId(),
|
||||
'eventId' => $event->getId(),
|
||||
'dateFrom' => $dateFrom,
|
||||
'dateTo' => $dateTo,
|
||||
];
|
||||
}
|
||||
|
||||
if (count($this->logData) >= 100) {
|
||||
$this->persistSummaries();
|
||||
}
|
||||
}
|
||||
|
||||
public function getRepository(): SummaryRepository
|
||||
{
|
||||
return $this->em->getRepository(Summary::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'campaign:campaigns';
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize all of history.
|
||||
*
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
*/
|
||||
public function summarize(OutputInterface $output, int $hoursPerBatch = 1, ?int $maxHours = null, bool $rebuild = false): void
|
||||
{
|
||||
$start = null;
|
||||
|
||||
if (!$rebuild) {
|
||||
$start = $this->getRepository()->getOldestTriggeredDate();
|
||||
}
|
||||
|
||||
// Start with the current hour.
|
||||
$start ??= new \DateTime('+1 hour');
|
||||
$start->setTimestamp($start->getTimestamp() - ($start->getTimestamp() % 3600));
|
||||
$end = $this->getCampaignLeadEventLogRepository()->getOldestTriggeredDate();
|
||||
|
||||
if (!$end) {
|
||||
$output->writeln('There are no records in the campaign lead event log table. Nothing to summarize.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$end = $end->setTimestamp($end->getTimestamp() - ($end->getTimestamp() % 3600));
|
||||
$startedAt = new \DateTime();
|
||||
$output->writeln('<comment>Started at: '.$startedAt->format('Y-m-d H:i:s').'</comment>');
|
||||
|
||||
if ($end <= $start) {
|
||||
$hours = ($end->diff($start)->days * 24) + $end->diff($start)->h;
|
||||
|
||||
if ($maxHours && $hours > $maxHours) {
|
||||
$end = clone $start;
|
||||
$end = $end->sub(new \DateInterval('PT'.$maxHours.'H'));
|
||||
}
|
||||
|
||||
$progressBar = ProgressBarHelper::init($output, $hours);
|
||||
$progressBar->start();
|
||||
|
||||
$interval = new \DateInterval('PT'.$hoursPerBatch.'H');
|
||||
$dateFrom = clone $start;
|
||||
$dateTo = (clone $start)->modify('-1 second');
|
||||
|
||||
do {
|
||||
$dateFrom = $dateFrom->sub($interval);
|
||||
$dateFromFormatted = $dateFrom->format('Y-m-d H:i:s');
|
||||
$dateToFormatted = $dateTo->format('Y-m-d H:i:s');
|
||||
$output->write("\t".$dateFromFormatted.' - '.$dateToFormatted);
|
||||
$this->getRepository()->summarize($dateFrom, $dateTo);
|
||||
$progressBar->advance($hoursPerBatch);
|
||||
$dateTo = $dateTo->sub($interval);
|
||||
} while ($end < $dateFrom);
|
||||
|
||||
$progressBar->finish();
|
||||
|
||||
$output->writeln("\n".'<info>Updating summary for log counts processed</info>');
|
||||
}
|
||||
|
||||
$this->outputProcessTime($startedAt, $output);
|
||||
}
|
||||
|
||||
public function getCampaignLeadEventLogRepository(): LeadEventLogRepository
|
||||
{
|
||||
return $this->em->getRepository(LeadEventLog::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
*/
|
||||
public function persistSummaries(): void
|
||||
{
|
||||
foreach ($this->logData as $log) {
|
||||
$dateFrom = $log['dateFrom'];
|
||||
$dateTo = $log['dateTo'];
|
||||
$campaignId = $log['campaignId'];
|
||||
$eventId = $log['eventId'];
|
||||
$this->getRepository()->summarize($dateFrom, $dateTo, $campaignId, $eventId);
|
||||
}
|
||||
}
|
||||
|
||||
private function outputProcessTime(\DateTime $startedAt, OutputInterface $output): void
|
||||
{
|
||||
$endedAt = new \DateTime();
|
||||
$output->writeln("\n".'<comment>Ended at: '.$endedAt->format('Y-m-d H:i:s').'</comment>');
|
||||
$completedInterval = $startedAt->diff($endedAt);
|
||||
$output->writeln('<info>Summary completed in: '.$completedInterval->format('%H:%I:%S').'</info>');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user