Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Stats;
|
||||
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\FormBundle\Model\ActionModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\PointBundle\Model\PointModel;
|
||||
use Mautic\PointBundle\Model\TriggerEventModel;
|
||||
use Mautic\ReportBundle\Model\ReportModel;
|
||||
|
||||
class EmailDependencies
|
||||
{
|
||||
public function __construct(
|
||||
private CampaignModel $campaignModel,
|
||||
private ListModel $listModel,
|
||||
private ActionModel $actionModel,
|
||||
private PointModel $pointModel,
|
||||
private TriggerEventModel $triggerEventModel,
|
||||
private ReportModel $reportModel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getChannelsIds(int $emailId): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => 'mautic.campaign.campaigns',
|
||||
'route' => 'mautic_campaign_index',
|
||||
'ids' => $this->campaignModel->getCampaignIdsWithDependenciesOnEmail($emailId),
|
||||
],
|
||||
[
|
||||
'label' => 'mautic.lead.lead.lists',
|
||||
'route' => 'mautic_segment_index',
|
||||
'ids' => $this->listModel->getSegmentIdsWithDependenciesOnEmail($emailId),
|
||||
],
|
||||
[
|
||||
'label' => 'mautic.form.forms',
|
||||
'route' => 'mautic_form_index',
|
||||
'ids' => $this->actionModel->getFormsIdsWithDependenciesOnEmail($emailId),
|
||||
],
|
||||
[
|
||||
'label' => 'mautic.point.actions.header.index',
|
||||
'route' => 'mautic_point_index',
|
||||
'ids' => $this->pointModel->getPointActionIdsWithDependenciesOnEmail($emailId),
|
||||
],
|
||||
[
|
||||
'label' => 'mautic.point.trigger.header.index',
|
||||
'route' => 'mautic_pointtrigger_index',
|
||||
'ids' => $this->triggerEventModel->getPointTriggerIdsWithDependenciesOnEmail($emailId),
|
||||
],
|
||||
[
|
||||
'label' => 'mautic.report.reports',
|
||||
'route' => 'mautic_report_index',
|
||||
'ids' => $this->reportModel->getReportsIdsWithDependenciesOnEmail($emailId),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Stats;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class EmailPeriodMetrics
|
||||
{
|
||||
private const CAMPAIGN_EVENT_SOURCE = 'campaign.event';
|
||||
private const EMAIL_SOURCE = 'email';
|
||||
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $eventsIds
|
||||
*
|
||||
* @return array<int, array<string, int|string>>
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function emailMetricsPerWeekdayByCampaignEvents(array $eventsIds, \DateTimeImmutable $dateFrom, \DateTimeImmutable $dateTo, string $timezoneOffset): array
|
||||
{
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
$daysSubQuery = $this->createDaysSubQuery();
|
||||
|
||||
$queryBuilder
|
||||
->select(
|
||||
'd.day',
|
||||
'IFNULL(s.sent_count, 0) AS sent_count',
|
||||
'IFNULL(r.read_count, 0) AS read_count',
|
||||
'IFNULL(c.hit_count, 0) AS hit_count'
|
||||
)
|
||||
->from("({$daysSubQuery->getSQL()})", 'd')
|
||||
->leftJoin('d', "({$this->createClicksSubQuery()->getSQL()})", 'c', 'c.hit_day = d.day')
|
||||
->leftJoin('d', "({$this->createSentSubQuery()->getSQL()})", 's', 's.sent_day = d.day')
|
||||
->leftJoin('d', "({$this->createReadSubQuery()->getSQL()})", 'r', 'r.read_day = d.day')
|
||||
->setParameter('source_ids', $eventsIds, ArrayParameterType::INTEGER)
|
||||
->setParameter('timezoneOffset', $timezoneOffset)
|
||||
->setParameter('dateFrom', $dateFrom->format(DateTimeHelper::FORMAT_DB))
|
||||
->setParameter('dateTo', $dateTo->setTime(23, 59, 59)->format(DateTimeHelper::FORMAT_DB))
|
||||
->setParameter('email_source', self::EMAIL_SOURCE)
|
||||
->setParameter('campaign_event_source', self::CAMPAIGN_EVENT_SOURCE)
|
||||
->orderBy('d.day');
|
||||
|
||||
return $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $eventsIds
|
||||
*
|
||||
* @return array<int, array<string, int|string>>
|
||||
*/
|
||||
public function emailMetricsPerHourByCampaignEvents(array $eventsIds, \DateTimeImmutable $dateFrom, \DateTimeImmutable $dateTo, string $timezoneOffset): array
|
||||
{
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
$hoursSubQuery = $this->createHoursSubQuery();
|
||||
|
||||
$queryBuilder
|
||||
->select(
|
||||
'h.hour',
|
||||
'IFNULL(s.sent_count, 0) AS sent_count',
|
||||
'IFNULL(r.read_count, 0) AS read_count',
|
||||
'IFNULL(c.hit_count, 0) AS hit_count'
|
||||
)
|
||||
->from("({$hoursSubQuery->getSQL()})", 'h')
|
||||
->leftJoin('h', "({$this->createClicksHourlySubQuery()->getSQL()})", 'c', 'c.hit_hour = h.hour')
|
||||
->leftJoin('h', "({$this->createSentHourlySubQuery()->getSQL()})", 's', 's.sent_hour = h.hour')
|
||||
->leftJoin('h', "({$this->createReadHourlySubQuery()->getSQL()})", 'r', 'r.read_hour = h.hour')
|
||||
->setParameter('source_ids', $eventsIds, ArrayParameterType::INTEGER)
|
||||
->setParameter('timezoneOffset', $timezoneOffset)
|
||||
->setParameter('format', '%H')
|
||||
->setParameter('dateFrom', $dateFrom->format(DateTimeHelper::FORMAT_DB))
|
||||
->setParameter('dateTo', $dateTo->setTime(23, 59, 59)->format('Y-m-d H:i:s'))
|
||||
->setParameter('email_source', self::EMAIL_SOURCE)
|
||||
->setParameter('campaign_event_source', self::CAMPAIGN_EVENT_SOURCE)
|
||||
->orderBy('h.hour');
|
||||
|
||||
return $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
private function createClicksSubQuery(): QueryBuilder
|
||||
{
|
||||
return $this->connection->createQueryBuilder()
|
||||
->select(
|
||||
'WEEKDAY(TIMESTAMPADD(SECOND, :timezoneOffset, ph.date_hit)) AS hit_day',
|
||||
'COUNT(DISTINCT ph.id) AS hit_count'
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 'es')
|
||||
->join('es', MAUTIC_TABLE_PREFIX.'page_hits', 'ph', 'es.lead_id = ph.lead_id')
|
||||
->join('es', MAUTIC_TABLE_PREFIX.'channel_url_trackables', 'cut', 'cut.channel_id = es.email_id AND cut.redirect_id = ph.redirect_id')
|
||||
->where('ph.date_hit BETWEEN :dateFrom AND :dateTo')
|
||||
->andWhere('ph.source = :email_source')
|
||||
->andWhere('cut.channel = :email_source')
|
||||
->andWhere('es.source = :campaign_event_source')
|
||||
->andWhere('es.source_id IN (:source_ids)')
|
||||
->groupBy('hit_day');
|
||||
}
|
||||
|
||||
private function createSentSubQuery(): QueryBuilder
|
||||
{
|
||||
return $this->createBasicStatsSubQuery('date_sent', 'sent_day', 'sent_count');
|
||||
}
|
||||
|
||||
private function createReadSubQuery(): QueryBuilder
|
||||
{
|
||||
return $this->createBasicStatsSubQuery('date_read', 'read_day', 'read_count')
|
||||
->andWhere('es.is_read = 1');
|
||||
}
|
||||
|
||||
private function createClicksHourlySubQuery(): QueryBuilder
|
||||
{
|
||||
return $this->connection->createQueryBuilder()
|
||||
->select(
|
||||
'TIME_FORMAT(TIMESTAMPADD(SECOND, :timezoneOffset, ph.date_hit), :format) AS hit_hour',
|
||||
'COUNT(DISTINCT ph.id) AS hit_count'
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 'es')
|
||||
->join('es', MAUTIC_TABLE_PREFIX.'page_hits', 'ph', 'es.lead_id = ph.lead_id')
|
||||
->join('es', MAUTIC_TABLE_PREFIX.'channel_url_trackables', 'cut', 'cut.channel_id = es.email_id AND cut.redirect_id = ph.redirect_id')
|
||||
->where('ph.date_hit BETWEEN :dateFrom AND :dateTo')
|
||||
->andWhere('ph.source = :email_source')
|
||||
->andWhere('cut.channel = :email_source')
|
||||
->andWhere('es.source = :campaign_event_source')
|
||||
->andWhere('es.source_id IN (:source_ids)')
|
||||
->groupBy('hit_hour')
|
||||
->orderBy('hit_hour', 'ASC');
|
||||
}
|
||||
|
||||
private function createSentHourlySubQuery(): QueryBuilder
|
||||
{
|
||||
return $this->createBasicHourlyStatsSubQuery('date_sent', 'sent_hour', 'sent_count');
|
||||
}
|
||||
|
||||
private function createReadHourlySubQuery(): QueryBuilder
|
||||
{
|
||||
return $this->createBasicHourlyStatsSubQuery('date_read', 'read_hour', 'read_count')
|
||||
->andWhere('es.is_read = 1');
|
||||
}
|
||||
|
||||
private function createBasicStatsSubQuery(string $dateColumn, string $groupByAlias, string $countAlias): QueryBuilder
|
||||
{
|
||||
return $this->connection->createQueryBuilder()
|
||||
->select(
|
||||
"WEEKDAY(TIMESTAMPADD(SECOND, :timezoneOffset, $dateColumn)) AS $groupByAlias",
|
||||
"COUNT(id) AS $countAlias"
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 'es')
|
||||
->where("es.$dateColumn IS NOT NULL")
|
||||
->andWhere("es.$dateColumn BETWEEN :dateFrom AND :dateTo")
|
||||
->andWhere('es.source = :campaign_event_source')
|
||||
->andWhere('es.source_id IN (:source_ids)')
|
||||
->groupBy($groupByAlias)
|
||||
->orderBy($groupByAlias);
|
||||
}
|
||||
|
||||
private function createBasicHourlyStatsSubQuery(string $dateColumn, string $groupByAlias, string $countAlias): QueryBuilder
|
||||
{
|
||||
return $this->connection->createQueryBuilder()
|
||||
->select(
|
||||
"TIME_FORMAT(TIMESTAMPADD(SECOND, :timezoneOffset, $dateColumn), :format) AS $groupByAlias",
|
||||
"COUNT(id) AS $countAlias"
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 'es')
|
||||
->where("es.$dateColumn IS NOT NULL")
|
||||
->andWhere("es.$dateColumn BETWEEN :dateFrom AND :dateTo")
|
||||
->andWhere('es.source = :campaign_event_source')
|
||||
->andWhere('es.source_id IN (:source_ids)')
|
||||
->groupBy($groupByAlias)
|
||||
->orderBy($groupByAlias, 'ASC')
|
||||
->setMaxResults(24);
|
||||
}
|
||||
|
||||
private function createDaysSubQuery(): QueryBuilder
|
||||
{
|
||||
$subQuery = $this->connection->createQueryBuilder();
|
||||
$daysQuery = '0 AS day';
|
||||
for ($i = 1; $i < 7; ++$i) {
|
||||
$daysQuery .= sprintf(' UNION ALL SELECT %d AS day', $i);
|
||||
}
|
||||
|
||||
return $subQuery->select($daysQuery);
|
||||
}
|
||||
|
||||
private function createHoursSubQuery(): QueryBuilder
|
||||
{
|
||||
$subQuery = $this->connection->createQueryBuilder();
|
||||
$hoursString = '00 AS hour';
|
||||
for ($i = 1; $i < 24; ++$i) {
|
||||
$hoursString .= sprintf(' UNION ALL SELECT %02d AS hour', $i);
|
||||
}
|
||||
|
||||
return $subQuery->select($hoursString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\Exception;
|
||||
|
||||
class InvalidStatHelperException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\FetchOptions;
|
||||
|
||||
use Mautic\StatsBundle\Event\Options\FetchOptions;
|
||||
|
||||
class EmailStatOptions extends FetchOptions
|
||||
{
|
||||
private array $ids = [];
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $companyId;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $campaignId;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $segmentId;
|
||||
|
||||
private array $filters = [];
|
||||
|
||||
private bool $canViewOthers = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $unit;
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setEmailIds(array $ids)
|
||||
{
|
||||
$this->ids = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getEmailIds()
|
||||
{
|
||||
return $this->ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getCompanyId()
|
||||
{
|
||||
return $this->companyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $companyId
|
||||
*/
|
||||
public function setCompanyId($companyId): self
|
||||
{
|
||||
$this->companyId = $companyId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getCampaignId()
|
||||
{
|
||||
return $this->campaignId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $campaignId
|
||||
*/
|
||||
public function setCampaignId($campaignId): self
|
||||
{
|
||||
$this->campaignId = $campaignId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getSegmentId()
|
||||
{
|
||||
return $this->segmentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $segmentId
|
||||
*/
|
||||
public function setSegmentId($segmentId): self
|
||||
{
|
||||
$this->segmentId = $segmentId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFilters()
|
||||
{
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function setFilters(array $filters): self
|
||||
{
|
||||
$this->filters = $filters;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function canViewOthers(): bool
|
||||
{
|
||||
return $this->canViewOthers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $canViewOthers
|
||||
*/
|
||||
public function setCanViewOthers($canViewOthers): self
|
||||
{
|
||||
$this->canViewOthers = $canViewOthers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUnit()
|
||||
{
|
||||
return $this->unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $unit
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setUnit($unit)
|
||||
{
|
||||
$this->unit = $unit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\Helper;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Doctrine\Provider\GeneratedColumnsProviderInterface;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\DateRangeUnitTrait;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\EmailBundle\Stats\FetchOptions\EmailStatOptions;
|
||||
use Mautic\StatsBundle\Aggregate\Collection\StatCollection;
|
||||
use Mautic\StatsBundle\Aggregate\Collector;
|
||||
|
||||
abstract class AbstractHelper implements StatHelperInterface
|
||||
{
|
||||
use FilterTrait;
|
||||
use DateRangeUnitTrait;
|
||||
|
||||
public function __construct(
|
||||
private Collector $collector,
|
||||
Connection $connection,
|
||||
protected GeneratedColumnsProviderInterface $generatedColumnsProvider,
|
||||
private UserHelper $userHelper,
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function fetchStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options)
|
||||
{
|
||||
$statCollection = $this->collector->fetchStats($this->getName(), $fromDateTime, $toDateTime, $options);
|
||||
$calculator = $statCollection->getCalculator($fromDateTime, $toDateTime);
|
||||
|
||||
$stats = match ($this->getTimeUnitFromDateRange($fromDateTime, $toDateTime)) {
|
||||
'Y' => $calculator->getSumsByYear(),
|
||||
'm' => $calculator->getSumsByMonth(),
|
||||
'W' => $calculator->getSumsByWeek(),
|
||||
'd' => $calculator->getSumsByDay(),
|
||||
default => $calculator->getCountsByHour(),
|
||||
};
|
||||
|
||||
// Chart.js only care about the values
|
||||
return array_values($stats->getStats());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ChartQuery
|
||||
*/
|
||||
protected function getQuery(\DateTime $fromDateTime, \DateTime $toDateTime)
|
||||
{
|
||||
$unit = $this->getTimeUnitFromDateRange($fromDateTime, $toDateTime);
|
||||
|
||||
if ('W' == $unit) { // We won't support week storage, we will store it by date
|
||||
$unit = 'd';
|
||||
}
|
||||
$query = new ChartQuery($this->connection, $fromDateTime, $toDateTime, $unit);
|
||||
|
||||
$query->setGeneratedColumnProvider($this->generatedColumnsProvider);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the email table and limits created_by to currently logged in user.
|
||||
*
|
||||
* @param string $emailIdColumn
|
||||
*/
|
||||
protected function limitQueryToCreator(QueryBuilder $q, $emailIdColumn = 't.email_id')
|
||||
{
|
||||
$q->join('t', MAUTIC_TABLE_PREFIX.'emails', 'e', 'e.id = '.$emailIdColumn)
|
||||
->andWhere('e.created_by = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $column
|
||||
* @param string $prefix
|
||||
*/
|
||||
protected function limitQueryToEmailIds(QueryBuilder $q, array $ids, $column, $prefix)
|
||||
{
|
||||
if (0 === count($ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (1 === count($ids)) {
|
||||
$q->andWhere("$prefix.$column = :email_id");
|
||||
$q->setParameter('email_id', $ids[0]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$q->andWhere("$prefix.$column IN (:email_ids)");
|
||||
$q->setParameter('email_ids', $ids, ArrayParameterType::INTEGER);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function fetchAndBindToCollection(QueryBuilder $q, StatCollection $statCollection)
|
||||
{
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
foreach ($results as $result) {
|
||||
$statCollection->addStatByDateTimeStringInUTC($result['date'], $result['count']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\Helper;
|
||||
|
||||
use Mautic\EmailBundle\Stats\FetchOptions\EmailStatOptions;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\StatsBundle\Aggregate\Collection\StatCollection;
|
||||
|
||||
class BouncedHelper extends AbstractHelper
|
||||
{
|
||||
public const NAME = 'email-bounced';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function generateStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options, StatCollection $statCollection): void
|
||||
{
|
||||
$query = $this->getQuery($fromDateTime, $toDateTime);
|
||||
$q = $query->prepareTimeDataQuery('lead_donotcontact', 'date_added');
|
||||
|
||||
$q->andWhere('t.channel = :channel')
|
||||
->setParameter('channel', 'email')
|
||||
->andWhere($q->expr()->eq('t.reason', ':reason'))
|
||||
->setParameter('reason', DoNotContact::BOUNCED);
|
||||
|
||||
$this->limitQueryToEmailIds($q, $options->getEmailIds(), 'channel_id', 't');
|
||||
|
||||
$q->join('t', MAUTIC_TABLE_PREFIX.'email_stats', 'es', 't.channel_id = es.email_id AND t.channel = \'email\' AND t.lead_id = es.lead_id');
|
||||
|
||||
if (!$options->canViewOthers()) {
|
||||
$this->limitQueryToCreator($q, 'es.email_id');
|
||||
}
|
||||
$this->addCompanyFilter($q, $options->getCompanyId());
|
||||
$this->addCampaignFilter($q, $options->getCampaignId(), 'es');
|
||||
$this->addSegmentFilter($q, $options->getSegmentId(), 'es');
|
||||
|
||||
$this->fetchAndBindToCollection($q, $statCollection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\Helper;
|
||||
|
||||
use Mautic\EmailBundle\Stats\FetchOptions\EmailStatOptions;
|
||||
use Mautic\StatsBundle\Aggregate\Collection\StatCollection;
|
||||
|
||||
class ClickedHelper extends AbstractHelper
|
||||
{
|
||||
public const NAME = 'email-clicked';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function generateStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options, StatCollection $statCollection): void
|
||||
{
|
||||
$query = $this->getQuery($fromDateTime, $toDateTime);
|
||||
$q = $query->prepareTimeDataQuery('page_hits', 'date_hit', [], 'DISTINCT t.email_id, t.redirect_id, t.lead_id');
|
||||
|
||||
if ($segmentId = $options->getSegmentId()) {
|
||||
$q->innerJoin(
|
||||
't',
|
||||
'(SELECT DISTINCT email_id, lead_id FROM '.MAUTIC_TABLE_PREFIX.'email_stats WHERE list_id = :segmentId)',
|
||||
'es',
|
||||
't.source_id = es.email_id'
|
||||
);
|
||||
$q->setParameter('segmentId', $segmentId);
|
||||
}
|
||||
|
||||
$q->andWhere('t.source = :source');
|
||||
$q->setParameter('source', 'email');
|
||||
|
||||
$this->limitQueryToEmailIds($q, $options->getEmailIds(), 'source_id', 't');
|
||||
|
||||
if (!$options->canViewOthers()) {
|
||||
$this->limitQueryToCreator($q);
|
||||
}
|
||||
|
||||
$this->addCompanyFilter($q, $options->getCompanyId());
|
||||
$this->addCampaignFilterForEmailSource($q, $options->getCampaignId());
|
||||
$this->addSegmentFilter($q, $segmentId, 'es');
|
||||
|
||||
$this->fetchAndBindToCollection($q, $statCollection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\Helper;
|
||||
|
||||
use Mautic\EmailBundle\Stats\FetchOptions\EmailStatOptions;
|
||||
use Mautic\StatsBundle\Aggregate\Collection\StatCollection;
|
||||
|
||||
class FailedHelper extends AbstractHelper
|
||||
{
|
||||
public const NAME = 'email-failed';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function generateStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options, StatCollection $statCollection): void
|
||||
{
|
||||
$query = $this->getQuery($fromDateTime, $toDateTime);
|
||||
$q = $query->prepareTimeDataQuery('email_stats', 'date_sent', $options->getFilters());
|
||||
|
||||
$this->limitQueryToEmailIds($q, $options->getEmailIds(), 'email_id', 't');
|
||||
|
||||
if (!$options->canViewOthers()) {
|
||||
$this->limitQueryToCreator($q);
|
||||
}
|
||||
|
||||
$q->andWhere($q->expr()->eq('t.is_failed', ':true'))
|
||||
->setParameter('true', true, 'boolean');
|
||||
|
||||
$this->addCompanyFilter($q, $options->getCompanyId());
|
||||
$this->addCampaignFilter($q, $options->getCampaignId());
|
||||
$this->addSegmentFilter($q, $options->getSegmentId());
|
||||
|
||||
$this->fetchAndBindToCollection($q, $statCollection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\Helper;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
trait FilterTrait
|
||||
{
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* @param int|null $companyId
|
||||
* @param string $fromAlias
|
||||
*/
|
||||
protected function addCompanyFilter(QueryBuilder $q, $companyId = null, $fromAlias = 't')
|
||||
{
|
||||
if (null !== $companyId && intval($companyId)) {
|
||||
$sb = $this->connection->createQueryBuilder();
|
||||
|
||||
$sb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
|
||||
->where(
|
||||
$sb->expr()->and(
|
||||
$sb->expr()->eq('cl.company_id', ':companyId'),
|
||||
$sb->expr()->eq('cl.lead_id', $fromAlias.'.lead_id')
|
||||
)
|
||||
);
|
||||
|
||||
$q->andWhere(
|
||||
sprintf('EXISTS (%s)', $sb->getSql())
|
||||
)->setParameter('companyId', $companyId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $campaignId
|
||||
* @param string $fromAlias
|
||||
*/
|
||||
protected function addCampaignFilter(QueryBuilder $q, $campaignId = null, $fromAlias = 't')
|
||||
{
|
||||
if (null !== $campaignId && intval($campaignId)) {
|
||||
$q->innerJoin($fromAlias, '(SELECT DISTINCT event_id, lead_id FROM '.MAUTIC_TABLE_PREFIX.'campaign_lead_event_log WHERE campaign_id = :campaignId)', 'clel', $fromAlias.'.source_id = clel.event_id AND '.$fromAlias.'.source = "campaign.event" AND '.$fromAlias.'.lead_id = clel.lead_id')
|
||||
->setParameter('campaignId', $campaignId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $campaignId
|
||||
* @param string $fromAlias
|
||||
*/
|
||||
protected function addCampaignFilterForEmailSource(QueryBuilder $q, $campaignId = null, $fromAlias = 't')
|
||||
{
|
||||
if (null !== $campaignId && intval($campaignId)) {
|
||||
$q->innerJoin($fromAlias, '(SELECT DISTINCT channel_id, lead_id FROM '.MAUTIC_TABLE_PREFIX.'campaign_lead_event_log WHERE campaign_id = :campaignId AND channel = "email")', 'clel', $fromAlias.'.source_id = clel.channel_id AND '.$fromAlias.'.source = "email" AND '.$fromAlias.'.lead_id = clel.lead_id')
|
||||
->setParameter('campaignId', $campaignId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $segmentId
|
||||
* @param string $fromAlias
|
||||
*/
|
||||
protected function addSegmentFilter(QueryBuilder $q, $segmentId = null, $fromAlias = 't')
|
||||
{
|
||||
if (null !== $segmentId && intval($segmentId)) {
|
||||
$sb = $this->connection->createQueryBuilder();
|
||||
|
||||
$sb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll')
|
||||
->where(
|
||||
$sb->expr()->and(
|
||||
$sb->expr()->eq('lll.leadlist_id', ':segmentId'),
|
||||
$sb->expr()->eq('lll.lead_id', $fromAlias.'.lead_id'),
|
||||
$sb->expr()->eq('lll.manually_removed', 0)
|
||||
)
|
||||
);
|
||||
|
||||
$q->andWhere(
|
||||
sprintf('EXISTS (%s)', $sb->getSql())
|
||||
)->setParameter('segmentId', $segmentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\Helper;
|
||||
|
||||
use Mautic\EmailBundle\Stats\FetchOptions\EmailStatOptions;
|
||||
use Mautic\StatsBundle\Aggregate\Collection\StatCollection;
|
||||
|
||||
class OpenedHelper extends AbstractHelper
|
||||
{
|
||||
public const NAME = 'email-opened';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function generateStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options, StatCollection $statCollection): void
|
||||
{
|
||||
$query = $this->getQuery($fromDateTime, $toDateTime);
|
||||
$q = $query->prepareTimeDataQuery('email_stats', 'date_read', $options->getFilters());
|
||||
|
||||
$this->limitQueryToEmailIds($q, $options->getEmailIds(), 'email_id', 't');
|
||||
|
||||
if (!$options->canViewOthers()) {
|
||||
$this->limitQueryToCreator($q);
|
||||
}
|
||||
|
||||
$this->addCompanyFilter($q, $options->getCompanyId());
|
||||
$this->addCampaignFilter($q, $options->getCampaignId());
|
||||
$this->addSegmentFilter($q, $options->getSegmentId());
|
||||
|
||||
$this->fetchAndBindToCollection($q, $statCollection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\Helper;
|
||||
|
||||
use Mautic\EmailBundle\Stats\FetchOptions\EmailStatOptions;
|
||||
use Mautic\StatsBundle\Aggregate\Collection\StatCollection;
|
||||
|
||||
class SentHelper extends AbstractHelper
|
||||
{
|
||||
public const NAME = 'email-sent';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function generateStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options, StatCollection $statCollection): void
|
||||
{
|
||||
$useGeneratedColumn = $this->generatedColumnsProvider->generatedColumnsAreSupported() && 'd' == $options->getUnit();
|
||||
$column = $useGeneratedColumn ? 'generated_sent_date' : 'date_sent';
|
||||
$query = $this->getQuery($fromDateTime, $toDateTime);
|
||||
$q = $query->prepareTimeDataQuery('email_stats', $column, $options->getFilters());
|
||||
|
||||
$this->limitQueryToEmailIds($q, $options->getEmailIds(), 'email_id', 't');
|
||||
|
||||
if (!$options->canViewOthers()) {
|
||||
$this->limitQueryToCreator($q);
|
||||
}
|
||||
|
||||
$this->addCompanyFilter($q, $options->getCompanyId());
|
||||
$this->addCampaignFilter($q, $options->getCampaignId());
|
||||
$this->addSegmentFilter($q, $options->getSegmentId());
|
||||
|
||||
$this->fetchAndBindToCollection($q, $statCollection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\Helper;
|
||||
|
||||
use Mautic\EmailBundle\Stats\FetchOptions\EmailStatOptions;
|
||||
use Mautic\StatsBundle\Aggregate\Collection\StatCollection;
|
||||
|
||||
interface StatHelperInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
|
||||
public function fetchStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options);
|
||||
|
||||
public function generateStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options, StatCollection $statCollection);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats\Helper;
|
||||
|
||||
use Mautic\EmailBundle\Stats\FetchOptions\EmailStatOptions;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\StatsBundle\Aggregate\Collection\StatCollection;
|
||||
|
||||
class UnsubscribedHelper extends AbstractHelper
|
||||
{
|
||||
public const NAME = 'email-unsubscribed';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function generateStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options, StatCollection $statCollection): void
|
||||
{
|
||||
$query = $this->getQuery($fromDateTime, $toDateTime);
|
||||
$q = $query->prepareTimeDataQuery('lead_donotcontact', 'date_added');
|
||||
|
||||
$q->andWhere('t.channel = :channel')
|
||||
->setParameter('channel', 'email')
|
||||
->andWhere($q->expr()->eq('t.reason', ':reason'))
|
||||
->setParameter('reason', DoNotContact::UNSUBSCRIBED);
|
||||
|
||||
$this->limitQueryToEmailIds($q, $options->getEmailIds(), 'channel_id', 't');
|
||||
|
||||
$q->join('t', MAUTIC_TABLE_PREFIX.'email_stats', 'es', 't.channel_id = es.email_id AND t.channel = \'email\' AND t.lead_id = es.lead_id');
|
||||
|
||||
if (!$options->canViewOthers()) {
|
||||
$this->limitQueryToCreator($q, 'es.email_id');
|
||||
}
|
||||
$this->addCompanyFilter($q, $options->getCompanyId());
|
||||
$this->addCampaignFilter($q, $options->getCampaignId(), 'es');
|
||||
$this->addSegmentFilter($q, $options->getSegmentId(), 'es');
|
||||
|
||||
$this->fetchAndBindToCollection($q, $statCollection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Stats;
|
||||
|
||||
use Mautic\EmailBundle\Stats\Exception\InvalidStatHelperException;
|
||||
use Mautic\EmailBundle\Stats\Helper\StatHelperInterface;
|
||||
|
||||
class StatHelperContainer
|
||||
{
|
||||
/**
|
||||
* @var array<string, StatHelperInterface>
|
||||
*/
|
||||
private array $helpers = [];
|
||||
|
||||
public function addHelper(StatHelperInterface $helper): void
|
||||
{
|
||||
$this->helpers[$helper->getName()] = $helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidStatHelperException
|
||||
*/
|
||||
public function getHelper($name): StatHelperInterface
|
||||
{
|
||||
if (!isset($this->helpers[$name])) {
|
||||
throw new InvalidStatHelperException($name.' has not been registered');
|
||||
}
|
||||
|
||||
return $this->helpers[$name];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user