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