Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,750 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CampaignBundle\Validator\Constraints\NoOrphanEvents;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\OptimisticLockInterface;
|
||||
use Mautic\CoreBundle\Entity\OptimisticLockTrait;
|
||||
use Mautic\CoreBundle\Entity\PublishStatusIconAttributesInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\FormBundle\Entity\Form;
|
||||
use Mautic\LeadBundle\Entity\Lead as Contact;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use Mautic\ProjectBundle\Entity\ProjectTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('campaign:campaigns:viewown')"),
|
||||
new Post(security: "is_granted('campaign:campaigns:create')"),
|
||||
new Get(security: "is_granted('campaign:campaigns:viewown')"),
|
||||
new Put(security: "is_granted('campaign:campaigns:editown')"),
|
||||
new Patch(security: "is_granted('campaign:campaigns:editother')"),
|
||||
new Delete(security: "is_granted('campaign:campaigns:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['campaign:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category', 'events', 'lists', 'forms', 'fields', 'actions'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['campaign:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Campaign extends FormEntity implements PublishStatusIconAttributesInterface, OptimisticLockInterface, UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
|
||||
use OptimisticLockTrait;
|
||||
|
||||
use ProjectTrait;
|
||||
|
||||
public const TABLE_NAME = 'campaigns';
|
||||
public const ENTITY_NAME = 'campaign';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private $publishUp;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private $publishDown;
|
||||
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
public ?\DateTimeInterface $deleted = null;
|
||||
|
||||
/**
|
||||
* @var Category|null
|
||||
**/
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private $category;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Event>|ArrayCollection<int, Event>
|
||||
*/
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private $events;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int, Lead>
|
||||
*/
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private Collection $leads;
|
||||
|
||||
/**
|
||||
* @var Collection<int, LeadList>
|
||||
*/
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private Collection $lists;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Form>
|
||||
*/
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private Collection $forms;
|
||||
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private array $canvasSettings = [];
|
||||
|
||||
#[Groups(['campaign:read', 'campaign:write'])]
|
||||
private bool $allowRestart = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->events = new ArrayCollection();
|
||||
$this->leads = new ArrayCollection();
|
||||
$this->lists = new ArrayCollection();
|
||||
$this->forms = new ArrayCollection();
|
||||
$this->initializeProjects();
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->leads = new ArrayCollection();
|
||||
$this->events = new ArrayCollection();
|
||||
$this->lists = new ArrayCollection();
|
||||
$this->forms = new ArrayCollection();
|
||||
$this->id = null;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(CampaignRepository::class);
|
||||
|
||||
$builder->addIdColumns();
|
||||
|
||||
$builder->addPublishDates();
|
||||
|
||||
$builder->addCategory();
|
||||
|
||||
$builder->createOneToMany('events', Event::class)
|
||||
->setIndexBy('id')
|
||||
->setOrderBy(['order' => 'ASC'])
|
||||
->mappedBy('campaign')
|
||||
->cascadeAll()
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('leads', Lead::class)
|
||||
->mappedBy('campaign')
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createManyToMany('lists', LeadList::class)
|
||||
->setJoinTable('campaign_leadlist_xref')
|
||||
->setIndexBy('id')
|
||||
->addInverseJoinColumn('leadlist_id', 'id', false, false, 'CASCADE')
|
||||
->addJoinColumn('campaign_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->createManyToMany('forms', Form::class)
|
||||
->setJoinTable('campaign_form_xref')
|
||||
->setIndexBy('id')
|
||||
->addInverseJoinColumn('form_id', 'id', false, false, 'CASCADE')
|
||||
->addJoinColumn('campaign_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->createField('canvasSettings', 'array')
|
||||
->columnName('canvas_settings')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addNamedField('allowRestart', 'boolean', 'allow_restart');
|
||||
$builder->addNullableField('deleted', 'datetime');
|
||||
|
||||
self::addVersionField($builder);
|
||||
static::addUuidField($builder);
|
||||
self::addProjectsField($builder, 'campaign_projects_xref', 'campaign_id');
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint(
|
||||
'name',
|
||||
new Assert\NotBlank(
|
||||
[
|
||||
'message' => 'mautic.core.name.required',
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$metadata->addConstraint(new NoOrphanEvents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('campaign')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'category',
|
||||
'description',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'allowRestart',
|
||||
'publishUp',
|
||||
'publishDown',
|
||||
'events',
|
||||
'forms',
|
||||
'lists', // @deprecated, will be renamed to 'segments' in 3.0.0
|
||||
'canvasSettings',
|
||||
]
|
||||
)
|
||||
->setGroupPrefix('campaignBasic')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'allowRestart',
|
||||
'events',
|
||||
'publishUp',
|
||||
'publishDown',
|
||||
'deleted',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'campaign');
|
||||
}
|
||||
|
||||
public function convertToArray(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prop
|
||||
* @param mixed $val
|
||||
*/
|
||||
protected function isChanged($prop, $val)
|
||||
{
|
||||
$getter = 'get'.ucfirst($prop);
|
||||
$current = $this->$getter();
|
||||
if ('category' == $prop) {
|
||||
$currentId = ($current) ? $current->getId() : '';
|
||||
$newId = ($val) ? $val->getId() : null;
|
||||
if ($currentId != $newId) {
|
||||
$this->changes[$prop] = [$currentId, $newId];
|
||||
}
|
||||
} elseif ('projects' === $prop) {
|
||||
// Initialize project tracking on first change
|
||||
if (!isset($this->changes['projects']['old'])) {
|
||||
$currentProjects = array_map(fn ($project) => $project->getName(), iterator_to_array($current));
|
||||
$this->changes['projects'] = [
|
||||
'old' => $currentProjects,
|
||||
'new' => $currentProjects,
|
||||
];
|
||||
}
|
||||
|
||||
// Update the new state based on the operation
|
||||
if ($val instanceof Project) {
|
||||
// Add project if not already in the list
|
||||
$projectName = $val->getName();
|
||||
if (!in_array($projectName, $this->changes['projects']['new'], true)) {
|
||||
$this->changes['projects']['new'][] = $projectName;
|
||||
}
|
||||
} else {
|
||||
// Remove project from the list
|
||||
$this->changes['projects']['new'] = array_values(
|
||||
array_diff($this->changes['projects']['new'], [$val])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
parent::isChanged($prop, $val);
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to convert projects changes to final format.
|
||||
*/
|
||||
public function getChanges($includePast = false)
|
||||
{
|
||||
$changes = parent::getChanges($includePast);
|
||||
|
||||
// Convert projects format if it exists and is in the intermediate format
|
||||
if (isset($changes['projects']['old']) && isset($changes['projects']['new'])) {
|
||||
$changes['projects'] = [
|
||||
implode(', ', $changes['projects']['old']),
|
||||
implode(', ', $changes['projects']['new']),
|
||||
];
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $description
|
||||
*
|
||||
* @return Campaign
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->isChanged('description', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Campaign
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls $this->addEvent on every item in the collection.
|
||||
*
|
||||
* @return Campaign
|
||||
*/
|
||||
public function addEvents(array $events)
|
||||
{
|
||||
foreach ($events as $id => $event) {
|
||||
$this->addEvent($id, $event);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Campaign
|
||||
*/
|
||||
public function addEvent($key, Event $event)
|
||||
{
|
||||
if ($changes = $event->getChanges()) {
|
||||
$this->changes['events']['added'][$key] = [$key, $changes];
|
||||
}
|
||||
$this->events[$key] = $event;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeEvent(Event $event): void
|
||||
{
|
||||
$this->changes['events']['removed'][$event->getId()] = $event->getName();
|
||||
|
||||
$this->events->removeElement($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection<int, Event>
|
||||
*/
|
||||
public function getEvents()
|
||||
{
|
||||
return $this->events;
|
||||
}
|
||||
|
||||
public function getRootEvents(): ArrayCollection
|
||||
{
|
||||
$criteria = Criteria::create()->where(
|
||||
Criteria::expr()->andX(
|
||||
Criteria::expr()->isNull('parent'),
|
||||
Criteria::expr()->isNull('deleted')
|
||||
)
|
||||
);
|
||||
$events = $this->getEvents()->matching($criteria);
|
||||
|
||||
return $this->reindexEventsByIdKey($events);
|
||||
}
|
||||
|
||||
public function getInactionBasedEvents(): ArrayCollection
|
||||
{
|
||||
$criteria = Criteria::create()->where(Criteria::expr()->eq('decisionPath', Event::PATH_INACTION));
|
||||
$events = $this->getEvents()->matching($criteria);
|
||||
|
||||
return $this->reindexEventsByIdKey($events);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
*
|
||||
* @return ArrayCollection<int,Event>
|
||||
*/
|
||||
public function getEventsByType($type): ArrayCollection
|
||||
{
|
||||
$criteria = Criteria::create()->where(Criteria::expr()->eq('eventType', $type));
|
||||
$events = $this->getEvents()->matching($criteria);
|
||||
|
||||
return $this->reindexEventsByIdKey($events);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection<int, Event>
|
||||
*/
|
||||
public function getEmailSendEvents(): ArrayCollection
|
||||
{
|
||||
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', 'email.send'));
|
||||
$events = $this->getEvents()->matching($criteria);
|
||||
|
||||
// Doctrine loses the indexBy mapping definition when using matching so we have to manually reset them.
|
||||
// @see https://github.com/doctrine/doctrine2/issues/4693
|
||||
$keyedArrayCollection = new ArrayCollection();
|
||||
/** @var Event $event */
|
||||
foreach ($events as $event) {
|
||||
$keyedArrayCollection->set($event->getId(), $event);
|
||||
}
|
||||
|
||||
return $keyedArrayCollection;
|
||||
}
|
||||
|
||||
public function isEmailCampaign(): bool
|
||||
{
|
||||
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', 'email.send'))->setMaxResults(1);
|
||||
$emailEvent = $this->getEvents()->matching($criteria);
|
||||
|
||||
return !$emailEvent->isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?\DateTime $publishUp
|
||||
*
|
||||
* @return Campaign
|
||||
*/
|
||||
public function setPublishUp($publishUp)
|
||||
{
|
||||
$this->isChanged('publishUp', $publishUp);
|
||||
$this->publishUp = $publishUp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getPublishUp()
|
||||
{
|
||||
return $this->publishUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?\DateTime $publishDown
|
||||
*
|
||||
* @return Campaign
|
||||
*/
|
||||
public function setPublishDown($publishDown)
|
||||
{
|
||||
$this->isChanged('publishDown', $publishDown);
|
||||
$this->publishDown = $publishDown;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPublishDown()
|
||||
{
|
||||
return $this->publishDown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $category
|
||||
*/
|
||||
public function setCategory($category): void
|
||||
{
|
||||
$this->isChanged('category', $category);
|
||||
$this->category = $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Campaign
|
||||
*/
|
||||
public function addLead($key, Lead $lead)
|
||||
{
|
||||
$action = ($this->leads->contains($lead)) ? 'updated' : 'added';
|
||||
$leadEntity = $lead->getLead();
|
||||
|
||||
$this->changes['leads'][$action][$leadEntity->getId()] = $leadEntity->getPrimaryIdentifier();
|
||||
$this->leads[$key] = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeLead(Lead $lead): void
|
||||
{
|
||||
$leadEntity = $lead->getLead();
|
||||
$this->changes['leads']['removed'][$leadEntity->getId()] = $leadEntity->getPrimaryIdentifier();
|
||||
$this->leads->removeElement($lead);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead[]|Collection
|
||||
*/
|
||||
public function getLeads()
|
||||
{
|
||||
return $this->leads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection<int, LeadList>
|
||||
*/
|
||||
public function getLists()
|
||||
{
|
||||
return $this->lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Campaign
|
||||
*/
|
||||
public function addList(LeadList $list)
|
||||
{
|
||||
$this->lists[$list->getId()] = $list;
|
||||
|
||||
$this->changes['lists']['added'][$list->getId()] = $list->getName();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeList(LeadList $list): void
|
||||
{
|
||||
$this->changes['lists']['removed'][$list->getId()] = $list->getName();
|
||||
$this->lists->removeElement($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection<int, Form>
|
||||
*/
|
||||
public function getForms()
|
||||
{
|
||||
return $this->forms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Campaign
|
||||
*/
|
||||
public function addForm(Form $form)
|
||||
{
|
||||
$this->forms[$form->getId()] = $form;
|
||||
|
||||
$this->changes['forms']['added'][$form->getId()] = $form->getName();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeForm(Form $form): void
|
||||
{
|
||||
$this->changes['forms']['removed'][$form->getId()] = $form->getName();
|
||||
$this->forms->removeElement($form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCanvasSettings()
|
||||
{
|
||||
return $this->canvasSettings;
|
||||
}
|
||||
|
||||
public function setCanvasSettings(array $canvasSettings): void
|
||||
{
|
||||
$this->canvasSettings = $canvasSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any orphan events that are not connected to any parent node.
|
||||
*/
|
||||
public function hasOrphanEvents(): bool
|
||||
{
|
||||
$canvasSettings = $this->getCanvasSettings() ?? [];
|
||||
|
||||
if (empty($canvasSettings['nodes'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract event IDs from canvas nodes (excludes 'lists', 'forms' and other non-event nodes)
|
||||
$eventIds = array_filter(
|
||||
array_column($canvasSettings['nodes'], 'id'),
|
||||
fn ($id) => !in_array($id, ['lists', 'forms'])
|
||||
);
|
||||
|
||||
if (empty($eventIds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract connected event IDs from connections
|
||||
$connectedEventIds = [];
|
||||
if (!empty($canvasSettings['connections'])) {
|
||||
$connectedEventIds = array_filter(array_column($canvasSettings['connections'], 'targetId'));
|
||||
}
|
||||
|
||||
return !empty(array_diff($eventIds, $connectedEventIds));
|
||||
}
|
||||
|
||||
public function getAllowRestart(): bool
|
||||
{
|
||||
return (bool) $this->allowRestart;
|
||||
}
|
||||
|
||||
public function allowRestart(): bool
|
||||
{
|
||||
return $this->getAllowRestart();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $allowRestart
|
||||
*
|
||||
* @return Campaign
|
||||
*/
|
||||
public function setAllowRestart($allowRestart)
|
||||
{
|
||||
$allowRestart = (bool) $allowRestart;
|
||||
$this->isChanged('allowRestart', $allowRestart);
|
||||
|
||||
$this->allowRestart = $allowRestart;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDeleted(?\DateTimeInterface $deleted): void
|
||||
{
|
||||
$this->isChanged('deleted', $deleted);
|
||||
$this->deleted = $deleted;
|
||||
}
|
||||
|
||||
public function isDeleted(): bool
|
||||
{
|
||||
return !is_null($this->deleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contact membership.
|
||||
*/
|
||||
public function getContactMembership(Contact $contact): Collection
|
||||
{
|
||||
return $this->leads->matching(
|
||||
Criteria::create()
|
||||
->where(Criteria::expr()->eq('lead', $contact))
|
||||
->orderBy(['dateAdded' => Order::Descending->value])
|
||||
);
|
||||
}
|
||||
|
||||
public function getOnclickMethod(): string
|
||||
{
|
||||
return 'Mautic.confirmationCampaignPublishStatus(mQuery(this));';
|
||||
}
|
||||
|
||||
public function getDataAttributes(): array
|
||||
{
|
||||
return [
|
||||
'data-toggle' => 'confirmation',
|
||||
'data-confirm-callback' => 'confirmCallbackCampaignPublishStatus',
|
||||
'data-cancel-callback' => 'dismissConfirmation',
|
||||
];
|
||||
}
|
||||
|
||||
public function getTranslationKeysDataAttributes(): array
|
||||
{
|
||||
return [
|
||||
'data-message' => 'mautic.campaign.form.confirmation.message',
|
||||
'data-confirm-text' => 'mautic.campaign.form.confirmation.confirm_text',
|
||||
'data-cancel-text' => 'mautic.campaign.form.confirmation.cancel_text',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-index collection by event ID to work around Doctrine's indexBy mapping issue.
|
||||
*
|
||||
* @see https://github.com/doctrine/doctrine2/issues/4693
|
||||
*/
|
||||
private function reindexEventsByIdKey(Collection $events): ArrayCollection
|
||||
{
|
||||
// Doctrine loses the indexBy mapping definition when using matching so we have to manually reset them.
|
||||
// @see https://github.com/doctrine/doctrine2/issues/4693
|
||||
$keyedArrayCollection = new ArrayCollection();
|
||||
/** @var Event $event */
|
||||
foreach ($events as $event) {
|
||||
$keyedArrayCollection->set($event->getId(), $event);
|
||||
}
|
||||
unset($events);
|
||||
|
||||
return $keyedArrayCollection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Cache\QueryCacheProfile;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Mautic\CampaignBundle\Entity\Result\CountResult;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Campaign>
|
||||
*/
|
||||
class CampaignRepository extends CommonRepository
|
||||
{
|
||||
use ContactLimiterTrait;
|
||||
use ReplicaConnectionTrait;
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder();
|
||||
$q->select($this->getTableAlias().', cat')
|
||||
->from(Campaign::class, $this->getTableAlias(), $this->getTableAlias().'.id')
|
||||
->leftJoin($this->getTableAlias().'.category', 'cat');
|
||||
|
||||
if (!empty($args['joinLists'])) {
|
||||
$q->leftJoin($this->getTableAlias().'.lists', 'l');
|
||||
}
|
||||
|
||||
if (!empty($args['joinForms'])) {
|
||||
$q->leftJoin($this->getTableAlias().'.forms', 'f');
|
||||
}
|
||||
$q->where($q->expr()->isNull($this->getTableAlias().'.deleted'));
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
public function setCampaignAsDeleted(int $campaignId): void
|
||||
{
|
||||
$dateTime = (new \DateTime())->format('Y-m-d H:i:s');
|
||||
|
||||
$this->getEntityManager()->getConnection()->update(
|
||||
MAUTIC_TABLE_PREFIX.Event::TABLE_NAME,
|
||||
['deleted' => $dateTime],
|
||||
['campaign_id' => $campaignId]
|
||||
);
|
||||
|
||||
$this->getEntityManager()->getConnection()->update(
|
||||
MAUTIC_TABLE_PREFIX.Campaign::TABLE_NAME,
|
||||
['deleted' => $dateTime, 'is_published' => 0],
|
||||
['id' => $campaignId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all published (and active) campaigns (optionally for a specific lead).
|
||||
*
|
||||
* @param bool $forList If true, returns ID and name only
|
||||
* @param bool $viewOther If true, returns all the campaigns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPublishedCampaigns($specificId = null, ?int $leadId = null, $forList = false, $viewOther = false)
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(Campaign::class, 'c', 'c.id');
|
||||
|
||||
if ($forList && $leadId) {
|
||||
$q->select('partial c.{id, name}, partial l.{campaign, lead, dateAdded, manuallyAdded, manuallyRemoved}, partial ll.{id}');
|
||||
} elseif ($forList) {
|
||||
$q->select('partial c.{id, name}, partial ll.{id}');
|
||||
} else {
|
||||
$q->select('c, l, partial ll.{id}')
|
||||
->leftJoin('c.events', 'e')
|
||||
->leftJoin('e.log', 'o');
|
||||
}
|
||||
|
||||
if ($leadId || !$forList) {
|
||||
$q->leftJoin('c.leads', 'l');
|
||||
}
|
||||
|
||||
$q->leftJoin('c.lists', 'll')
|
||||
->where($this->getPublishedByDateExpression($q));
|
||||
|
||||
if (!$viewOther) {
|
||||
$q->andWhere($q->expr()->eq('c.createdBy', ':id'))
|
||||
->setParameter('id', $this->currentUser->getId());
|
||||
}
|
||||
|
||||
if (!empty($specificId)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('c.id', (int) $specificId)
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($leadId)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('IDENTITY(l.lead)', (int) $leadId)
|
||||
);
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('l.manuallyRemoved', ':manuallyRemoved')
|
||||
)->setParameter('manuallyRemoved', false);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all published (and active) campaigns that specific lead lists are part of.
|
||||
*
|
||||
* @param int|array $leadLists
|
||||
*/
|
||||
public function getPublishedCampaignsByLeadLists($leadLists): array
|
||||
{
|
||||
if (!is_array($leadLists)) {
|
||||
$leadLists = [(int) $leadLists];
|
||||
} else {
|
||||
foreach ($leadLists as &$id) {
|
||||
$id = (int) $id;
|
||||
}
|
||||
}
|
||||
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('c.id, c.name, ll.leadlist_id as list_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaigns', 'c');
|
||||
|
||||
$q->join('c', MAUTIC_TABLE_PREFIX.'campaign_leadlist_xref', 'll', 'c.id = ll.campaign_id')
|
||||
->where($this->getPublishedByDateExpression($q));
|
||||
|
||||
$q->andWhere(
|
||||
$q->expr()->in('ll.leadlist_id', $leadLists)
|
||||
);
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$campaigns = [];
|
||||
foreach ($results as $result) {
|
||||
if (!isset($campaigns[$result['id']])) {
|
||||
$campaigns[$result['id']] = [
|
||||
'id' => $result['id'],
|
||||
'name' => $result['name'],
|
||||
'lists' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$campaigns[$result['id']]['lists'][$result['list_id']] = [
|
||||
'id' => $result['list_id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $campaigns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of list IDs assigned to this campaign.
|
||||
*
|
||||
* @param int|null $id
|
||||
*/
|
||||
public function getCampaignListIds($id = null): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leadlist_xref', 'cl');
|
||||
|
||||
if ($id) {
|
||||
$q->select('cl.leadlist_id')
|
||||
->where(
|
||||
$q->expr()->eq('cl.campaign_id', $id)
|
||||
);
|
||||
} else {
|
||||
// Retrieve a list of unique IDs that are assigned to a campaign
|
||||
$q->select('DISTINCT cl.leadlist_id');
|
||||
}
|
||||
|
||||
$lists = [];
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($results as $r) {
|
||||
$lists[] = $r['leadlist_id'];
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of list IDs => name assigned to this campaign.
|
||||
*/
|
||||
public function getCampaignListSources($id): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('cl.leadlist_id, l.name')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leadlist_xref', 'cl')
|
||||
->join('cl', MAUTIC_TABLE_PREFIX.'lead_lists', 'l', 'l.id = cl.leadlist_id');
|
||||
$q->where(
|
||||
$q->expr()->eq('cl.campaign_id', $id)
|
||||
);
|
||||
|
||||
$lists = [];
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($results as $r) {
|
||||
$lists[$r['leadlist_id']] = $r['name'];
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of form IDs => name assigned to this campaign.
|
||||
*/
|
||||
public function getCampaignFormSources($id): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('cf.form_id, f.name')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_form_xref', 'cf')
|
||||
->join('cf', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = cf.form_id');
|
||||
$q->where(
|
||||
$q->expr()->eq('cf.campaign_id', $id)
|
||||
);
|
||||
|
||||
$forms = [];
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($results as $r) {
|
||||
$forms[$r['form_id']] = $r['name'];
|
||||
}
|
||||
|
||||
return $forms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function findByFormId($formId)
|
||||
{
|
||||
$q = $this->createQueryBuilder('c')
|
||||
->join('c.forms', 'f');
|
||||
$q->where(
|
||||
$q->expr()->eq('f.id', $formId)
|
||||
);
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'c';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause($q, $filter, [
|
||||
'c.name',
|
||||
'c.description',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
[$expr, $parameters] = $this->addStandardSearchCommandWhereClause($q, $filter);
|
||||
if ($expr) {
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
$unique = $this->generateRandomParameterName();
|
||||
|
||||
switch ($filter->command) {
|
||||
case $this->translator->trans('mautic.campaign.campaign.searchcommand.isexpired'):
|
||||
case $this->translator->trans('mautic.campaign.campaign.searchcommand.isexpired', [], null, 'en_US'):
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->eq('c.isPublished', ":$unique"),
|
||||
$q->expr()->isNotNull('c.publishDown'),
|
||||
$q->expr()->neq('c.publishDown', $q->expr()->literal('')),
|
||||
$q->expr()->lt('c.publishDown', 'CURRENT_TIMESTAMP()')
|
||||
);
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.campaign.campaign.searchcommand.ispending'):
|
||||
case $this->translator->trans('mautic.campaign.campaign.searchcommand.ispending', [], null, 'en_US'):
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->eq('c.isPublished', ":$unique"),
|
||||
$q->expr()->isNotNull('c.publishUp'),
|
||||
$q->expr()->neq('c.publishUp', $q->expr()->literal('')),
|
||||
$q->expr()->gt('c.publishUp', 'CURRENT_TIMESTAMP()')
|
||||
);
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.project.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US'):
|
||||
return $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'campaign_id',
|
||||
'campaign_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
);
|
||||
}
|
||||
|
||||
if ($expr && $filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
}
|
||||
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
return array_merge([
|
||||
'mautic.campaign.campaign.searchcommand.isexpired',
|
||||
'mautic.campaign.campaign.searchcommand.ispending',
|
||||
'mautic.project.searchcommand.name',
|
||||
], $this->getStandardSearchCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of popular (by logs) campaigns.
|
||||
*
|
||||
* @param int $limit
|
||||
*/
|
||||
public function getPopularCampaigns($limit = 10): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('count(cl.ip_id) as hits, c.id AS campaign_id, c.name')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'cl')
|
||||
->leftJoin('cl', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'cl.campaign_id = c.id')
|
||||
->orderBy('hits', 'DESC')
|
||||
->groupBy('c.id, c.name')
|
||||
->setMaxResults($limit);
|
||||
|
||||
$expr = $this->getPublishedByDateExpression($q, 'c');
|
||||
$q->where($expr);
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
public function getCountsForPendingContacts($campaignId, array $pendingEvents, ContactLimiter $limiter): CountResult
|
||||
{
|
||||
$q = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
|
||||
$q->select('min(cl.lead_id) as min_id, max(cl.lead_id) as max_id, count(cl.lead_id) as the_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('cl.campaign_id', (int) $campaignId),
|
||||
$q->expr()->eq('cl.manually_removed', ':false')
|
||||
)
|
||||
)
|
||||
->setParameter('false', false, 'boolean');
|
||||
|
||||
$this->updateQueryFromContactLimiter('cl', $q, $limiter, true);
|
||||
|
||||
if (count($pendingEvents) > 0) {
|
||||
$sq = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$sq->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e')
|
||||
->where(
|
||||
$sq->expr()->and(
|
||||
$sq->expr()->eq('cl.lead_id', 'e.lead_id'),
|
||||
$sq->expr()->eq('e.rotation', 'cl.rotation'),
|
||||
$sq->expr()->in('e.event_id', $pendingEvents)
|
||||
)
|
||||
);
|
||||
$this->updateQueryFromContactLimiter('e', $sq, $limiter, true);
|
||||
|
||||
$q->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $sq->getSQL())
|
||||
);
|
||||
}
|
||||
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return new CountResult($result['the_count'], $result['min_id'], $result['max_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending contact IDs for a campaign.
|
||||
*/
|
||||
public function getPendingContactIds($campaignId, ContactLimiter $limiter): array
|
||||
{
|
||||
if ($limiter->hasCampaignLimit() && 0 === $limiter->getCampaignLimitRemaining()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$q = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
|
||||
$q->select('cl.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('cl.campaign_id', (int) $campaignId),
|
||||
$q->expr()->eq('cl.manually_removed', ':false')
|
||||
)
|
||||
)
|
||||
->setParameter('false', false, 'boolean')
|
||||
->orderBy('cl.lead_id', 'ASC');
|
||||
|
||||
$this->updateQueryFromContactLimiter('cl', $q, $limiter);
|
||||
|
||||
// Only leads that have not started the campaign
|
||||
$sq = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
$sq->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e')
|
||||
->where(
|
||||
$sq->expr()->and(
|
||||
$sq->expr()->eq('e.lead_id', 'cl.lead_id'),
|
||||
$sq->expr()->eq('e.campaign_id', (int) $campaignId),
|
||||
$sq->expr()->eq('e.rotation', 'cl.rotation')
|
||||
)
|
||||
);
|
||||
|
||||
$q->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $sq->getSQL())
|
||||
);
|
||||
|
||||
if ($limiter->hasCampaignLimit() && $limiter->getCampaignLimitRemaining() < $limiter->getBatchLimit()) {
|
||||
$q->setMaxResults($limiter->getCampaignLimitRemaining());
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$leads = [];
|
||||
foreach ($results as $r) {
|
||||
$leads[] = $r['lead_id'];
|
||||
}
|
||||
unset($results);
|
||||
|
||||
if ($limiter->hasCampaignLimit()) {
|
||||
$limiter->reduceCampaignLimitRemaining(count($leads));
|
||||
}
|
||||
|
||||
return $leads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a count of leads that belong to the campaign.
|
||||
*
|
||||
* @param int $campaignId
|
||||
* @param int $leadId Optional lead ID to check if lead is part of campaign
|
||||
* @param array $pendingEvents List of specific events to rule out
|
||||
*
|
||||
* @throws \Doctrine\DBAL\Cache\CacheException
|
||||
*/
|
||||
public function getCampaignLeadCount($campaignId, $leadId = null, $pendingEvents = [], ?\DateTimeInterface $dateFrom = null, ?\DateTimeInterface $dateTo = null): int
|
||||
{
|
||||
$q = $this->getReplicaConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('count(cl.lead_id) as lead_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('cl.campaign_id', (int) $campaignId),
|
||||
$q->expr()->eq('cl.manually_removed', ':false')
|
||||
)
|
||||
)
|
||||
->setParameter('false', false, Types::BOOLEAN);
|
||||
|
||||
if ($leadId) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('cl.lead_id', (int) $leadId)
|
||||
);
|
||||
}
|
||||
|
||||
if ($dateFrom && $dateTo) {
|
||||
$q->andWhere('cl.date_added BETWEEN FROM_UNIXTIME(:dateFrom) AND FROM_UNIXTIME(:dateTo)')
|
||||
->setParameter('dateFrom', $dateFrom->getTimestamp(), \PDO::PARAM_INT)
|
||||
->setParameter('dateTo', $dateTo->getTimestamp(), \PDO::PARAM_INT);
|
||||
}
|
||||
|
||||
if (count($pendingEvents) > 0) {
|
||||
$sq = $this->getReplicaConnection()->createQueryBuilder();
|
||||
$sq->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e')
|
||||
->where(
|
||||
$sq->expr()->and(
|
||||
$sq->expr()->eq('cl.lead_id', 'e.lead_id'),
|
||||
$sq->expr()->in('e.event_id', $pendingEvents)
|
||||
)
|
||||
);
|
||||
|
||||
if ($dateFrom && $dateTo) {
|
||||
$sq->andWhere('cl.date_triggered BETWEEN FROM_UNIXTIME(:dateFrom) AND FROM_UNIXTIME(:dateTo)')
|
||||
->setParameter('dateFrom', $dateFrom->getTimestamp(), \PDO::PARAM_INT)
|
||||
->setParameter('dateTo', $dateTo->getTimestamp(), \PDO::PARAM_INT);
|
||||
}
|
||||
|
||||
$q->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $sq->getSQL())
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->getReplicaConnection()->getConfiguration()->getResultCache()) {
|
||||
$results = $this->getReplicaConnection()->executeCacheQuery(
|
||||
$q->getSQL(),
|
||||
$q->getParameters(),
|
||||
$q->getParameterTypes(),
|
||||
new QueryCacheProfile(600)
|
||||
)->fetchAllAssociative();
|
||||
} else {
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
return (int) $results[0]['lead_count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lead data of a campaign.
|
||||
*
|
||||
* @param int $start
|
||||
* @param bool|false $limit
|
||||
* @param array $select
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getCampaignLeads($campaignId, $start = 0, $limit = false, $select = ['cl.lead_id']): array
|
||||
{
|
||||
$q = $this->getReplicaConnection()->createQueryBuilder();
|
||||
|
||||
$q->select($select)
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('cl.campaign_id', (int) $campaignId),
|
||||
$q->expr()->eq('cl.manually_removed', ':false')
|
||||
)
|
||||
)
|
||||
->setParameter('false', false, 'boolean')
|
||||
->orderBy('cl.lead_id', 'ASC');
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getContactSingleSegmentByCampaign($contactId, $campaignId)
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
return $q->select('ll.id, ll.name')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists', 'll')
|
||||
->join('ll', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll', 'lll.leadlist_id = ll.id and lll.lead_id = :contactId and lll.manually_removed = 0')
|
||||
->join('ll', MAUTIC_TABLE_PREFIX.'campaign_leadlist_xref', 'clx', 'clx.leadlist_id = ll.id and clx.campaign_id = :campaignId')
|
||||
->setParameter('contactId', (int) $contactId)
|
||||
->setParameter('campaignId', (int) $campaignId)
|
||||
->setMaxResults(1)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $segmentId
|
||||
* @param array $campaignIds
|
||||
*/
|
||||
public function getCampaignsSegmentShare($segmentId, $campaignIds = []): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->select('c.id, c.name, ROUND(IFNULL(COUNT(DISTINCT t.lead_id)/COUNT(DISTINCT cl.lead_id)*100, 0),1) segmentCampaignShare');
|
||||
$q->from(MAUTIC_TABLE_PREFIX.'campaigns', 'c')
|
||||
->leftJoin('c', MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl', 'cl.campaign_id = c.id AND cl.manually_removed = 0')
|
||||
->leftJoin('cl',
|
||||
'(SELECT lll.lead_id AS ll, lll.lead_id FROM '.MAUTIC_TABLE_PREFIX.'lead_lists_leads lll WHERE lll.leadlist_id = '.$segmentId
|
||||
.' AND lll.manually_removed = 0)',
|
||||
't',
|
||||
't.lead_id = cl.lead_id'
|
||||
);
|
||||
$q->groupBy('c.id');
|
||||
|
||||
if (!empty($campaignIds)) {
|
||||
$q->where($q->expr()->in('c.id', $campaignIds));
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for emails assigned to campaign and returns associative array of email ids in format:.
|
||||
*
|
||||
* array (size=1)
|
||||
* 0 =>
|
||||
* array (size=2)
|
||||
* 'channelId' => int 18
|
||||
*
|
||||
* or empty array if nothing found.
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function fetchEmailIdsById($id): array
|
||||
{
|
||||
$emails = $this->getEntityManager()
|
||||
->createQueryBuilder()
|
||||
->select('e.channelId')
|
||||
->from(Campaign::class, $this->getTableAlias(), $this->getTableAlias().'.id')
|
||||
->leftJoin(
|
||||
$this->getTableAlias().'.events',
|
||||
'e',
|
||||
Expr\Join::WITH,
|
||||
"e.channel = '".Event::CHANNEL_EMAIL."'"
|
||||
)
|
||||
->where($this->getTableAlias().'.id = :id')
|
||||
->setParameter('id', $id)
|
||||
->andWhere('e.channelId IS NOT NULL')
|
||||
->getQuery()
|
||||
->setHydrationMode(\Doctrine\ORM\Query::HYDRATE_ARRAY)
|
||||
->getResult();
|
||||
|
||||
$return = [];
|
||||
foreach ($emails as $email) {
|
||||
// Every channelId represents e-mail ID
|
||||
$return[] = $email['channelId']; // mautic_campaign_events.channel_id
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getCampaignIdsWithDependenciesOnEmail(int $emailId): array
|
||||
{
|
||||
$query = $this->getEntityManager()
|
||||
->createQueryBuilder()
|
||||
->select($this->getTableAlias().'.id')
|
||||
->distinct()
|
||||
->from(Campaign::class, $this->getTableAlias(), $this->getTableAlias().'.id')
|
||||
->leftJoin(
|
||||
$this->getTableAlias().'.events',
|
||||
'e',
|
||||
Expr\Join::WITH,
|
||||
"e.channel = '".Event::CHANNEL_EMAIL."'"
|
||||
)
|
||||
->where('e.channelId = :emailId')
|
||||
->setParameter('emailId', $emailId)
|
||||
->getQuery();
|
||||
|
||||
return array_unique(array_map(fn ($val): int => (int) $val, $query->getSingleColumnResult()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getCampaignPublishAndVersionData(int $campaignId): array
|
||||
{
|
||||
$result = $this->getEntityManager()->getConnection()
|
||||
->executeQuery(
|
||||
'SELECT is_published, version FROM '.MAUTIC_TABLE_PREFIX.'campaigns WHERE id = ? FOR UPDATE',
|
||||
[$campaignId],
|
||||
[\PDO::PARAM_INT]
|
||||
)->fetchAssociative();
|
||||
|
||||
return $result ?: [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
interface ChannelInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getChannel();
|
||||
|
||||
public function setChannel($channel): void;
|
||||
|
||||
/**
|
||||
* @return int|string
|
||||
*/
|
||||
public function getChannelId();
|
||||
|
||||
public function setChannelId($id): void;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilder;
|
||||
use Doctrine\ORM\QueryBuilder as OrmQueryBuilder;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
|
||||
trait ContactLimiterTrait
|
||||
{
|
||||
/**
|
||||
* @param string $alias
|
||||
* @param bool $isCount
|
||||
*/
|
||||
private function updateQueryFromContactLimiter($alias, DbalQueryBuilder $qb, ContactLimiter $contactLimiter, $isCount = false): void
|
||||
{
|
||||
$minContactId = $contactLimiter->getMinContactId();
|
||||
$maxContactId = $contactLimiter->getMaxContactId();
|
||||
if ($contactId = $contactLimiter->getContactId()) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->eq("$alias.lead_id", ':contactId')
|
||||
)
|
||||
->setParameter('contactId', $contactId, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
} elseif ($contactIds = $contactLimiter->getContactIdList()) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->in("$alias.lead_id", ':contactIds')
|
||||
)
|
||||
->setParameter('contactIds', $contactIds, ArrayParameterType::INTEGER);
|
||||
} elseif ($minContactId && $maxContactId) {
|
||||
$qb->andWhere(
|
||||
"$alias.lead_id BETWEEN :minContactId AND :maxContactId"
|
||||
)
|
||||
->setParameter('minContactId', $minContactId, \Doctrine\DBAL\ParameterType::INTEGER)
|
||||
->setParameter('maxContactId', $maxContactId, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
} elseif ($minContactId) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->gte("$alias.lead_id", ':minContactId')
|
||||
)
|
||||
->setParameter('minContactId', $minContactId, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
} elseif ($maxContactId) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->lte("$alias.lead_id", ':maxContactId')
|
||||
)
|
||||
->setParameter('maxContactId', $maxContactId, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
if ($threadId = $contactLimiter->getThreadId()) {
|
||||
if ($maxThreads = $contactLimiter->getMaxThreads()) {
|
||||
if ($threadId <= $maxThreads) {
|
||||
$qb->andWhere("MOD(($alias.lead_id + :threadShift), :maxThreads) = 0")
|
||||
->setParameter('threadShift', $threadId - 1, \Doctrine\DBAL\ParameterType::INTEGER)
|
||||
->setParameter('maxThreads', $maxThreads, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isCount && $limit = $contactLimiter->getBatchLimit()) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $alias
|
||||
* @param bool $isCount
|
||||
*/
|
||||
private function updateOrmQueryFromContactLimiter($alias, OrmQueryBuilder $qb, ContactLimiter $contactLimiter, $isCount = false): void
|
||||
{
|
||||
$minContactId = $contactLimiter->getMinContactId();
|
||||
$maxContactId = $contactLimiter->getMaxContactId();
|
||||
if ($contactId = $contactLimiter->getContactId()) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->eq("IDENTITY($alias.lead)", ':contact')
|
||||
)
|
||||
->setParameter('contact', $contactId, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
} elseif ($contactIds = $contactLimiter->getContactIdList()) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->in("IDENTITY($alias.lead)", ':contactIds')
|
||||
)
|
||||
->setParameter('contactIds', $contactIds, ArrayParameterType::INTEGER);
|
||||
} elseif ($minContactId && $maxContactId) {
|
||||
$qb->andWhere(
|
||||
"IDENTITY($alias.lead) BETWEEN :minContactId AND :maxContactId"
|
||||
)
|
||||
->setParameter('minContactId', $minContactId, \Doctrine\DBAL\ParameterType::INTEGER)
|
||||
->setParameter('maxContactId', $maxContactId, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
} elseif ($minContactId) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->gte("IDENTITY($alias.lead)", ':minContactId')
|
||||
)
|
||||
->setParameter('minContactId', $minContactId, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
} elseif ($maxContactId) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->lte("IDENTITY($alias.lead)", ':maxContactId')
|
||||
)
|
||||
->setParameter('maxContactId', $maxContactId, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
if ($threadId = $contactLimiter->getThreadId()) {
|
||||
if ($maxThreads = $contactLimiter->getMaxThreads()) {
|
||||
$qb->andWhere("MOD((IDENTITY($alias.lead) + :threadShift), :maxThreads) = 0")
|
||||
->setParameter('threadShift', $threadId - 1, \Doctrine\DBAL\ParameterType::INTEGER)
|
||||
->setParameter('maxThreads', $maxThreads, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isCount && $limit = $contactLimiter->getBatchLimit()) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,437 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Event>
|
||||
*/
|
||||
class EventRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Get a list of entities.
|
||||
*
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return \Doctrine\ORM\Tools\Pagination\Paginator<object>|object[]|mixed[]
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$select = 'e';
|
||||
$q = $this
|
||||
->createQueryBuilder('e')
|
||||
->join('e.campaign', 'c');
|
||||
|
||||
if (!empty($args['campaign_id'])) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('IDENTITY(e.campaign)', (int) $args['campaign_id'])
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($args['ignore_children'])) {
|
||||
$select .= ', ec, ep';
|
||||
$q->leftJoin('e.children', 'ec')
|
||||
->leftJoin('e.parent', 'ep');
|
||||
}
|
||||
|
||||
$q->select($select);
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $contactId
|
||||
* @param string $type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getContactPendingEvents($contactId, $type)
|
||||
{
|
||||
// Limit to events that hasn't been executed or scheduled yet
|
||||
$eventQb = $this->getEntityManager()->createQueryBuilder();
|
||||
$eventQb->select('IDENTITY(log_event.event)')
|
||||
->from(LeadEventLog::class, 'log_event')
|
||||
->where(
|
||||
$eventQb->expr()->andX(
|
||||
$eventQb->expr()->eq('log_event.event', 'e'),
|
||||
$eventQb->expr()->eq('log_event.lead', 'l.lead'),
|
||||
$eventQb->expr()->eq('log_event.rotation', 'l.rotation')
|
||||
)
|
||||
);
|
||||
|
||||
// Limit to events that has no parent or whose parent has already been executed
|
||||
$parentQb = $this->getEntityManager()->createQueryBuilder();
|
||||
$parentQb->select('parent_log_event.id')
|
||||
->from(LeadEventLog::class, 'parent_log_event')
|
||||
->where(
|
||||
$parentQb->expr()->eq('parent_log_event.event', 'e.parent'),
|
||||
$parentQb->expr()->eq('parent_log_event.lead', 'l.lead'),
|
||||
$parentQb->expr()->eq('parent_log_event.rotation', 'l.rotation'),
|
||||
$parentQb->expr()->eq('parent_log_event.isScheduled', 0)
|
||||
);
|
||||
|
||||
$q = $this->createQueryBuilder('e', 'e.id');
|
||||
$q->select('e,c')
|
||||
->innerJoin('e.campaign', 'c')
|
||||
->innerJoin('c.leads', 'l')
|
||||
->where(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->eq('c.isPublished', 1),
|
||||
$q->expr()->orX(
|
||||
$q->expr()->isNull('c.publishUp'),
|
||||
$q->expr()->lt('c.publishUp', 'CURRENT_TIMESTAMP()'),
|
||||
),
|
||||
$q->expr()->orX(
|
||||
$q->expr()->isNull('c.publishDown'),
|
||||
$q->expr()->gt('c.publishDown', 'CURRENT_TIMESTAMP()'),
|
||||
),
|
||||
$q->expr()->isNull('c.deleted'),
|
||||
$q->expr()->eq('e.type', ':type'),
|
||||
$q->expr()->isNull('e.deleted'),
|
||||
$q->expr()->eq('IDENTITY(l.lead)', ':contactId'),
|
||||
$q->expr()->eq('l.manuallyRemoved', 0),
|
||||
$q->expr()->notIn('e.id', $eventQb->getDQL()),
|
||||
$q->expr()->orX(
|
||||
$q->expr()->isNull('e.parent'),
|
||||
$q->expr()->exists($parentQb->getDQL())
|
||||
)
|
||||
)
|
||||
)
|
||||
->setParameter('type', $type)
|
||||
->setParameter('contactId', (int) $contactId);
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of events by parent.
|
||||
*
|
||||
* @param int $parentId
|
||||
* @param string|null $decisionPath
|
||||
* @param string|null $eventType
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEventsByParent($parentId, $decisionPath = null, $eventType = null)
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$q->select('e')
|
||||
->from(Event::class, 'e', 'e.id')
|
||||
->where(
|
||||
$q->expr()->eq('IDENTITY(e.parent)', (int) $parentId)
|
||||
);
|
||||
|
||||
if (null !== $decisionPath) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('e.decisionPath', ':decisionPath')
|
||||
)
|
||||
->setParameter('decisionPath', $decisionPath);
|
||||
}
|
||||
|
||||
if (null !== $eventType) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('e.eventType', ':eventType')
|
||||
)
|
||||
->setParameter('eventType', $eventType);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $campaignId
|
||||
* @param bool $ignoreDeleted
|
||||
*
|
||||
* @return array<int,mixed[]>
|
||||
*/
|
||||
public function getCampaignEvents($campaignId, $ignoreDeleted = true): array
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder();
|
||||
$q->select('e, IDENTITY(e.parent)')
|
||||
->from(Event::class, 'e', 'e.id')
|
||||
->where(
|
||||
$q->expr()->eq('IDENTITY(e.campaign)', (int) $campaignId)
|
||||
)
|
||||
->orderBy('e.order', Order::Ascending->value);
|
||||
|
||||
if ($ignoreDeleted) {
|
||||
$q->andWhere($q->expr()->isNull('e.deleted'));
|
||||
}
|
||||
|
||||
$results = $q->getQuery()->getArrayResult();
|
||||
|
||||
// Fix the parent ID
|
||||
$events = [];
|
||||
foreach ($results as $id => $r) {
|
||||
$r[0]['parent_id'] = $r[1];
|
||||
$events[$id] = $r[0];
|
||||
}
|
||||
unset($results);
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getCampaignEventIds(int $campaignId): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->select('e.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.Event::TABLE_NAME, 'e')
|
||||
->where($q->expr()->eq('e.campaign_id', $campaignId));
|
||||
|
||||
return array_column($q->executeQuery()->fetchAllAssociative(), 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of events with stats.
|
||||
*
|
||||
* @param array $args
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEvents($args = [])
|
||||
{
|
||||
$q = $this->createQueryBuilder('e')
|
||||
->select('e, ec, ep')
|
||||
->join('e.campaign', 'c')
|
||||
->leftJoin('e.children', 'ec')
|
||||
->leftJoin('e.parent', 'ep')
|
||||
->orderBy('e.order');
|
||||
|
||||
if (!empty($args['campaigns'])) {
|
||||
$q->andWhere($q->expr()->in('e.campaign', ':campaigns'))
|
||||
->setParameter('campaigns', $args['campaigns']);
|
||||
}
|
||||
|
||||
if (isset($args['positivePathOnly'])) {
|
||||
$q->andWhere(
|
||||
$q->expr()->orX(
|
||||
$q->expr()->neq(
|
||||
'e.decisionPath',
|
||||
$q->expr()->literal('no')
|
||||
),
|
||||
$q->expr()->isNull('e.decisionPath')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Null event parents in preparation for deleI'lting a campaign.
|
||||
*
|
||||
* @param int $campaignId
|
||||
*/
|
||||
public function nullEventParents($campaignId): void
|
||||
{
|
||||
$this->getEntityManager()->getConnection()->update(
|
||||
MAUTIC_TABLE_PREFIX.'campaign_events',
|
||||
['parent_id' => null],
|
||||
['campaign_id' => (int) $campaignId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Null event parents in preparation for deleting events from a campaign.
|
||||
*
|
||||
* @param string[] $events
|
||||
*/
|
||||
public function nullEventRelationships($events): void
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->update(MAUTIC_TABLE_PREFIX.'campaign_events')
|
||||
->set('parent_id', ':null')
|
||||
->setParameter('null', null)
|
||||
->where(
|
||||
$qb->expr()->in('parent_id', $events)
|
||||
)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $eventIds
|
||||
*/
|
||||
public function deleteEvents(array $eventIds): void
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->delete(Event::class, 'e')
|
||||
->where($qb->expr()->in('e.id', ':event_ids'))
|
||||
->setParameter('event_ids', $eventIds, ArrayParameterType::INTEGER)
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $eventIds
|
||||
*/
|
||||
public function setEventsAsDeleted(array $eventIds): void
|
||||
{
|
||||
$dateTime = (new \DateTime())->format('Y-m-d H:i:s');
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->update(MAUTIC_TABLE_PREFIX.Event::TABLE_NAME)
|
||||
->set('deleted', ':deleted')
|
||||
->setParameter('deleted', $dateTime)
|
||||
->where(
|
||||
$qb->expr()->in('id', $eventIds)
|
||||
)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'e';
|
||||
}
|
||||
|
||||
/**
|
||||
* For the API.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
return $this->getStandardSearchCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of events that have been triggered by this lead.
|
||||
*/
|
||||
public function getLeadTriggeredEvents($leadId): array
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->select('e, c, l')
|
||||
->from(Event::class, 'e')
|
||||
->join('e.campaign', 'c')
|
||||
->join('e.log', 'l');
|
||||
|
||||
// make sure the published up and down dates are good
|
||||
$q->where($q->expr()->eq('IDENTITY(l.lead)', (int) $leadId));
|
||||
|
||||
$results = $q->getQuery()->getArrayResult();
|
||||
|
||||
$return = [];
|
||||
foreach ($results as $r) {
|
||||
$return[$r['id']] = $r;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* For the API
|
||||
*/
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
[
|
||||
$this->getTableAlias().'.name',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* For the API
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardSearchCommandWhereClause($q, $filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the failed count using DBAL to avoid
|
||||
* race conditions and deadlocks.
|
||||
*/
|
||||
public function incrementFailedCount(Event $event): int
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'campaign_events')
|
||||
->set('failed_count', 'failed_count + 1')
|
||||
->where($q->expr()->eq('id', ':id'))
|
||||
->setParameter('id', $event->getId());
|
||||
|
||||
$q->executeStatement();
|
||||
|
||||
return $this->getFailedCount($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the failed count using DBAL to avoid
|
||||
* race conditions and deadlocks.
|
||||
*/
|
||||
public function decreaseFailedCount(Event $event): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'campaign_events')
|
||||
->set('failed_count', 'failed_count - 1')
|
||||
->where($q->expr()->eq('id', ':id'))
|
||||
->andWhere($q->expr()->gt('failed_count', 0))
|
||||
->setParameter('id', $event->getId());
|
||||
|
||||
$q->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the up to date failed count
|
||||
* for the given Event.
|
||||
*/
|
||||
public function getFailedCount(Event $event): int
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('failed_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_events')
|
||||
->where($q->expr()->eq('id', ':id'))
|
||||
->setParameter('id', $event->getId());
|
||||
|
||||
return (int) $q->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the failed_count's for all events
|
||||
* within the given Campaign.
|
||||
*/
|
||||
public function resetFailedCountsForEventsInCampaign(Campaign $campaign): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'campaign_events')
|
||||
->set('failed_count', ':failedCount')
|
||||
->where($q->expr()->eq('campaign_id', ':campaignId'))
|
||||
->setParameter('failedCount', 0)
|
||||
->setParameter('campaignId', $campaign->getId());
|
||||
|
||||
$q->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of failed event for Lead/Event.
|
||||
*/
|
||||
public function getFailedCountLeadEvent(int $leadId, int $eventId): int
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('count(le.id)')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'le')
|
||||
->innerJoin('le', MAUTIC_TABLE_PREFIX.'campaign_lead_event_failed_log', 'fle', 'le.id = fle.log_id')
|
||||
->where('le.lead_id = :leadId')
|
||||
->andWhere('le.event_id = :eventId')
|
||||
->setParameters(['leadId' => $leadId, 'eventId' => $eventId]);
|
||||
|
||||
return (int) $q->executeQuery()->fetchOne();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class FailedLeadEventLog
|
||||
{
|
||||
/**
|
||||
* @var LeadEventLog
|
||||
*/
|
||||
private $log;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $reason;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('campaign_lead_event_failed_log')
|
||||
->setCustomRepositoryClass(FailedLeadEventLogRepository::class)
|
||||
->addIndex(['date_added'], 'campaign_event_failed_date');
|
||||
|
||||
$builder->createOneToOne('log', 'LeadEventLog')
|
||||
->makePrimaryKey()
|
||||
->inversedBy('failedLog')
|
||||
->addJoinColumn('log_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->addNullableField('reason', 'text');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('campaignEventFailedLog')
|
||||
->addProperties(
|
||||
[
|
||||
'dateAdded',
|
||||
'reason',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function getLog()
|
||||
{
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FailedLeadEventLog
|
||||
*/
|
||||
public function setLog(?LeadEventLog $log = null)
|
||||
{
|
||||
$this->log = $log;
|
||||
|
||||
if ($log) {
|
||||
$log->setFailedLog($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FailedLeadEventLog
|
||||
*/
|
||||
public function setDateAdded(?\DateTime $dateAdded = null)
|
||||
{
|
||||
if (null === $dateAdded) {
|
||||
$dateAdded = new \DateTime();
|
||||
}
|
||||
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getReason()
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $reason
|
||||
*
|
||||
* @return FailedLeadEventLog
|
||||
*/
|
||||
public function setReason($reason)
|
||||
{
|
||||
$this->reason = $reason;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<FailedLeadEventLog>
|
||||
*/
|
||||
class FailedLeadEventLogRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @param array<string|int> $ids
|
||||
*/
|
||||
public function deleteByIds(array $ids): void
|
||||
{
|
||||
if (!$ids) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->_em->getConnection()
|
||||
->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'campaign_lead_event_failed_log')
|
||||
->where('log_id IN (:ids)')
|
||||
->setParameter('ids', $ids, ArrayParameterType::STRING)
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class Lead
|
||||
{
|
||||
/**
|
||||
* @var Campaign
|
||||
*/
|
||||
private $campaign;
|
||||
|
||||
/**
|
||||
* @var \Mautic\LeadBundle\Entity\Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
**/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateLastExited;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $manuallyRemoved = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $manuallyAdded = false;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $rotation = 1;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('campaign_leads')
|
||||
->setCustomRepositoryClass(LeadRepository::class)
|
||||
->addIndex(['date_added'], 'campaign_leads_date_added')
|
||||
->addIndex(['date_last_exited'], 'campaign_leads_date_exited')
|
||||
->addIndex(['campaign_id', 'manually_removed', 'lead_id', 'rotation'], 'campaign_leads');
|
||||
|
||||
$builder->createManyToOne('campaign', 'Campaign')
|
||||
->makePrimaryKey()
|
||||
->inversedBy('leads')
|
||||
->addJoinColumn('campaign_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', true);
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('manuallyRemoved', 'boolean')
|
||||
->columnName('manually_removed')
|
||||
->build();
|
||||
|
||||
$builder->createField('manuallyAdded', 'boolean')
|
||||
->columnName('manually_added')
|
||||
->build();
|
||||
|
||||
$builder->addNamedField('dateLastExited', 'datetime', 'date_last_exited', true);
|
||||
|
||||
$builder->addField('rotation', 'integer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('campaignLead')
|
||||
->addListProperties(
|
||||
[
|
||||
'dateAdded',
|
||||
'manuallyRemoved',
|
||||
'manuallyAdded',
|
||||
'rotation',
|
||||
'dateLastExited',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'lead',
|
||||
'campaign',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
*/
|
||||
public function setDateAdded($date): void
|
||||
{
|
||||
$this->dateAdded = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\LeadBundle\Entity\Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
public function setLead(\Mautic\LeadBundle\Entity\Lead $lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Campaign
|
||||
*/
|
||||
public function getCampaign()
|
||||
{
|
||||
return $this->campaign;
|
||||
}
|
||||
|
||||
public function setCampaign(Campaign $campaign): void
|
||||
{
|
||||
$this->campaign = $campaign;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getManuallyRemoved()
|
||||
{
|
||||
return $this->manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $manuallyRemoved
|
||||
*/
|
||||
public function setManuallyRemoved($manuallyRemoved): void
|
||||
{
|
||||
$this->manuallyRemoved = $manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasManuallyRemoved()
|
||||
{
|
||||
return $this->manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getManuallyAdded()
|
||||
{
|
||||
return $this->manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $manuallyAdded
|
||||
*/
|
||||
public function setManuallyAdded($manuallyAdded): void
|
||||
{
|
||||
$this->manuallyAdded = $manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasManuallyAdded()
|
||||
{
|
||||
return $this->manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getRotation()
|
||||
{
|
||||
return $this->rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $rotation
|
||||
*
|
||||
* @return Lead
|
||||
*/
|
||||
public function setRotation($rotation)
|
||||
{
|
||||
$this->rotation = (int) $rotation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function startNewRotation()
|
||||
{
|
||||
++$this->rotation;
|
||||
$this->dateAdded = new \DateTime();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getDateLastExited()
|
||||
{
|
||||
return $this->dateLastExited;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function setDateLastExited(?\DateTime $dateLastExited = null)
|
||||
{
|
||||
$this->dateLastExited = $dateLastExited;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Entity\OptimisticLockInterface;
|
||||
use Mautic\CoreBundle\Entity\OptimisticLockTrait;
|
||||
use Mautic\LeadBundle\Entity\Lead as LeadEntity;
|
||||
|
||||
class LeadEventLog implements ChannelInterface, OptimisticLockInterface
|
||||
{
|
||||
use OptimisticLockTrait;
|
||||
|
||||
public const TABLE_NAME = 'campaign_lead_event_log';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Event
|
||||
*/
|
||||
private $event;
|
||||
|
||||
/**
|
||||
* @var LeadEntity
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var Campaign|null
|
||||
*/
|
||||
private $campaign;
|
||||
|
||||
/**
|
||||
* @var IpAddress|null
|
||||
*/
|
||||
private $ipAddress;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
**/
|
||||
private $dateTriggered;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isScheduled = false;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
private $triggerDate;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $systemTriggered = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $metadata = [];
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private $nonActionPathTaken = false;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $channel;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $channelId;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private $previousScheduledState;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $rotation = 1;
|
||||
|
||||
/**
|
||||
* @var FailedLeadEventLog|null
|
||||
*/
|
||||
private $failedLog;
|
||||
|
||||
/**
|
||||
* Subscribers can fail log with custom reschedule interval.
|
||||
*
|
||||
* @var \DateInterval|null
|
||||
*/
|
||||
private $rescheduleInterval;
|
||||
|
||||
private ?\DateTime $dateQueued = null;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(LeadEventLogRepository::class)
|
||||
->addIndex(['is_scheduled', 'lead_id'], 'campaign_event_upcoming_search')
|
||||
->addIndex(['campaign_id', 'is_scheduled', 'trigger_date'], 'campaign_event_schedule_counts')
|
||||
->addIndex(['date_triggered'], 'campaign_date_triggered')
|
||||
->addIndex(['lead_id', 'campaign_id', 'rotation'], 'campaign_leads')
|
||||
->addIndex(['channel', 'channel_id', 'lead_id'], 'campaign_log_channel')
|
||||
->addIndex(['campaign_id', 'event_id', 'date_triggered'], 'campaign_actions')
|
||||
->addIndex(['campaign_id', 'date_triggered', 'event_id', 'non_action_path_taken'], 'campaign_stats')
|
||||
->addIndex(['trigger_date'], 'campaign_trigger_date_order')
|
||||
->addIndex(['is_scheduled', 'event_id', 'trigger_date'], 'idx_scheduled_events')
|
||||
->addUniqueConstraint(['event_id', 'lead_id', 'rotation'], 'campaign_rotation');
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->createManyToOne('event', 'Event')
|
||||
->inversedBy('log')
|
||||
->addJoinColumn('event_id', 'id', false, false)
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE');
|
||||
|
||||
$builder->addField('rotation', 'integer');
|
||||
|
||||
$builder->createManyToOne('campaign', 'Campaign')
|
||||
->addJoinColumn('campaign_id', 'id')
|
||||
->build();
|
||||
|
||||
$builder->addIpAddress(true);
|
||||
|
||||
$builder->createField('dateTriggered', 'datetime')
|
||||
->columnName('date_triggered')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('isScheduled', 'boolean')
|
||||
->columnName('is_scheduled')
|
||||
->build();
|
||||
|
||||
$builder->createField('triggerDate', 'datetime')
|
||||
->columnName('trigger_date')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('systemTriggered', 'boolean')
|
||||
->columnName('system_triggered')
|
||||
->build();
|
||||
|
||||
$builder->createField('metadata', 'array')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('channel', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addNamedField('channelId', 'integer', 'channel_id', true);
|
||||
|
||||
$builder->addNullableField('nonActionPathTaken', 'boolean', 'non_action_path_taken');
|
||||
|
||||
$builder->createOneToOne('failedLog', 'FailedLeadEventLog')
|
||||
->mappedBy('log')
|
||||
->fetchExtraLazy()
|
||||
->cascadeAll()
|
||||
->build();
|
||||
|
||||
$builder->createField('dateQueued', Types::DATETIME_MUTABLE)
|
||||
->columnName('date_queued')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
self::addVersionField($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('campaignEventLog')
|
||||
->addProperties(
|
||||
[
|
||||
'ipAddress',
|
||||
'dateTriggered',
|
||||
'isScheduled',
|
||||
'triggerDate',
|
||||
'metadata',
|
||||
'nonActionPathTaken',
|
||||
'channel',
|
||||
'channelId',
|
||||
'rotation',
|
||||
]
|
||||
)
|
||||
|
||||
// Add standalone groups
|
||||
->setGroupPrefix('campaignEventStandaloneLog')
|
||||
->addProperties(
|
||||
[
|
||||
'event',
|
||||
'lead',
|
||||
'campaign',
|
||||
'ipAddress',
|
||||
'dateTriggered',
|
||||
'isScheduled',
|
||||
'triggerDate',
|
||||
'metadata',
|
||||
'nonActionPathTaken',
|
||||
'channel',
|
||||
'channelId',
|
||||
'rotation',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getDateTriggered()
|
||||
{
|
||||
return $this->dateTriggered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setDateTriggered(?\DateTimeInterface $dateTriggered = null)
|
||||
{
|
||||
$this->dateTriggered = $dateTriggered;
|
||||
if (null !== $dateTriggered) {
|
||||
$this->setIsScheduled(false);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpAddress|null
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setIpAddress(IpAddress $ipAddress)
|
||||
{
|
||||
$this->ipAddress = $ipAddress;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadEntity|null
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLead(LeadEntity $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Event|null
|
||||
*/
|
||||
public function getEvent()
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
/***
|
||||
* @param $event
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setEvent(Event $event)
|
||||
{
|
||||
$this->event = $event;
|
||||
|
||||
if (!$this->campaign) {
|
||||
$this->setCampaign($event->getCampaign());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsScheduled()
|
||||
{
|
||||
return $this->isScheduled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isScheduled
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setIsScheduled($isScheduled)
|
||||
{
|
||||
if (null === $this->previousScheduledState) {
|
||||
$this->previousScheduledState = $this->isScheduled;
|
||||
}
|
||||
|
||||
$this->isScheduled = $isScheduled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If isScheduled was changed, this will have the previous state.
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
public function getPreviousScheduledState()
|
||||
{
|
||||
return $this->previousScheduledState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getTriggerDate()
|
||||
{
|
||||
return $this->triggerDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setTriggerDate(?\DateTimeInterface $triggerDate = null)
|
||||
{
|
||||
$this->triggerDate = $triggerDate;
|
||||
$this->setIsScheduled(true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Campaign|null
|
||||
*/
|
||||
public function getCampaign()
|
||||
{
|
||||
return $this->campaign;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCampaign(Campaign $campaign)
|
||||
{
|
||||
$this->campaign = $campaign;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getSystemTriggered()
|
||||
{
|
||||
return $this->systemTriggered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $systemTriggered
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSystemTriggered($systemTriggered)
|
||||
{
|
||||
$this->systemTriggered = $systemTriggered;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getNonActionPathTaken()
|
||||
{
|
||||
return $this->nonActionPathTaken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $nonActionPathTaken
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setNonActionPathTaken($nonActionPathTaken)
|
||||
{
|
||||
$this->nonActionPathTaken = $nonActionPathTaken;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]|null
|
||||
*/
|
||||
public function getMetadata()
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $metadata
|
||||
*/
|
||||
public function appendToMetadata($metadata): void
|
||||
{
|
||||
if (!is_array($metadata)) {
|
||||
// Assumed output for timeline BC for <2.14
|
||||
$metadata = ['timeline' => $metadata];
|
||||
}
|
||||
|
||||
$this->metadata = array_merge($this->metadata, $metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $metadata
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setMetadata($metadata)
|
||||
{
|
||||
if (!is_array($metadata)) {
|
||||
// Assumed output for timeline
|
||||
$metadata = ['timeline' => $metadata];
|
||||
}
|
||||
|
||||
$this->metadata = $metadata;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*/
|
||||
public function setChannel($channel): void
|
||||
{
|
||||
$this->channel = $channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getChannelId()
|
||||
{
|
||||
return $this->channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $channelId
|
||||
*/
|
||||
public function setChannelId($channelId): void
|
||||
{
|
||||
$this->channelId = $channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getRotation()
|
||||
{
|
||||
return $this->rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $rotation
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setRotation($rotation)
|
||||
{
|
||||
$this->rotation = (int) $rotation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FailedLeadEventLog|null
|
||||
*/
|
||||
public function getFailedLog()
|
||||
{
|
||||
return $this->failedLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setFailedLog(?FailedLeadEventLog $log = null)
|
||||
{
|
||||
$this->failedLog = $log;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isFailed(): bool
|
||||
{
|
||||
$log = $this->getFailedLog();
|
||||
|
||||
return !empty($log);
|
||||
}
|
||||
|
||||
public function isSuccess(): bool
|
||||
{
|
||||
return !$this->isFailed();
|
||||
}
|
||||
|
||||
public function setRescheduleInterval(?\DateInterval $interval): void
|
||||
{
|
||||
$this->rescheduleInterval = $interval;
|
||||
}
|
||||
|
||||
public function getRescheduleInterval(): ?\DateInterval
|
||||
{
|
||||
return $this->rescheduleInterval;
|
||||
}
|
||||
|
||||
public function getDateQueued(): ?\DateTime
|
||||
{
|
||||
return $this->dateQueued;
|
||||
}
|
||||
|
||||
public function setDateQueued(?\DateTime $dateQueued): LeadEventLog
|
||||
{
|
||||
$this->dateQueued = $dateQueued;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Cache\QueryCacheProfile;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\LeadBundle\Entity\TimelineTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadEventLog>
|
||||
*/
|
||||
class LeadEventLogRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
use ContactLimiterTrait;
|
||||
use ReplicaConnectionTrait;
|
||||
public const LOG_DELETE_BATCH_SIZE = 5000;
|
||||
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$alias = $this->getTableAlias();
|
||||
$q = $this
|
||||
->createQueryBuilder($alias)
|
||||
->join($alias.'.ipAddress', 'i');
|
||||
|
||||
if (empty($args['campaign_id'])) {
|
||||
$q->join($alias.'.event', 'e')
|
||||
->join($alias.'.campaign', 'c');
|
||||
} else {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('IDENTITY('.$this->getTableAlias().'.campaign)', (int) $args['campaign_id'])
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($args['contact_id'])) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('IDENTITY('.$this->getTableAlias().'.lead)', (int) $args['contact_id'])
|
||||
);
|
||||
}
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'll';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lead's page event log.
|
||||
*
|
||||
* @param int|null $leadId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLeadLogs($leadId = null, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->select('ll.id as log_id,
|
||||
ll.event_id,
|
||||
ll.campaign_id,
|
||||
ll.date_triggered as dateTriggered,
|
||||
e.name AS event_name,
|
||||
e.description AS event_description,
|
||||
e.parent_id AS parent_id,
|
||||
e.decision_path AS decision_path,
|
||||
c.name AS campaign_name,
|
||||
c.description AS campaign_description,
|
||||
ll.metadata,
|
||||
e.type,
|
||||
ll.is_scheduled as isScheduled,
|
||||
ll.trigger_date as triggerDate,
|
||||
ll.channel,
|
||||
ll.channel_id as channel_id,
|
||||
ll.lead_id,
|
||||
fl.reason as fail_reason
|
||||
'
|
||||
)
|
||||
->add('from', [
|
||||
'table' => MAUTIC_TABLE_PREFIX.'campaign_lead_event_log',
|
||||
'alias' => 'll',
|
||||
'hint' => 'USE INDEX ('.MAUTIC_TABLE_PREFIX.'campaign_date_triggered)',
|
||||
], true)
|
||||
->join('ll', MAUTIC_TABLE_PREFIX.'campaign_events', 'e', 'll.event_id = e.id')
|
||||
->join('ll', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'll.campaign_id = c.id')
|
||||
->leftJoin('ll', MAUTIC_TABLE_PREFIX.'campaign_lead_event_failed_log', 'fl', 'fl.log_id = ll.id')
|
||||
->andWhere('e.event_type != :eventType')
|
||||
->setParameter('eventType', 'decision');
|
||||
|
||||
if ($leadId) {
|
||||
$query->where('ll.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
}
|
||||
|
||||
if (isset($options['scheduledState'])) {
|
||||
if ($options['scheduledState']) {
|
||||
// Include cancelled as well
|
||||
$query->andWhere(
|
||||
$query->expr()->or(
|
||||
$query->expr()->eq('ll.is_scheduled', ':scheduled'),
|
||||
$query->expr()->and(
|
||||
$query->expr()->eq('ll.is_scheduled', 0),
|
||||
$query->expr()->isNull('ll.date_triggered')
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$query->andWhere(
|
||||
$query->expr()->eq('ll.is_scheduled', ':scheduled')
|
||||
);
|
||||
}
|
||||
$query->setParameter('scheduled', $options['scheduledState'], 'boolean');
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere(
|
||||
$query->expr()->or(
|
||||
$query->expr()->like('e.name', ':search'),
|
||||
$query->expr()->like('e.description', ':search'),
|
||||
$query->expr()->like('c.name', ':search'),
|
||||
$query->expr()->like('c.description', ':search')
|
||||
)
|
||||
)->setParameter('search', '%'.$options['search'].'%');
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($query, $options, 'e.name', 'll.date_triggered', ['metadata'], ['dateTriggered', 'triggerDate'], null, 'll.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lead's upcoming events.
|
||||
*/
|
||||
public function getUpcomingEvents(?array $options = null): array
|
||||
{
|
||||
$leadIps = [];
|
||||
|
||||
$query = $this->_em->getConnection()->createQueryBuilder();
|
||||
$query->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'll')
|
||||
->select('ll.event_id,
|
||||
ll.campaign_id,
|
||||
ll.trigger_date,
|
||||
ll.lead_id,
|
||||
e.name AS event_name,
|
||||
e.description AS event_description,
|
||||
c.name AS campaign_name,
|
||||
c.description AS campaign_description,
|
||||
ll.metadata,
|
||||
CONCAT(CONCAT(l.firstname, \' \'), l.lastname) AS lead_name')
|
||||
->leftJoin('ll', MAUTIC_TABLE_PREFIX.'campaign_events', 'e', 'e.id = ll.event_id')
|
||||
->leftJoin('ll', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'c.id = e.campaign_id')
|
||||
->leftJoin('ll', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = ll.lead_id')
|
||||
->where($query->expr()->eq('ll.is_scheduled', 1))
|
||||
->andWhere('ll.trigger_date > NOW()');
|
||||
|
||||
if (isset($options['lead'])) {
|
||||
/** @var \Mautic\CoreBundle\Entity\IpAddress $ip */
|
||||
foreach ($options['lead']->getIpAddresses() as $ip) {
|
||||
$leadIps[] = $ip->getId();
|
||||
}
|
||||
|
||||
$query->andWhere('ll.lead_id = :leadId')
|
||||
->setParameter('leadId', $options['lead']->getId());
|
||||
}
|
||||
|
||||
if (isset($options['type'])) {
|
||||
$query->andwhere('e.type = :type')
|
||||
->setParameter('type', $options['type']);
|
||||
}
|
||||
|
||||
if (isset($options['eventType'])) {
|
||||
if (is_array($options['eventType'])) {
|
||||
$query->andWhere(
|
||||
$query->expr()->in('e.event_type', array_map([$query->expr(), 'literal'], $options['eventType']))
|
||||
);
|
||||
} else {
|
||||
$query->andwhere('e.event_type = :eventTypes')
|
||||
->setParameter('eventTypes', $options['eventType']);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($options['limit'])) {
|
||||
$query->setMaxResults($options['limit']);
|
||||
} else {
|
||||
$query->setMaxResults(10);
|
||||
}
|
||||
|
||||
$query->orderBy('ll.trigger_date');
|
||||
|
||||
if (empty($options['canViewOthers']) && isset($this->currentUser)) {
|
||||
$query->andWhere('c.created_by = :userId')
|
||||
->setParameter('userId', $this->currentUser->getId());
|
||||
}
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $campaignId
|
||||
* @param bool $excludeScheduled
|
||||
* @param bool $excludeNegative
|
||||
* @param bool $all
|
||||
*
|
||||
* @throws \Doctrine\DBAL\Cache\CacheException
|
||||
*/
|
||||
public function getCampaignLogCounts(
|
||||
$campaignId,
|
||||
$excludeScheduled = false,
|
||||
$excludeNegative = true,
|
||||
$all = false,
|
||||
?\DateTimeInterface $dateFrom = null,
|
||||
?\DateTimeInterface $dateTo = null,
|
||||
?int $eventId = null,
|
||||
): array {
|
||||
$join = $all ? 'leftJoin' : 'innerJoin';
|
||||
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'o');
|
||||
$q->$join(
|
||||
'o',
|
||||
MAUTIC_TABLE_PREFIX.'campaign_leads',
|
||||
'l',
|
||||
'l.campaign_id = '.(int) $campaignId.' and o.lead_id = l.lead_id'
|
||||
);
|
||||
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->eq('o.campaign_id', (int) $campaignId)
|
||||
);
|
||||
|
||||
if ($eventId) {
|
||||
$expr = $expr->with(
|
||||
$q->expr()->eq('o.event_id', $eventId)
|
||||
);
|
||||
}
|
||||
|
||||
$groupBy = 'o.event_id';
|
||||
if ($excludeNegative) {
|
||||
$q->select('o.event_id, count(o.lead_id) as lead_count');
|
||||
$expr = $expr->with(
|
||||
$q->expr()->or(
|
||||
$q->expr()->isNull('o.non_action_path_taken'),
|
||||
$q->expr()->eq('o.non_action_path_taken', ':false')
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$q->select('o.event_id, count(o.lead_id) as lead_count, o.non_action_path_taken');
|
||||
$groupBy .= ', o.non_action_path_taken';
|
||||
}
|
||||
|
||||
if ($excludeScheduled) {
|
||||
$expr = $expr->with(
|
||||
$q->expr()->eq('o.is_scheduled', ':false')
|
||||
);
|
||||
}
|
||||
|
||||
// Exclude failed events
|
||||
$failedSq = $this->getReplicaConnection()->createQueryBuilder();
|
||||
$failedSq->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_failed_log', 'fe')
|
||||
->where(
|
||||
$failedSq->expr()->eq('fe.log_id', 'o.id')
|
||||
);
|
||||
if ($dateFrom && $dateTo) {
|
||||
$failedSq->andWhere('fe.date_added BETWEEN FROM_UNIXTIME(:dateFrom) AND FROM_UNIXTIME(:dateTo)')
|
||||
->setParameter('dateFrom', $dateFrom->getTimestamp(), \PDO::PARAM_INT)
|
||||
->setParameter('dateTo', $dateTo->getTimestamp(), \PDO::PARAM_INT);
|
||||
}
|
||||
$expr = $expr->with(
|
||||
sprintf('NOT EXISTS (%s)', $failedSq->getSQL())
|
||||
);
|
||||
|
||||
$q->where($expr)
|
||||
->setParameter('false', false, 'boolean')
|
||||
->groupBy($groupBy);
|
||||
|
||||
if ($dateFrom && $dateTo) {
|
||||
$q->andWhere('o.date_triggered BETWEEN FROM_UNIXTIME(:dateFrom) AND FROM_UNIXTIME(:dateTo)')
|
||||
->setParameter('dateFrom', $dateFrom->getTimestamp(), \PDO::PARAM_INT)
|
||||
->setParameter('dateTo', $dateTo->getTimestamp(), \PDO::PARAM_INT);
|
||||
}
|
||||
|
||||
if ($this->_em->getConnection()->getConfiguration()->getResultCache()) {
|
||||
$results = $this->_em->getConnection()->executeCacheQuery(
|
||||
$q->getSQL(),
|
||||
$q->getParameters(),
|
||||
$q->getParameterTypes(),
|
||||
new QueryCacheProfile(600)
|
||||
)->fetchAllAssociative();
|
||||
} else {
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
// group by event id
|
||||
foreach ($results as $l) {
|
||||
if (!$excludeNegative) {
|
||||
if (!isset($return[$l['event_id']])) {
|
||||
$return[$l['event_id']] = [
|
||||
0 => 0,
|
||||
1 => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$key = (int) $l['non_action_path_taken'] ? 0 : 1;
|
||||
$return[$l['event_id']][$key] = (int) $l['lead_count'];
|
||||
} else {
|
||||
$return[$l['event_id']] = (int) $l['lead_count'];
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
// First check to ensure the $toLead doesn't already exist
|
||||
$results = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('cl.event_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'cl')
|
||||
->where('cl.lead_id = '.$toLeadId)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
$exists = [];
|
||||
foreach ($results as $r) {
|
||||
$exists[] = $r['event_id'];
|
||||
}
|
||||
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId);
|
||||
|
||||
if (!empty($exists)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->notIn('event_id', $exists)
|
||||
)->executeStatement();
|
||||
|
||||
// Delete remaining leads as the new lead already belongs
|
||||
$this->_em->getConnection()->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log')
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
} else {
|
||||
$q->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
public function getChartQuery($options): array
|
||||
{
|
||||
$chartQuery = new ChartQuery($this->getReplicaConnection(), $options['dateFrom'], $options['dateTo']);
|
||||
|
||||
// Load points for selected period
|
||||
$query = $this->getReplicaConnection()->createQueryBuilder();
|
||||
$query->select('ll.id, ll.date_triggered')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'll')
|
||||
->join('ll', MAUTIC_TABLE_PREFIX.'campaign_events', 'e', 'e.id = ll.event_id');
|
||||
|
||||
if (isset($options['channel'])) {
|
||||
$query->andWhere('e.channel = '.$query->expr()->literal($options['channel']));
|
||||
}
|
||||
|
||||
if (isset($options['channelId'])) {
|
||||
$query->andWhere('e.channel_id = '.(int) $options['channelId']);
|
||||
}
|
||||
|
||||
if (isset($options['type'])) {
|
||||
$query->andWhere('e.type = '.$query->expr()->literal($options['type']));
|
||||
}
|
||||
|
||||
if (isset($options['logChannel'])) {
|
||||
$query->andWhere('ll.channel = '.$query->expr()->literal($options['logChannel']));
|
||||
}
|
||||
|
||||
if (isset($options['logChannelId'])) {
|
||||
$query->andWhere('ll.channel_id = '.(int) $options['logChannelId']);
|
||||
}
|
||||
|
||||
$isScheduled = isset($options['is_scheduled']) ? 1 : 0;
|
||||
$query->andWhere('ll.is_scheduled = '.$isScheduled);
|
||||
|
||||
return $chartQuery->fetchTimeData('('.$query.')', 'date_triggered');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $eventId
|
||||
*
|
||||
* @return ArrayCollection
|
||||
*
|
||||
* @throws \Doctrine\ORM\Query\QueryException
|
||||
*/
|
||||
public function getScheduled($eventId, \DateTime $now, ContactLimiter $limiter)
|
||||
{
|
||||
if ($limiter->hasCampaignLimit() && 0 === $limiter->getCampaignLimitRemaining()) {
|
||||
return new ArrayCollection();
|
||||
}
|
||||
|
||||
$this->getReplicaConnection($limiter);
|
||||
|
||||
$q = $this->createQueryBuilder('o');
|
||||
|
||||
$q->select('o, e, c')
|
||||
->indexBy('o', 'o.id')
|
||||
->innerJoin('o.event', 'e')
|
||||
->innerJoin('e.campaign', 'c')
|
||||
->where(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->eq('IDENTITY(o.event)', ':eventId'),
|
||||
$q->expr()->eq('o.isScheduled', ':true'),
|
||||
$q->expr()->lte('o.triggerDate', ':now'),
|
||||
$q->expr()->eq('c.isPublished', 1)
|
||||
)
|
||||
)
|
||||
->setParameter('eventId', (int) $eventId)
|
||||
->setParameter('now', $now)
|
||||
->setParameter('true', true, Types::BOOLEAN);
|
||||
|
||||
$this->updateOrmQueryFromContactLimiter('o', $q, $limiter);
|
||||
|
||||
if ($limiter->hasCampaignLimit() && $limiter->getCampaignLimitRemaining() < $limiter->getBatchLimit()) {
|
||||
$q->setMaxResults($limiter->getCampaignLimitRemaining());
|
||||
}
|
||||
|
||||
$result = new ArrayCollection($q->getQuery()->getResult());
|
||||
|
||||
if ($limiter->hasCampaignLimit()) {
|
||||
$limiter->reduceCampaignLimitRemaining($result->count());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\Query\QueryException
|
||||
*/
|
||||
public function getScheduledByIds(array $ids): ArrayCollection
|
||||
{
|
||||
$this->getReplicaConnection();
|
||||
$q = $this->createQueryBuilder('o');
|
||||
|
||||
$q->select('o, e, c')
|
||||
->indexBy('o', 'o.id')
|
||||
->innerJoin('o.event', 'e')
|
||||
->innerJoin('e.campaign', 'c')
|
||||
->where(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->in('o.id', $ids),
|
||||
$q->expr()->eq('o.isScheduled', 1),
|
||||
$q->expr()->eq('c.isPublished', 1),
|
||||
$q->expr()->isNull('c.deleted'),
|
||||
$q->expr()->isNull('e.deleted')
|
||||
)
|
||||
);
|
||||
|
||||
return new ArrayCollection($q->getQuery()->getResult());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $campaignId
|
||||
*/
|
||||
public function getScheduledCounts($campaignId, \DateTime $date, ContactLimiter $limiter): array
|
||||
{
|
||||
$now = clone $date;
|
||||
$now->setTimezone(new \DateTimeZone('UTC'));
|
||||
|
||||
$q = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->eq('l.campaign_id', ':campaignId'),
|
||||
$q->expr()->eq('l.is_scheduled', ':true'),
|
||||
$q->expr()->lte('l.trigger_date', ':now'),
|
||||
$q->expr()->eq('c.is_published', 1)
|
||||
);
|
||||
|
||||
$this->updateQueryFromContactLimiter('l', $q, $limiter, true);
|
||||
|
||||
$results = $q->select('COUNT(*) as event_count, l.event_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'l')
|
||||
->join('l', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'l.campaign_id = c.id')
|
||||
->where($expr)
|
||||
->setParameter('campaignId', (int) $campaignId)
|
||||
->setParameter('now', $now->format('Y-m-d H:i:s'))
|
||||
->setParameter('true', true, \PDO::PARAM_BOOL)
|
||||
->groupBy('l.event_id')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$events = [];
|
||||
|
||||
foreach ($results as $result) {
|
||||
$events[$result['event_id']] = (int) $result['event_count'];
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
public function getDatesExecuted($eventId, array $contactIds): array
|
||||
{
|
||||
$qb = $this->getReplicaConnection()->createQueryBuilder();
|
||||
$qb->select('log.lead_id, log.date_triggered, log.is_scheduled')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->eq('log.event_id', $eventId),
|
||||
$qb->expr()->in('log.lead_id', $contactIds)
|
||||
)
|
||||
);
|
||||
|
||||
$results = $qb->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$dates = [];
|
||||
foreach ($results as $result) {
|
||||
$dates[$result['lead_id']] = new \DateTime($result['date_triggered'], new \DateTimeZone('UTC'));
|
||||
if (1 === (int) $result['is_scheduled']) {
|
||||
unset($dates[$result['lead_id']]);
|
||||
}
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
public function getOldestTriggeredDate(): ?\DateTime
|
||||
{
|
||||
$qb = $this->getReplicaConnection()->createQueryBuilder();
|
||||
$qb->select('log.date_triggered')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log')
|
||||
->orderBy('log.date_triggered', 'ASC')
|
||||
->setMaxResults(1);
|
||||
|
||||
$results = $qb->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return isset($results[0]['date_triggered']) ? new \DateTime($results[0]['date_triggered']) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $contactId
|
||||
* @param int $campaignId
|
||||
* @param int $rotation
|
||||
*/
|
||||
public function hasBeenInCampaignRotation($contactId, $campaignId, $rotation): bool
|
||||
{
|
||||
$qb = $this->getReplicaConnection()->createQueryBuilder();
|
||||
$qb->select('log.rotation')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->eq('log.lead_id', ':contactId'),
|
||||
$qb->expr()->eq('log.campaign_id', ':campaignId'),
|
||||
$qb->expr()->in('log.rotation', ':rotation')
|
||||
)
|
||||
)
|
||||
->setParameter('contactId', (int) $contactId)
|
||||
->setParameter('campaignId', (int) $campaignId)
|
||||
->setParameter('rotation', (int) $rotation)
|
||||
->setMaxResults(1);
|
||||
|
||||
$results = $qb->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return !empty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
*
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
*/
|
||||
public function unscheduleEvents(Lead $campaignMember, $message): void
|
||||
{
|
||||
$contactId = $campaignMember->getLead()->getId();
|
||||
$campaignId = $campaignMember->getCampaign()->getId();
|
||||
$rotation = $campaignMember->getRotation();
|
||||
$dateAdded = (new \DateTime('now', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
// Insert entries into the failed log so it's known why they were never executed
|
||||
$prefix = MAUTIC_TABLE_PREFIX;
|
||||
$sql = <<<SQL
|
||||
REPLACE INTO {$prefix}campaign_lead_event_failed_log( `log_id`, `date_added`, `reason`)
|
||||
SELECT id, :dateAdded as date_added, :message as reason from {$prefix}campaign_lead_event_log
|
||||
WHERE is_scheduled = 1 AND lead_id = :contactId AND campaign_id = :campaignId AND rotation = :rotation
|
||||
SQL;
|
||||
|
||||
$connection = $this->getEntityManager()->getConnection();
|
||||
$stmt = $connection->prepare($sql);
|
||||
$stmt->bindValue('dateAdded', $dateAdded, \PDO::PARAM_STR);
|
||||
$stmt->bindValue('message', $message, \PDO::PARAM_STR);
|
||||
$stmt->bindValue('contactId', $contactId, \PDO::PARAM_INT);
|
||||
$stmt->bindValue('campaignId', $campaignId, \PDO::PARAM_INT);
|
||||
$stmt->bindValue('rotation', $rotation, \PDO::PARAM_INT);
|
||||
$stmt->executeStatement();
|
||||
|
||||
// Now unschedule them
|
||||
$qb = $connection->createQueryBuilder();
|
||||
$qb->update(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log')
|
||||
->set('is_scheduled', 0)
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->eq('is_scheduled', 1),
|
||||
$qb->expr()->eq('lead_id', ':contactId'),
|
||||
$qb->expr()->eq('campaign_id', ':campaignId'),
|
||||
$qb->expr()->eq('rotation', ':rotation')
|
||||
)
|
||||
)
|
||||
->setParameters(
|
||||
[
|
||||
'contactId' => (int) $contactId,
|
||||
'campaignId' => (int) $campaignId,
|
||||
'rotation' => (int) $rotation,
|
||||
]
|
||||
)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function removeEventLogsByCampaignId(int $campaignId): void
|
||||
{
|
||||
$table_name = $this->getTableName();
|
||||
$sql = "DELETE FROM {$table_name} WHERE campaign_id = (?) LIMIT ".self::LOG_DELETE_BATCH_SIZE;
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
$deleteEntries = true;
|
||||
while ($deleteEntries) {
|
||||
$deleteEntries = $conn->executeStatement($sql, [$campaignId], [Types::INTEGER]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $eventIds
|
||||
*/
|
||||
public function removeEventLogs(array $eventIds): void
|
||||
{
|
||||
$table_name = $this->getTableName();
|
||||
$sql = "DELETE FROM {$table_name} WHERE event_id IN (?) ORDER BY event_id ASC LIMIT ".self::LOG_DELETE_BATCH_SIZE;
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
$deleteEntries = true;
|
||||
while ($deleteEntries) {
|
||||
$deleteEntries = $conn->executeStatement($sql, [$eventIds], [ArrayParameterType::INTEGER]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if last lead/event failed.
|
||||
*
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function isLastFailed(int $leadId, int $eventId): bool
|
||||
{
|
||||
/** @var LeadEventLog $log */
|
||||
$log = $this->findOneBy(['lead' => $leadId, 'event' => $eventId], ['dateTriggered' => 'DESC']);
|
||||
|
||||
if (null !== $log && null !== $log->getFailedLog()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function deleteAnonymousContacts(): int
|
||||
{
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
$tableName = $this->getTableName();
|
||||
$leadsTableName = MAUTIC_TABLE_PREFIX.'leads';
|
||||
$tempTableName = 'to_delete';
|
||||
$conn->executeQuery(sprintf('DROP TEMPORARY TABLE IF EXISTS %s', $tempTableName));
|
||||
$conn->executeQuery(sprintf('CREATE TEMPORARY TABLE %s select id AS lead_id from %s where date_identified is null;', $tempTableName, $leadsTableName));
|
||||
$deleteQuery = sprintf('DELETE lll FROM %s lll JOIN (SELECT lead_id FROM %s LIMIT %d) d USING (lead_id); ', $tableName, $tempTableName, self::LOG_DELETE_BATCH_SIZE);
|
||||
$deletedRecordCount= 0;
|
||||
while ($deletedRows = $conn->executeQuery($deleteQuery)->rowCount()) {
|
||||
$deletedRecordCount += $deletedRows;
|
||||
}
|
||||
|
||||
return $deletedRecordCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $ids
|
||||
*/
|
||||
public function markEventLogsQueued(array $ids): void
|
||||
{
|
||||
if (!$ids) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getEntityManager()
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->update($this->getTableName())
|
||||
->set('date_queued', 'NOW()')
|
||||
->where('id IN (:ids)')
|
||||
->setParameter('ids', $ids, ArrayParameterType::STRING)
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CampaignBundle\Entity\Result\CountResult;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Lead>
|
||||
*/
|
||||
class LeadRepository extends CommonRepository
|
||||
{
|
||||
use ContactLimiterTrait;
|
||||
use ReplicaConnectionTrait;
|
||||
|
||||
public const DELETE_BATCH_SIZE = 5000;
|
||||
|
||||
/**
|
||||
* Get the details of leads added to a campaign.
|
||||
*/
|
||||
public function getLeadDetails($campaignId, $leads = null): array
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(Lead::class, 'lc')
|
||||
->select('lc')
|
||||
->leftJoin('lc.campaign', 'c')
|
||||
->leftJoin('lc.lead', 'l');
|
||||
$q->where(
|
||||
$q->expr()->eq('c.id', ':campaign')
|
||||
)->setParameter('campaign', $campaignId);
|
||||
|
||||
if (!empty($leads)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('l.id', ':leads')
|
||||
)->setParameter('leads', $leads);
|
||||
}
|
||||
|
||||
$results = $q->getQuery()->getArrayResult();
|
||||
|
||||
$return = [];
|
||||
foreach ($results as $r) {
|
||||
$return[$r['lead_id']][] = $r;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get leads for a specific campaign.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLeads($campaignId, $eventId = null)
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(Lead::class, 'lc')
|
||||
->select('lc, l')
|
||||
->leftJoin('lc.campaign', 'c')
|
||||
->leftJoin('lc.lead', 'l');
|
||||
$q->where(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->eq('lc.manuallyRemoved', ':false'),
|
||||
$q->expr()->eq('c.id', ':campaign')
|
||||
)
|
||||
)
|
||||
->setParameter('false', false, 'boolean')
|
||||
->setParameter('campaign', $campaignId);
|
||||
|
||||
if (null != $eventId) {
|
||||
$dq = $this->getEntityManager()->createQueryBuilder();
|
||||
$dq->select('el.id')
|
||||
->from(LeadEventLog::class, 'ell')
|
||||
->leftJoin('ell.lead', 'el')
|
||||
->leftJoin('ell.event', 'ev')
|
||||
->where(
|
||||
$dq->expr()->eq('ev.id', ':eventId')
|
||||
);
|
||||
|
||||
$q->andWhere('l.id NOT IN('.$dq->getDQL().')')
|
||||
->setParameter('eventId', $eventId);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
// First check to ensure the $toLead doesn't already exist
|
||||
$results = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('cl.campaign_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where('cl.lead_id = '.$toLeadId)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$campaigns = [];
|
||||
foreach ($results as $r) {
|
||||
$campaigns[] = $r['campaign_id'];
|
||||
}
|
||||
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'campaign_leads')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId);
|
||||
|
||||
if (!empty($campaigns)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->notIn('campaign_id', $campaigns)
|
||||
)->executeStatement();
|
||||
|
||||
// Delete remaining leads as the new lead already belongs
|
||||
$this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'campaign_leads')
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
} else {
|
||||
$q->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Lead in campaign.
|
||||
*
|
||||
* @param Lead $lead
|
||||
* @param array $options
|
||||
*/
|
||||
public function checkLeadInCampaigns($lead, $options = []): bool
|
||||
{
|
||||
if (empty($options['campaigns'])) {
|
||||
return false;
|
||||
}
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('l.campaign_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l');
|
||||
$q->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.lead_id', ':leadId'),
|
||||
$q->expr()->in('l.campaign_id', $options['campaigns'])
|
||||
)
|
||||
);
|
||||
|
||||
if (!empty($options['dataAddedLimit'])) {
|
||||
$q->andWhere($q->expr()
|
||||
->{$options['expr']}('l.date_added', ':dateAdded'))
|
||||
->setParameter('dateAdded', $options['dateAdded']);
|
||||
}
|
||||
|
||||
$q->setParameter('leadId', $lead->getId());
|
||||
|
||||
return (bool) $q->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $campaignId
|
||||
* @param int $decisionId
|
||||
* @param int $parentDecisionId
|
||||
*
|
||||
* @return array<string, \DateTimeInterface>
|
||||
*/
|
||||
public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, ContactLimiter $limiter): array
|
||||
{
|
||||
// Main query
|
||||
$q = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
$q->select('l.lead_id, l.date_added')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.campaign_id', ':campaignId'),
|
||||
$q->expr()->eq('l.manually_removed', 0)
|
||||
)
|
||||
)
|
||||
// Order by ID so we can query by greater than X contact ID when batching
|
||||
->orderBy('l.lead_id')
|
||||
->setMaxResults($limiter->getBatchLimit())
|
||||
->setParameter('campaignId', (int) $campaignId)
|
||||
->setParameter('decisionId', (int) $decisionId);
|
||||
|
||||
// Contact IDs
|
||||
$this->updateQueryFromContactLimiter('l', $q, $limiter);
|
||||
|
||||
// Limit to events that have not been executed or scheduled yet
|
||||
$eventQb = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
$eventQb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log')
|
||||
->where(
|
||||
$eventQb->expr()->and(
|
||||
$eventQb->expr()->eq('log.event_id', ':decisionId'),
|
||||
$eventQb->expr()->eq('log.lead_id', 'l.lead_id'),
|
||||
$eventQb->expr()->eq('log.rotation', 'l.rotation')
|
||||
)
|
||||
);
|
||||
$q->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $eventQb->getSQL())
|
||||
);
|
||||
|
||||
if ($parentDecisionId) {
|
||||
// Limit to events whose grandparent has already been executed
|
||||
$grandparentQb = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
$grandparentQb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'grandparent_log')
|
||||
->where(
|
||||
$grandparentQb->expr()->eq('grandparent_log.event_id', ':grandparentId'),
|
||||
$grandparentQb->expr()->eq('grandparent_log.lead_id', 'l.lead_id'),
|
||||
$grandparentQb->expr()->eq('grandparent_log.rotation', 'l.rotation')
|
||||
);
|
||||
$q->setParameter('grandparentId', (int) $parentDecisionId);
|
||||
|
||||
$q->andWhere(
|
||||
sprintf('EXISTS (%s)', $grandparentQb->getSQL())
|
||||
);
|
||||
} else {
|
||||
// Limit to events that have no grandparent and any of events was already executed by jump to event
|
||||
$anyEventQb = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
$anyEventQb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'any_log')
|
||||
->where(
|
||||
$anyEventQb->expr()->eq('any_log.lead_id', 'l.lead_id'),
|
||||
$anyEventQb->expr()->eq('any_log.campaign_id', 'l.campaign_id'),
|
||||
$anyEventQb->expr()->eq('any_log.rotation', 'l.rotation')
|
||||
);
|
||||
|
||||
$q->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $anyEventQb->getSQL())
|
||||
);
|
||||
}
|
||||
|
||||
if ($limiter->hasCampaignLimit() && $limiter->getCampaignLimitRemaining() < $limiter->getBatchLimit()) {
|
||||
$q->setMaxResults($limiter->getCampaignLimitRemaining());
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$contacts = [];
|
||||
foreach ($results as $result) {
|
||||
$contacts[$result['lead_id']] = new \DateTime($result['date_added'], new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
if ($limiter->hasCampaignLimit()) {
|
||||
$limiter->reduceCampaignLimitRemaining(count($contacts));
|
||||
}
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is approximate because the query that fetches contacts per decision is based on if the grandparent has been executed or not.
|
||||
*/
|
||||
public function getInactiveContactCount($campaignId, array $decisionIds, ContactLimiter $limiter): int
|
||||
{
|
||||
// We have to loop over each decision to get a count or else any contact that has executed any single one of the decision IDs
|
||||
// will not be included potentially resulting in not having the inactive path analyzed
|
||||
|
||||
$totalCount = 0;
|
||||
|
||||
foreach ($decisionIds as $decisionId) {
|
||||
// Main query
|
||||
$q = $this->getReplicaConnection()->createQueryBuilder();
|
||||
$q->select('count(*)')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.campaign_id', ':campaignId'),
|
||||
$q->expr()->eq('l.manually_removed', 0)
|
||||
)
|
||||
)
|
||||
// Order by ID so we can query by greater than X contact ID when batching
|
||||
->orderBy('l.lead_id')
|
||||
->setParameter('campaignId', (int) $campaignId);
|
||||
|
||||
// Contact IDs
|
||||
$this->updateQueryFromContactLimiter('l', $q, $limiter, true);
|
||||
|
||||
// Limit to events that have not been executed or scheduled yet
|
||||
$eventQb = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
$eventQb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log')
|
||||
->where(
|
||||
$eventQb->expr()->and(
|
||||
$eventQb->expr()->eq('log.event_id', $decisionId),
|
||||
$eventQb->expr()->eq('log.lead_id', 'l.lead_id'),
|
||||
$eventQb->expr()->eq('log.rotation', 'l.rotation')
|
||||
)
|
||||
);
|
||||
$q->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $eventQb->getSQL())
|
||||
);
|
||||
|
||||
$totalCount += (int) $q->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
return $totalCount;
|
||||
}
|
||||
|
||||
public function getCampaignMembers(array $contactIds, Campaign $campaign): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('l');
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('l.campaign', ':campaign'),
|
||||
$qb->expr()->in('IDENTITY(l.lead)', ':contactIds')
|
||||
)
|
||||
)
|
||||
->setParameter('campaign', $campaign)
|
||||
->setParameter('contactIds', $contactIds, \Doctrine\DBAL\ArrayParameterType::INTEGER);
|
||||
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
$campaignMembers = [];
|
||||
|
||||
/** @var Lead $result */
|
||||
foreach ($results as $result) {
|
||||
$campaignMembers[$result->getLead()->getId()] = $result;
|
||||
}
|
||||
|
||||
return $campaignMembers;
|
||||
}
|
||||
|
||||
public function getContactRotations(array $contactIds, $campaignId): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->select('cl.lead_id, cl.rotation, cl.manually_removed')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->eq('cl.campaign_id', ':campaignId'),
|
||||
$qb->expr()->in('cl.lead_id', ':contactIds')
|
||||
)
|
||||
)
|
||||
->setParameter('campaignId', (int) $campaignId)
|
||||
->setParameter('contactIds', $contactIds, \Doctrine\DBAL\ArrayParameterType::INTEGER);
|
||||
|
||||
$results = $qb->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$contactRotations = [];
|
||||
foreach ($results as $result) {
|
||||
$contactRotations[$result['lead_id']] = ['rotation' => $result['rotation'], 'manually_removed' => $result['manually_removed']];
|
||||
}
|
||||
|
||||
return $contactRotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $campaignId
|
||||
* @param bool $campaignCanBeRestarted
|
||||
*/
|
||||
public function getCountsForCampaignContactsBySegment($campaignId, ContactLimiter $limiter, $campaignCanBeRestarted = false): CountResult
|
||||
{
|
||||
if (!$segments = $this->getCampaignSegments($campaignId)) {
|
||||
return new CountResult(0, 0, 0);
|
||||
}
|
||||
|
||||
$qb = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
$qb->select('min(ll.lead_id) as min_id, max(ll.lead_id) as max_id, count(distinct(ll.lead_id)) as the_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->eq('ll.manually_removed', 0),
|
||||
$qb->expr()->in('ll.leadlist_id', $segments)
|
||||
)
|
||||
);
|
||||
|
||||
$this->updateQueryFromContactLimiter('ll', $qb, $limiter, true);
|
||||
$this->updateQueryWithExistingMembershipExclusion((int) $campaignId, $qb, (bool) $campaignCanBeRestarted);
|
||||
|
||||
if (!$campaignCanBeRestarted) {
|
||||
$this->updateQueryWithHistoryExclusion($campaignId, $qb);
|
||||
}
|
||||
|
||||
$result = $qb->executeQuery()->fetchAssociative();
|
||||
|
||||
return new CountResult($result['the_count'], $result['min_id'], $result['max_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all contacts based on the campaigns segment
|
||||
* a limit for how many contacts to process at one time
|
||||
* and the campaign setting if a contact is allowed to restart
|
||||
* a campaign.
|
||||
*
|
||||
* @param int $campaignId
|
||||
* @param bool $campaignCanBeRestarted
|
||||
*
|
||||
* @return array<int|string, string>
|
||||
*/
|
||||
public function getCampaignContactsBySegments($campaignId, ContactLimiter $limiter, $campaignCanBeRestarted = false): array
|
||||
{
|
||||
if (!$segments = $this->getCampaignSegments($campaignId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
$qb->select('distinct(ll.lead_id) as id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->eq('ll.manually_removed', 0),
|
||||
$qb->expr()->in('ll.leadlist_id', $segments)
|
||||
)
|
||||
)->orderBy('ll.lead_id');
|
||||
|
||||
$this->updateQueryFromContactLimiter('ll', $qb, $limiter);
|
||||
$this->updateQueryWithExistingMembershipExclusion((int) $campaignId, $qb, (bool) $campaignCanBeRestarted);
|
||||
|
||||
if (!$campaignCanBeRestarted) {
|
||||
$this->updateQueryWithHistoryExclusion($campaignId, $qb);
|
||||
}
|
||||
|
||||
$results = $qb->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$contacts = [];
|
||||
foreach ($results as $result) {
|
||||
$contacts[$result['id']] = $result['id'];
|
||||
}
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $campaignId
|
||||
*/
|
||||
public function getCountsForOrphanedContactsBySegments($campaignId, ContactLimiter $limiter): CountResult
|
||||
{
|
||||
$segments = $this->getCampaignSegments($campaignId);
|
||||
|
||||
$qb = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
$qb->select('min(cl.lead_id) as min_id, max(cl.lead_id) as max_id, count(cl.lead_id) as the_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->eq('cl.campaign_id', (int) $campaignId),
|
||||
$qb->expr()->eq('cl.manually_removed', 0),
|
||||
$qb->expr()->eq('cl.manually_added', 0)
|
||||
)
|
||||
);
|
||||
|
||||
$this->updateQueryFromContactLimiter('cl', $qb, $limiter, true);
|
||||
$this->updateQueryWithSegmentMembershipExclusion($segments, $qb);
|
||||
|
||||
$result = $qb->executeQuery()->fetchAssociative();
|
||||
|
||||
return new CountResult($result['the_count'], $result['min_id'], $result['max_id']);
|
||||
}
|
||||
|
||||
public function getOrphanedContacts($campaignId, ContactLimiter $limiter): array
|
||||
{
|
||||
$segments = $this->getCampaignSegments($campaignId);
|
||||
|
||||
$qb = $this->getReplicaConnection($limiter)->createQueryBuilder();
|
||||
$qb->select('cl.lead_id as id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->eq('cl.campaign_id', (int) $campaignId),
|
||||
$qb->expr()->eq('cl.manually_removed', 0),
|
||||
$qb->expr()->eq('cl.manually_added', 0)
|
||||
)
|
||||
);
|
||||
|
||||
$this->updateQueryFromContactLimiter('cl', $qb, $limiter, false);
|
||||
$this->updateQueryWithSegmentMembershipExclusion($segments, $qb);
|
||||
|
||||
$results = $qb->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$contacts = [];
|
||||
foreach ($results as $result) {
|
||||
$contacts[$result['id']] = $result['id'];
|
||||
}
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of contact ID's and increments
|
||||
* their current rotation in a campaign by 1.
|
||||
*
|
||||
* @param int[] $contactIds
|
||||
* @param int $campaignId
|
||||
*/
|
||||
public function incrementCampaignRotationForContacts(array $contactIds, $campaignId): void
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->set('cl.rotation', 'cl.rotation + 1')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->in('cl.lead_id', ':contactIds'),
|
||||
$q->expr()->eq('cl.campaign_id', ':campaignId')
|
||||
)
|
||||
)
|
||||
->setParameter('contactIds', $contactIds, \Doctrine\DBAL\ArrayParameterType::INTEGER)
|
||||
->setParameter('campaignId', (int) $campaignId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
private function getCampaignSegments($campaignId): array
|
||||
{
|
||||
// Get published segments for this campaign
|
||||
$segmentResults = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('cl.leadlist_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leadlist_xref', 'cl')
|
||||
->join('cl', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 'll.id = cl.leadlist_id and ll.is_published = 1')
|
||||
->where('cl.campaign_id = '.(int) $campaignId)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
if (empty($segmentResults)) {
|
||||
// No segments so no contacts
|
||||
return [];
|
||||
}
|
||||
|
||||
$segments = [];
|
||||
foreach ($segmentResults as $result) {
|
||||
$segments[] = $result['leadlist_id'];
|
||||
}
|
||||
|
||||
return $segments;
|
||||
}
|
||||
|
||||
private function updateQueryWithExistingMembershipExclusion(int $campaignId, QueryBuilder $qb, bool $campaignCanBeRestarted = false): void
|
||||
{
|
||||
$membershipConditions = $qb->expr()->and(
|
||||
$qb->expr()->eq('cl.lead_id', 'll.lead_id'),
|
||||
$qb->expr()->eq('cl.campaign_id', (int) $campaignId)
|
||||
);
|
||||
|
||||
if ($campaignCanBeRestarted) {
|
||||
$alreadyInCampaign = $qb->expr()->eq('cl.manually_removed', 0);
|
||||
$removedFromCampaignManually = $qb->expr()->and(
|
||||
$qb->expr()->eq('cl.manually_removed', 1),
|
||||
$qb->expr()->isNull('cl.date_last_exited'),
|
||||
);
|
||||
|
||||
$membershipConditions = $qb->expr()->and(
|
||||
$membershipConditions,
|
||||
$qb->expr()->or($alreadyInCampaign, $removedFromCampaignManually)
|
||||
);
|
||||
}
|
||||
|
||||
$subq = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where(
|
||||
$qb->expr()->and($membershipConditions)
|
||||
);
|
||||
|
||||
$qb->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $subq->getSQL())
|
||||
);
|
||||
}
|
||||
|
||||
private function updateQueryWithSegmentMembershipExclusion(array $segments, QueryBuilder $qb): void
|
||||
{
|
||||
if (0 === count($segments)) {
|
||||
// No segments so nothing to exclude
|
||||
return;
|
||||
}
|
||||
|
||||
$subq = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->eq('ll.lead_id', 'cl.lead_id'),
|
||||
$qb->expr()->eq('ll.manually_removed', 0),
|
||||
$qb->expr()->in('ll.leadlist_id', $segments)
|
||||
)
|
||||
);
|
||||
|
||||
$qb->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $subq->getSQL())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude contacts with any previous campaign history; this is mainly BC for pre 2.14.0 where the membership entry was deleted.
|
||||
*/
|
||||
private function updateQueryWithHistoryExclusion($campaignId, QueryBuilder $qb): void
|
||||
{
|
||||
$subq = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'el')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->eq('el.lead_id', 'll.lead_id'),
|
||||
$qb->expr()->eq('el.campaign_id', (int) $campaignId)
|
||||
)
|
||||
);
|
||||
|
||||
$qb->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $subq->getSQL())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{}|array<int, array<string, string|null>>
|
||||
*
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
*/
|
||||
public function getCampaignMembersGroupByCountry(Campaign $campaign, \DateTimeImmutable $dateFromObject, \DateTimeImmutable $dateToObject): array
|
||||
{
|
||||
$queryBuilder = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$leadCampaignAlias = 'lc';
|
||||
$leadAlias = 'l';
|
||||
|
||||
$queryBuilder->select(
|
||||
"$leadAlias.country",
|
||||
'count(id) AS contacts'
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', $leadCampaignAlias)
|
||||
->leftJoin(
|
||||
$leadCampaignAlias,
|
||||
MAUTIC_TABLE_PREFIX.'leads',
|
||||
$leadAlias,
|
||||
"$leadAlias.id = $leadCampaignAlias.lead_id"
|
||||
)
|
||||
->andWhere("$leadCampaignAlias.campaign_id = :campaign")
|
||||
->andWhere("$leadCampaignAlias.manually_removed = :false")
|
||||
->andWhere("$leadCampaignAlias.date_added BETWEEN :dateFrom AND :dateTo")
|
||||
->groupBy("$leadAlias.country")
|
||||
->orderBy("$leadAlias.country", 'ASC')
|
||||
->setParameter('campaign', $campaign->getId())
|
||||
->setParameter('false', false)
|
||||
->setParameter('dateFrom', $dateFromObject->format('Y-m-d H:i:s'))
|
||||
->setParameter('dateTo', $dateToObject->setTime(23, 59, 59)->format('Y-m-d H:i:s'));
|
||||
|
||||
return $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
public function deleteAnonymousContacts(): int
|
||||
{
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
$tableName = $this->getTableName();
|
||||
$leadsTableName = MAUTIC_TABLE_PREFIX.'leads';
|
||||
$tempTableName = 'to_delete';
|
||||
$conn->executeQuery(sprintf('DROP TEMPORARY TABLE IF EXISTS %s', $tempTableName));
|
||||
$conn->executeQuery(sprintf('CREATE TEMPORARY TABLE %s select DISTINCT lll.lead_id from %s lll join %s l on l.id = lll.lead_id where l.date_identified is null;', $tempTableName, $tableName, $leadsTableName));
|
||||
$deleteQuery = sprintf('DELETE lll FROM %s lll JOIN (SELECT lead_id FROM %s LIMIT %d) d USING (lead_id); ', $tableName, $tempTableName, self::DELETE_BATCH_SIZE);
|
||||
$deletedRecordCount= 0;
|
||||
while ($deletedRows = $conn->executeQuery($deleteQuery)->rowCount()) {
|
||||
$deletedRecordCount += $deletedRows;
|
||||
}
|
||||
|
||||
return $deletedRecordCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
|
||||
/**
|
||||
* Trait ReplicaConnectionTrait.
|
||||
*/
|
||||
trait ReplicaConnectionTrait
|
||||
{
|
||||
/**
|
||||
* Get a connection, preferring a replica connection if available and prudent.
|
||||
*
|
||||
* If a query is being executed with a limiter with specific contacts
|
||||
* then this could be a real-time request being handled so we should avoid forcing a replica connection.
|
||||
*/
|
||||
private function getReplicaConnection(?ContactLimiter $limiter = null): Connection
|
||||
{
|
||||
/** @var Connection $connection */
|
||||
$connection = $this->getEntityManager()->getConnection();
|
||||
if ($connection instanceof PrimaryReadReplicaConnection) {
|
||||
if (
|
||||
!$limiter
|
||||
|| !($limiter->getContactId() || $limiter->getContactIdList())
|
||||
) {
|
||||
$connection->ensureConnectedToReplica();
|
||||
}
|
||||
}
|
||||
|
||||
return $connection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity\Result;
|
||||
|
||||
class CountResult
|
||||
{
|
||||
private int $count;
|
||||
|
||||
private int $minId;
|
||||
|
||||
private int $maxId;
|
||||
|
||||
public function __construct($count, $minId, $maxId)
|
||||
{
|
||||
$this->count = (int) $count;
|
||||
$this->minId = (int) $minId;
|
||||
$this->maxId = (int) $maxId;
|
||||
}
|
||||
|
||||
public function getCount(): int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
public function getMinId(): int
|
||||
{
|
||||
return $this->minId;
|
||||
}
|
||||
|
||||
public function getMaxId(): int
|
||||
{
|
||||
return $this->maxId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class Summary
|
||||
{
|
||||
public const TABLE_NAME = 'campaign_summary';
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var \DateTimeImmutable|null
|
||||
**/
|
||||
private $dateTriggered;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $scheduledCount = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $triggeredCount = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $nonActionPathTakenCount = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $failedCount = 0;
|
||||
|
||||
/**
|
||||
* @var Event|null
|
||||
*/
|
||||
private $event;
|
||||
|
||||
/**
|
||||
* @var Campaign|null
|
||||
*/
|
||||
private $campaign;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $logCountsProcessed = 0;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(SummaryRepository::class)
|
||||
->addUniqueConstraint(['campaign_id', 'event_id', 'date_triggered'], 'campaign_event_date_triggered');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->createManyToOne('campaign', Campaign::class)
|
||||
->addJoinColumn('campaign_id', 'id')
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('event', Event::class)
|
||||
->addJoinColumn('event_id', 'id', false, false, 'CASCADE')
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('dateTriggered', Types::DATETIME_IMMUTABLE, 'date_triggered');
|
||||
$builder->addNamedField('scheduledCount', Types::INTEGER, 'scheduled_count');
|
||||
$builder->addNamedField('triggeredCount', Types::INTEGER, 'triggered_count');
|
||||
$builder->addNamedField('nonActionPathTakenCount', Types::INTEGER, 'non_action_path_taken_count');
|
||||
$builder->addNamedField('failedCount', Types::INTEGER, 'failed_count');
|
||||
$builder->addNamedField('logCountsProcessed', Types::INTEGER, 'log_counts_processed', true);
|
||||
}
|
||||
|
||||
public function getScheduledCount(): ?int
|
||||
{
|
||||
return $this->scheduledCount;
|
||||
}
|
||||
|
||||
public function setScheduledCount(int $scheduledCount): void
|
||||
{
|
||||
$this->scheduledCount = $scheduledCount;
|
||||
}
|
||||
|
||||
public function getTriggeredCount(): ?int
|
||||
{
|
||||
return $this->triggeredCount;
|
||||
}
|
||||
|
||||
public function setTriggeredCount(int $triggeredCount): void
|
||||
{
|
||||
$this->triggeredCount = $triggeredCount;
|
||||
}
|
||||
|
||||
public function getNonActionPathTakenCount(): ?int
|
||||
{
|
||||
return $this->nonActionPathTakenCount;
|
||||
}
|
||||
|
||||
public function setNonActionPathTakenCount(int $nonActionPathTakenCount): void
|
||||
{
|
||||
$this->nonActionPathTakenCount = $nonActionPathTakenCount;
|
||||
}
|
||||
|
||||
public function getFailedCount(): ?int
|
||||
{
|
||||
return $this->failedCount;
|
||||
}
|
||||
|
||||
public function setFailedCount(int $failedCount): void
|
||||
{
|
||||
$this->failedCount = $failedCount;
|
||||
}
|
||||
|
||||
public function getCampaign(): ?Campaign
|
||||
{
|
||||
return $this->campaign;
|
||||
}
|
||||
|
||||
public function setCampaign(Campaign $campaign): void
|
||||
{
|
||||
$this->campaign = $campaign;
|
||||
}
|
||||
|
||||
public function getEvent(): ?Event
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
public function setEvent(Event $event): void
|
||||
{
|
||||
$this->event = $event;
|
||||
|
||||
if (!$this->campaign) {
|
||||
$this->setCampaign($event->getCampaign());
|
||||
}
|
||||
}
|
||||
|
||||
public function getDateTriggered(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->dateTriggered;
|
||||
}
|
||||
|
||||
public function setDateTriggered(?\DateTimeImmutable $dateTriggered = null): void
|
||||
{
|
||||
$this->dateTriggered = $dateTriggered;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLogCountsProcessed(): ?int
|
||||
{
|
||||
return $this->logCountsProcessed;
|
||||
}
|
||||
|
||||
public function setLogCountsProcessed(?int $logCountsProcessed): void
|
||||
{
|
||||
$this->logCountsProcessed = $logCountsProcessed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\LeadBundle\Entity\TimelineTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Summary>
|
||||
*/
|
||||
class SummaryRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
use ContactLimiterTrait;
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 's';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int|string, array<int|string, int|string>>
|
||||
*/
|
||||
public function getCampaignLogCounts(
|
||||
int $campaignId,
|
||||
?\DateTimeInterface $dateFrom = null,
|
||||
?\DateTimeInterface $dateTo = null,
|
||||
): array {
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select(
|
||||
[
|
||||
'cs.event_id',
|
||||
'SUM(cs.scheduled_count) as scheduled_count',
|
||||
'SUM(cs.triggered_count) as triggered_count',
|
||||
'SUM(cs.non_action_path_taken_count) as non_action_path_taken_count',
|
||||
'SUM(cs.failed_count) as failed_count',
|
||||
'SUM(cs.log_counts_processed) as log_counts_processed',
|
||||
]
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_summary', 'cs')
|
||||
->where('cs.campaign_id = '.(int) $campaignId)
|
||||
->groupBy('cs.event_id');
|
||||
|
||||
if ($dateFrom && $dateTo) {
|
||||
$q->andWhere('cs.date_triggered BETWEEN FROM_UNIXTIME(:dateFrom) AND FROM_UNIXTIME(:dateTo)')
|
||||
->setParameter('dateFrom', $dateFrom->getTimestamp(), \PDO::PARAM_INT)
|
||||
->setParameter('dateTo', $dateTo->getTimestamp(), \PDO::PARAM_INT);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
// Group by event id
|
||||
foreach ($results as $row) {
|
||||
$return[$row['event_id']] = [
|
||||
0 => (int) $row['non_action_path_taken_count'],
|
||||
1 => (int) $row['triggered_count'] + (int) $row['scheduled_count'],
|
||||
2 => (int) $row['log_counts_processed'],
|
||||
];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the oldest triggered time for back-filling historical data.
|
||||
*/
|
||||
public function getOldestTriggeredDate(): ?\DateTimeInterface
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->select('cs.date_triggered')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_summary', 'cs')
|
||||
->orderBy('cs.date_triggered', 'ASC')
|
||||
->setMaxResults(1);
|
||||
|
||||
$results = $qb->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return isset($results[0]['date_triggered']) ? new \DateTime($results[0]['date_triggered']) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate summary entries for a given time frame.
|
||||
*
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
*/
|
||||
public function summarize(
|
||||
\DateTimeInterface $dateFrom,
|
||||
\DateTimeInterface $dateTo,
|
||||
?int $campaignId = null,
|
||||
?int $eventId = null,
|
||||
): void {
|
||||
$dateFromTsActual = $dateFrom->getTimestamp();
|
||||
$dateToTsActual = $dateTo->getTimestamp();
|
||||
$intervalInSeconds= 3600;
|
||||
|
||||
$dateFromStartWithZeroMinutes = $dateFromTsActual - ($dateFromTsActual % $intervalInSeconds);
|
||||
$numberOfIntervals = ceil(($dateToTsActual - $dateFromStartWithZeroMinutes) / $intervalInSeconds);
|
||||
|
||||
for ($interval = 0; $interval < $numberOfIntervals; ++$interval) {
|
||||
$dateFromTs = date('Y-m-d H:i:s', $dateFromStartWithZeroMinutes + ($interval * $intervalInSeconds));
|
||||
$dateToTs = date('Y-m-d H:i:s', strtotime($dateFromTs) + ($intervalInSeconds - 1));
|
||||
|
||||
$sql = 'INSERT INTO '.MAUTIC_TABLE_PREFIX.'campaign_summary '.
|
||||
' (campaign_id, event_id, date_triggered, scheduled_count, non_action_path_taken_count, failed_count, triggered_count, log_counts_processed) '.
|
||||
' SELECT * FROM (SELECT '.
|
||||
' mclel.campaign_id AS campaign_id, '.
|
||||
' mclel.event_id AS event_id, '.
|
||||
' "'.$dateFromTs.'" AS date_triggered_i, '.
|
||||
' SUM(IF(mclel.is_scheduled = 1 AND mclel.trigger_date > NOW(), 1, 0)) AS scheduled_count_i, '.
|
||||
' SUM(IF(mclel.is_scheduled = 1 AND mclel.trigger_date > NOW(), 0, mclel.non_action_path_taken)) AS non_action_path_taken_count_i, '.
|
||||
' SUM(IF((mclel.is_scheduled = 1 AND mclel.trigger_date > NOW()) OR mclel.non_action_path_taken, 0, mclefl.log_id IS NOT NULL)) AS failed_count_i, '.
|
||||
' SUM(IF((mclel.is_scheduled = 1 AND mclel.trigger_date > NOW()) OR mclel.non_action_path_taken OR mclefl.log_id IS NOT NULL, 0, 1)) AS triggered_count_i, '.
|
||||
' COUNT((SELECT mcl.campaign_id FROM '.MAUTIC_TABLE_PREFIX.'campaign_leads mcl
|
||||
WHERE mcl.campaign_id = mclel.campaign_id
|
||||
AND mclel.lead_id = mcl.lead_id
|
||||
AND mclel.is_scheduled = 0
|
||||
AND mclel.date_triggered IS NOT NULL
|
||||
AND NOT EXISTS(SELECT NULL FROM '.MAUTIC_TABLE_PREFIX.'campaign_lead_event_failed_log mclefl2
|
||||
WHERE mclefl2.log_id = mclel.id AND mclefl2.date_added BETWEEN "'.$dateFromTs.'" AND "'.$dateToTs.'")
|
||||
)) AS log_counts_processed_i '.
|
||||
' FROM '.MAUTIC_TABLE_PREFIX.'campaign_lead_event_log mclel LEFT JOIN '.MAUTIC_TABLE_PREFIX.'campaign_lead_event_failed_log mclefl ON mclefl.log_id = mclel.id '.
|
||||
' WHERE (mclel.date_triggered BETWEEN "'.$dateFromTs.'" AND "'.$dateToTs.'") ';
|
||||
if ($campaignId) {
|
||||
$sql .= ' AND mclel.campaign_id = '.$campaignId;
|
||||
}
|
||||
|
||||
if ($eventId) {
|
||||
$sql .= ' AND mclel.event_id = '.$eventId;
|
||||
}
|
||||
|
||||
$sql .= ' GROUP BY mclel.campaign_id, mclel.event_id) AS `s` '.
|
||||
' ON DUPLICATE KEY UPDATE '.
|
||||
' scheduled_count = s.scheduled_count_i, '.
|
||||
' non_action_path_taken_count = s.non_action_path_taken_count_i, '.
|
||||
' failed_count = s.failed_count_i, '.
|
||||
' triggered_count = s.triggered_count_i, '.
|
||||
' log_counts_processed = s.log_counts_processed_i;';
|
||||
|
||||
$this->getEntityManager()->getConnection()->executeStatement($sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user