348 lines
12 KiB
PHP
Executable File
348 lines
12 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Mautic\LeadBundle\Segment;
|
|
|
|
use Mautic\LeadBundle\Entity\LeadList;
|
|
use Mautic\LeadBundle\Segment\Query\ContactSegmentQueryBuilder;
|
|
use Mautic\LeadBundle\Segment\Query\LeadBatchLimiterTrait;
|
|
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
|
|
|
class ContactSegmentService
|
|
{
|
|
use LeadBatchLimiterTrait;
|
|
|
|
public function __construct(
|
|
private ContactSegmentFilterFactory $contactSegmentFilterFactory,
|
|
private ContactSegmentQueryBuilder $contactSegmentQueryBuilder,
|
|
private \Psr\Log\LoggerInterface $logger,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @return array<int,mixed[]>
|
|
*
|
|
* @throws Exception\SegmentQueryException
|
|
* @throws \Doctrine\DBAL\Exception
|
|
*/
|
|
public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters): array
|
|
{
|
|
$segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment, $batchLimiters);
|
|
|
|
if (!count($segmentFilters)) {
|
|
$this->logger->debug('Segment QB: Segment has no filters', ['segmentId' => $segment->getId()]);
|
|
|
|
return [
|
|
$segment->getId() => [
|
|
'count' => '0',
|
|
'maxId' => '0',
|
|
],
|
|
];
|
|
}
|
|
|
|
$qb = $this->getNewSegmentContactsQuery($segment, $batchLimiters);
|
|
|
|
$this->addLeadAndMinMaxLimiters($qb, $batchLimiters, 'leads', 'id');
|
|
|
|
if (!empty($batchLimiters['excludeVisitors'])) {
|
|
$this->excludeVisitors($qb);
|
|
}
|
|
|
|
$qb = $this->contactSegmentQueryBuilder->wrapInCount($qb);
|
|
|
|
$this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]);
|
|
|
|
$result = $this->timedFetch($qb, $segment->getId());
|
|
|
|
return [$segment->getId() => $result];
|
|
}
|
|
|
|
/**
|
|
* @param array|null $batchLimiters for debug purpose only
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function getTotalLeadListLeadsCount(LeadList $segment, ?array $batchLimiters = null): array
|
|
{
|
|
$segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment);
|
|
|
|
if (!count($segmentFilters)) {
|
|
$this->logger->debug('Segment QB: Segment has no filters', ['segmentId' => $segment->getId()]);
|
|
|
|
return [
|
|
$segment->getId() => [
|
|
'count' => '0',
|
|
'maxId' => '0',
|
|
],
|
|
];
|
|
}
|
|
|
|
$qb = $this->getTotalSegmentContactsQuery($segment);
|
|
|
|
if (!empty($batchLimiters['excludeVisitors'])) {
|
|
$this->excludeVisitors($qb);
|
|
}
|
|
|
|
$qb = $this->contactSegmentQueryBuilder->wrapInCount($qb);
|
|
|
|
$this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]);
|
|
|
|
$result = $this->timedFetch($qb, $segment->getId());
|
|
|
|
return [$segment->getId() => $result];
|
|
}
|
|
|
|
/**
|
|
* @param int $limit
|
|
*
|
|
* @return array<int,mixed[]>
|
|
*
|
|
* @throws \Doctrine\DBAL\Exception
|
|
* @throws Exception\SegmentQueryException
|
|
*/
|
|
public function getNewLeadListLeads(LeadList $segment, array $batchLimiters, $limit = 1000): array
|
|
{
|
|
$queryBuilder = $this->getNewLeadListLeadsQueryBuilder($segment, $batchLimiters);
|
|
$queryBuilder->setMaxResults($limit);
|
|
|
|
$result = $this->timedFetchAll($queryBuilder, $segment->getId());
|
|
|
|
return [$segment->getId() => $result];
|
|
}
|
|
|
|
/**
|
|
* @param mixed[] $batchLimiters
|
|
*/
|
|
public function getNewLeadListLeadsQueryBuilder(LeadList $segment, array $batchLimiters, bool $addNewContactsRestrictions = true): QueryBuilder
|
|
{
|
|
$queryBuilder = $this->getNewSegmentContactsQuery($segment, $batchLimiters, $addNewContactsRestrictions);
|
|
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
|
|
|
// Prepend the DISTINCT to the beginning of the select array
|
|
$select = $queryBuilder->getQueryPart('select');
|
|
|
|
// We are removing it because we will have to add it later
|
|
// to make sure it's the first column in the query
|
|
$key = array_search($leadsTableAlias.'.id', $select);
|
|
if (false !== $key) {
|
|
unset($select[$key]);
|
|
}
|
|
|
|
// We only need to use distinct if we join other tables to the leads table
|
|
$join = $queryBuilder->getQueryPart('join');
|
|
$distinct = is_array($join) && (0 < count($join)) ? 'DISTINCT ' : '';
|
|
// Make sure that leads.id is the first column
|
|
array_unshift($select, $distinct.$leadsTableAlias.'.id');
|
|
$queryBuilder->resetQueryPart('select');
|
|
$queryBuilder->select($select);
|
|
|
|
$this->logger->debug('Segment QB: Create Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]);
|
|
|
|
$this->addLeadAndMinMaxLimiters($queryBuilder, $batchLimiters, 'leads', 'id');
|
|
|
|
if (!empty($batchLimiters['dateTime'])) {
|
|
// Only leads in the list at the time of count
|
|
$queryBuilder->andWhere(
|
|
$queryBuilder->expr()->or(
|
|
$queryBuilder->expr()->lte($leadsTableAlias.'.date_added', $queryBuilder->expr()->literal($batchLimiters['dateTime'])),
|
|
$queryBuilder->expr()->isNull($leadsTableAlias.'.date_added')
|
|
)
|
|
);
|
|
}
|
|
|
|
if (!empty($batchLimiters['excludeVisitors'])) {
|
|
$this->excludeVisitors($queryBuilder);
|
|
}
|
|
|
|
return $queryBuilder;
|
|
}
|
|
|
|
/**
|
|
* @throws Exception\SegmentQueryException
|
|
* @throws \Doctrine\DBAL\Exception
|
|
*/
|
|
public function getOrphanedLeadListLeadsCount(LeadList $segment, array $batchLimiters = []): array
|
|
{
|
|
$queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($segment, $batchLimiters);
|
|
$queryBuilder = $this->contactSegmentQueryBuilder->wrapInCount($queryBuilder);
|
|
|
|
$this->logger->debug('Segment QB: Orphan Leads Count SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]);
|
|
|
|
$result = $this->timedFetch($queryBuilder, $segment->getId());
|
|
|
|
return [$segment->getId() => $result];
|
|
}
|
|
|
|
/**
|
|
* @param int|null $limit
|
|
*
|
|
* @throws Exception\SegmentQueryException
|
|
* @throws \Doctrine\DBAL\Exception
|
|
*/
|
|
public function getOrphanedLeadListLeads(LeadList $segment, array $batchLimiters = [], $limit = null): array
|
|
{
|
|
$queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($segment, $batchLimiters, $limit);
|
|
|
|
$this->logger->debug('Segment QB: Orphan Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]);
|
|
|
|
$result = $this->timedFetchAll($queryBuilder, $segment->getId());
|
|
|
|
return [$segment->getId() => $result];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $batchLimiters
|
|
*
|
|
* @throws Exception\SegmentQueryException
|
|
* @throws \Exception
|
|
*/
|
|
private function getNewSegmentContactsQuery(LeadList $segment, array $batchLimiters = [], bool $addNewContactsRestrictions = true): QueryBuilder
|
|
{
|
|
$queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder(
|
|
$segment->getId(),
|
|
$this->contactSegmentFilterFactory->getSegmentFilters($segment, $batchLimiters)
|
|
);
|
|
|
|
if ($addNewContactsRestrictions) {
|
|
$queryBuilder = $this->contactSegmentQueryBuilder->addNewContactsRestrictions($queryBuilder, (int) $segment->getId(), $batchLimiters);
|
|
}
|
|
|
|
$this->contactSegmentQueryBuilder->queryBuilderGenerated($segment, $queryBuilder);
|
|
|
|
return $queryBuilder;
|
|
}
|
|
|
|
/**
|
|
* @throws Exception\SegmentQueryException
|
|
* @throws \Exception
|
|
*/
|
|
private function getTotalSegmentContactsQuery(LeadList $segment): QueryBuilder
|
|
{
|
|
$segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment);
|
|
|
|
$queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segment->getId(), $segmentFilters);
|
|
$queryBuilder = $this->contactSegmentQueryBuilder->addManuallySubscribedQuery($queryBuilder, $segment->getId());
|
|
|
|
return $this->contactSegmentQueryBuilder->addManuallyUnsubscribedQuery($queryBuilder, $segment->getId());
|
|
}
|
|
|
|
/**
|
|
* @param int|null $limit
|
|
*
|
|
* @return QueryBuilder
|
|
*
|
|
* @throws Exception\SegmentQueryException
|
|
* @throws \Doctrine\DBAL\Exception
|
|
*/
|
|
public function getOrphanedLeadListLeadsQueryBuilder(LeadList $segment, array $batchLimiters = [], $limit = null)
|
|
{
|
|
$segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment, $batchLimiters);
|
|
|
|
$queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segment->getId(), $segmentFilters);
|
|
|
|
$this->addLeadAndMinMaxLimiters($queryBuilder, $batchLimiters, 'leads', 'id');
|
|
|
|
$this->contactSegmentQueryBuilder->queryBuilderGenerated($segment, $queryBuilder);
|
|
|
|
$expr = $queryBuilder->expr();
|
|
$qbO = $queryBuilder->createQueryBuilder();
|
|
$qbO->select('orp.lead_id as id, orp.leadlist_id');
|
|
$qbO->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp');
|
|
$qbO->setParameters($queryBuilder->getParameters(), $queryBuilder->getParameterTypes());
|
|
$qbO->andWhere($expr->eq('orp.leadlist_id', ':orpsegid'));
|
|
$qbO->andWhere($expr->eq('orp.manually_added', $expr->literal(0)));
|
|
$qbO->andWhere($expr->notIn('orp.lead_id', $queryBuilder->getSQL()));
|
|
$qbO->setParameter('orpsegid', $segment->getId());
|
|
$this->addLeadAndMinMaxLimiters($qbO, $batchLimiters, 'lead_lists_leads');
|
|
|
|
if ($limit) {
|
|
$qbO->setMaxResults((int) $limit);
|
|
}
|
|
|
|
return $qbO;
|
|
}
|
|
|
|
private function excludeVisitors(QueryBuilder $queryBuilder): void
|
|
{
|
|
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
|
$queryBuilder->andWhere($queryBuilder->expr()->isNotNull($leadsTableAlias.'.date_identified'));
|
|
}
|
|
|
|
/***** DEBUG *****/
|
|
|
|
/**
|
|
* Formatting helper.
|
|
*
|
|
* @return string
|
|
*/
|
|
private function formatPeriod($inputSeconds)
|
|
{
|
|
$now = \DateTime::createFromFormat('U.u', number_format($inputSeconds, 6, '.', ''));
|
|
|
|
return $now->format('H:i:s.u');
|
|
}
|
|
|
|
/**
|
|
* @param int $segmentId
|
|
*
|
|
* @return mixed
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
private function timedFetch(QueryBuilder $qb, $segmentId)
|
|
{
|
|
try {
|
|
$start = microtime(true);
|
|
|
|
$result = $qb->executeQuery()->fetchAssociative();
|
|
|
|
$end = microtime(true) - $start;
|
|
|
|
$this->logger->debug('Segment QB: Query took: '.$this->formatPeriod($end).', Result count: '.count($result), ['segmentId' => $segmentId]);
|
|
} catch (\Exception $e) {
|
|
$this->logger->error(
|
|
'Segment QB: Query Exception: '.$e->getMessage(),
|
|
[
|
|
'query' => $qb->getSQL(),
|
|
'parameters' => $qb->getParameters(),
|
|
]
|
|
);
|
|
throw $e;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param int $segmentId
|
|
*
|
|
* @return mixed
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
private function timedFetchAll(QueryBuilder $qb, $segmentId)
|
|
{
|
|
try {
|
|
$start = microtime(true);
|
|
$result = $qb->executeQuery()->fetchAllAssociative();
|
|
|
|
$end = microtime(true) - $start;
|
|
|
|
$this->logger->debug(
|
|
'Segment QB: Query took: '.$this->formatPeriod($end).'ms. Result count: '.count($result),
|
|
['segmentId' => $segmentId]
|
|
);
|
|
} catch (\Exception $e) {
|
|
$this->logger->error(
|
|
'Segment QB: Query Exception: '.$e->getMessage(),
|
|
[
|
|
'query' => $qb->getSQL(),
|
|
'parameters' => $qb->getParameters(),
|
|
]
|
|
);
|
|
throw $e;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|