Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -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),
],
];
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Mautic\EmailBundle\Stats\Exception;
class InvalidStatHelperException extends \Exception
{
}

View File

@@ -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;
}
}

View File

@@ -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']);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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];
}
}