897 lines
30 KiB
PHP
Executable File
897 lines
30 KiB
PHP
Executable File
<?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();
|
|
}
|
|
}
|