Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Event\LeadListFilteringEvent;
|
||||
use Mautic\LeadBundle\Event\LeadListQueryBuilderGeneratedEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilters;
|
||||
use Mautic\LeadBundle\Segment\Exception\PluginHandledFilterException;
|
||||
use Mautic\LeadBundle\Segment\Exception\SegmentQueryException;
|
||||
use Mautic\LeadBundle\Segment\RandomParameterName;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Responsible for building queries for segments.
|
||||
*/
|
||||
class ContactSegmentQueryBuilder
|
||||
{
|
||||
use LeadBatchLimiterTrait;
|
||||
|
||||
/**
|
||||
* @var array Contains segment edges mapping
|
||||
*/
|
||||
private array $dependencyMap = [];
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private RandomParameterName $randomParameterName,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $segmentId
|
||||
* @param ContactSegmentFilters $segmentFilters
|
||||
*
|
||||
* @throws SegmentQueryException
|
||||
*/
|
||||
public function assembleContactsSegmentQueryBuilder($segmentId, $segmentFilters, bool $changeAlias = false): QueryBuilder
|
||||
{
|
||||
/** @var Connection $connection */
|
||||
$connection = $this->entityManager->getConnection();
|
||||
if ($connection instanceof \Doctrine\DBAL\Connections\PrimaryReadReplicaConnection) {
|
||||
// Prefer a replica connection if available.
|
||||
$connection->ensureConnectedToReplica();
|
||||
}
|
||||
|
||||
/** @var QueryBuilder $queryBuilder */
|
||||
$queryBuilder = new QueryBuilder($connection);
|
||||
|
||||
$leadsTableAlias = $changeAlias ? $this->generateRandomParameterName() : Lead::DEFAULT_ALIAS;
|
||||
|
||||
$queryBuilder->select($leadsTableAlias.'.id')->from(MAUTIC_TABLE_PREFIX.'leads', $leadsTableAlias);
|
||||
|
||||
/*
|
||||
* Validate the plan, check for circular dependencies.
|
||||
*
|
||||
* the bigger count($plan), the higher complexity of query
|
||||
*/
|
||||
$this->getResolutionPlan($segmentId);
|
||||
|
||||
$params = $queryBuilder->getParameters();
|
||||
$paramTypes = $queryBuilder->getParameterTypes();
|
||||
|
||||
/** @var ContactSegmentFilter $filter */
|
||||
foreach ($segmentFilters as $filter) {
|
||||
try {
|
||||
$this->dispatchPluginFilteringEvent($filter, $queryBuilder);
|
||||
} catch (PluginHandledFilterException) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$queryBuilder = $filter->applyQuery($queryBuilder);
|
||||
// We need to collect params between union queries in this iteration,
|
||||
// because they are overwritten by new union query build
|
||||
$params = array_merge($params, $queryBuilder->getParameters());
|
||||
$paramTypes = array_merge($paramTypes, $queryBuilder->getParameterTypes());
|
||||
}
|
||||
|
||||
$queryBuilder->setParameters($params, $paramTypes);
|
||||
$queryBuilder->applyStackLogic();
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
*/
|
||||
public function wrapInCount(QueryBuilder $qb): QueryBuilder
|
||||
{
|
||||
/** @var Connection $connection */
|
||||
$connection = $this->entityManager->getConnection();
|
||||
if ($connection instanceof \Doctrine\DBAL\Connections\PrimaryReadReplicaConnection) {
|
||||
// Prefer a replica connection if available.
|
||||
$connection->ensureConnectedToReplica();
|
||||
}
|
||||
|
||||
// Add count functions to the query
|
||||
$queryBuilder = new QueryBuilder($connection);
|
||||
|
||||
// If there is any right join in the query we need to select its it
|
||||
$primary = $qb->guessPrimaryLeadContactIdColumn();
|
||||
|
||||
$currentSelects = [];
|
||||
foreach ($qb->getQueryParts()['select'] as $select) {
|
||||
if ($select != $primary) {
|
||||
$currentSelects[] = $select;
|
||||
}
|
||||
}
|
||||
|
||||
$qb->select('DISTINCT '.$primary.' as leadIdPrimary');
|
||||
foreach ($currentSelects as $select) {
|
||||
$qb->addSelect($select);
|
||||
}
|
||||
|
||||
$queryBuilder->select('count(leadIdPrimary) count, max(leadIdPrimary) maxId, min(leadIdPrimary) minId')
|
||||
->from('('.$qb->getSQL().')', 'sss');
|
||||
|
||||
$queryBuilder->setParameters($qb->getParameters(), $qb->getParameterTypes());
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict the query to NEW members of segment.
|
||||
*
|
||||
* @param array<string, mixed> $batchLimiters
|
||||
*
|
||||
* @throws QueryException
|
||||
*/
|
||||
public function addNewContactsRestrictions(QueryBuilder $queryBuilder, int $segmentId, array $batchLimiters = []): QueryBuilder
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
||||
$expr = $queryBuilder->expr();
|
||||
$tableAlias = $this->generateRandomParameterName();
|
||||
$segmentIdParameter = ":{$tableAlias}segmentId";
|
||||
|
||||
$segmentQueryBuilder = $queryBuilder->createQueryBuilder()
|
||||
->select($tableAlias.'.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias)
|
||||
->andWhere($expr->eq($tableAlias.'.leadlist_id', $segmentIdParameter));
|
||||
|
||||
$queryBuilder->setParameter("{$tableAlias}segmentId", $segmentId);
|
||||
|
||||
$this->addLeadAndMinMaxLimiters($segmentQueryBuilder, $batchLimiters, 'lead_lists_leads');
|
||||
|
||||
$queryBuilder->andWhere($expr->notIn($leadsTableAlias.'.id', $segmentQueryBuilder->getSQL()));
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, int $leadListId): QueryBuilder
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
||||
$tableAlias = $this->generateRandomParameterName();
|
||||
|
||||
$existsQueryBuilder = $queryBuilder->createQueryBuilder();
|
||||
|
||||
$existsQueryBuilder
|
||||
->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias)
|
||||
->andWhere($queryBuilder->expr()->eq($tableAlias.'.leadlist_id', intval($leadListId)))
|
||||
->andWhere(
|
||||
$queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->eq($tableAlias.'.manually_added', 1),
|
||||
$queryBuilder->expr()->eq($tableAlias.'.manually_removed', $queryBuilder->expr()->literal(''))
|
||||
)
|
||||
);
|
||||
|
||||
$existingQueryWherePart = $existsQueryBuilder->getQueryPart('where');
|
||||
$existsQueryBuilder->where("$leadsTableAlias.id = $tableAlias.lead_id");
|
||||
$existsQueryBuilder->andWhere($existingQueryWherePart);
|
||||
|
||||
$queryBuilder->orWhere(
|
||||
$queryBuilder->expr()->exists($existsQueryBuilder->getSQL())
|
||||
);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws QueryException
|
||||
*/
|
||||
public function addManuallyUnsubscribedQuery(QueryBuilder $queryBuilder, int $leadListId): QueryBuilder
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
||||
$tableAlias = $this->generateRandomParameterName();
|
||||
$queryBuilder->leftJoin(
|
||||
$leadsTableAlias,
|
||||
MAUTIC_TABLE_PREFIX.'lead_lists_leads',
|
||||
$tableAlias,
|
||||
$leadsTableAlias.'.id = '.$tableAlias.'.lead_id and '.$tableAlias.'.leadlist_id = '.intval($leadListId)
|
||||
);
|
||||
$queryBuilder->addJoinCondition($tableAlias, $queryBuilder->expr()->eq($tableAlias.'.manually_removed', 1));
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->isNull($tableAlias.'.lead_id'));
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function queryBuilderGenerated(LeadList $segment, QueryBuilder $queryBuilder): void
|
||||
{
|
||||
if (!$this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_QUERYBUILDER_GENERATED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = new LeadListQueryBuilderGeneratedEvent($segment, $queryBuilder);
|
||||
$this->dispatcher->dispatch($event, LeadEvents::LIST_FILTERS_QUERYBUILDER_GENERATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique parameter name.
|
||||
*/
|
||||
private function generateRandomParameterName(): string
|
||||
{
|
||||
return $this->randomParameterName->generateRandomParameterName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PluginHandledFilterException
|
||||
*/
|
||||
private function dispatchPluginFilteringEvent(ContactSegmentFilter $filter, QueryBuilder $queryBuilder): void
|
||||
{
|
||||
if ($this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_ON_FILTERING)) {
|
||||
// This has to run for every filter
|
||||
$filterCrate = $filter->contactSegmentFilterCrate->getArray();
|
||||
|
||||
$alias = $this->generateRandomParameterName();
|
||||
$event = new LeadListFilteringEvent($filterCrate, null, $alias, $filterCrate['operator'], $queryBuilder, $this->entityManager);
|
||||
$this->dispatcher->dispatch($event, LeadEvents::LIST_FILTERS_ON_FILTERING);
|
||||
if ($event->isFilteringDone()) {
|
||||
$queryBuilder->addLogic($event->getSubQuery(), $filter->getGlue());
|
||||
|
||||
throw new PluginHandledFilterException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array with plan for processing.
|
||||
*
|
||||
* @param int $segmentId
|
||||
* @param array $seen
|
||||
* @param array $resolved
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws SegmentQueryException
|
||||
*/
|
||||
private function getResolutionPlan($segmentId, $seen = [], &$resolved = [])
|
||||
{
|
||||
$seen[] = $segmentId;
|
||||
|
||||
if (!isset($this->dependencyMap[$segmentId])) {
|
||||
$this->dependencyMap[$segmentId] = $this->getSegmentEdges($segmentId);
|
||||
}
|
||||
|
||||
$edges = $this->dependencyMap[$segmentId];
|
||||
|
||||
foreach ($edges as $edge) {
|
||||
if (!in_array($edge, $resolved)) {
|
||||
if (in_array($edge, $seen)) {
|
||||
throw new SegmentQueryException('Circular reference detected.');
|
||||
}
|
||||
$this->getResolutionPlan($edge, $seen, $resolved);
|
||||
}
|
||||
}
|
||||
|
||||
$resolved[] = $segmentId;
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $segmentId
|
||||
*/
|
||||
private function getSegmentEdges($segmentId): array
|
||||
{
|
||||
$segment = $this->entityManager->getRepository(LeadList::class)->find($segmentId);
|
||||
if (null === $segment) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$segmentFilters = $segment->getFilters();
|
||||
$segmentEdges = [];
|
||||
|
||||
foreach ($segmentFilters as $segmentFilter) {
|
||||
if (isset($segmentFilter['field']) && 'leadlist' === $segmentFilter['field']) {
|
||||
$bcFilter = $segmentFilter['filter'] ?? [];
|
||||
$filterEdges = $segmentFilter['properties']['filter'] ?? $bcFilter;
|
||||
$segmentEdges = array_merge($segmentEdges, $filterEdges);
|
||||
}
|
||||
}
|
||||
|
||||
return $segmentEdges;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query\Expression;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\ExpressionBuilder as BaseExpressionBuilder;
|
||||
use Mautic\LeadBundle\Segment\Exception\SegmentQueryException;
|
||||
|
||||
class ExpressionBuilder extends BaseExpressionBuilder
|
||||
{
|
||||
public const REGEXP = 'REGEXP';
|
||||
|
||||
public const BETWEEN = 'BETWEEN';
|
||||
|
||||
/**
|
||||
* Creates a between comparison expression.
|
||||
*
|
||||
* @throws SegmentQueryException
|
||||
*/
|
||||
public function between($x, $arr): string
|
||||
{
|
||||
if (!is_array($arr) || 2 != count($arr)) {
|
||||
throw new SegmentQueryException('Between expression expects second argument to be an array with exactly two elements');
|
||||
}
|
||||
|
||||
return $x.' '.self::BETWEEN.' '.$this->comparison($arr[0], 'AND', $arr[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a not between comparison expression.
|
||||
*
|
||||
* First argument is considered the left expression and the second is the right expression.
|
||||
* When converted to string, it will generated a <left expr> = <right expr>. Example:
|
||||
*
|
||||
* [php]
|
||||
* // u.id = ?
|
||||
* $expr->eq('u.id', '?');
|
||||
*
|
||||
* @throws SegmentQueryException
|
||||
*/
|
||||
public function notBetween($x, $arr): string
|
||||
{
|
||||
return 'NOT '.$this->between($x, $arr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an equality comparison expression with the given arguments.
|
||||
*
|
||||
* First argument is considered the left expression and the second is the right expression.
|
||||
* When converted to string, it will generated a <left expr> = <right expr>. Example:
|
||||
*
|
||||
* [php]
|
||||
* // u.id = ?
|
||||
* $expr->eq('u.id', '?');
|
||||
*
|
||||
* @param mixed $x the left expression
|
||||
* @param mixed $y the right expression
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function regexp($x, $y)
|
||||
{
|
||||
return $this->comparison($x, self::REGEXP, $y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an equality comparison expression with the given arguments.
|
||||
*
|
||||
* First argument is considered the left expression and the second is the right expression.
|
||||
* When converted to string, it will generated a <left expr> = <right expr>. Example:
|
||||
*
|
||||
* [php]
|
||||
* // u.id = ?
|
||||
* $expr->eq('u.id', '?');
|
||||
*
|
||||
* @param mixed $x the left expression
|
||||
* @param mixed $y the right expression
|
||||
*/
|
||||
public function notRegexp($x, $y): string
|
||||
{
|
||||
return 'NOT '.$this->comparison($x, self::REGEXP, $y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts argument into EXISTS mysql function.
|
||||
*/
|
||||
public function exists($input): string
|
||||
{
|
||||
return $this->func('EXISTS', $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts argument into NOT EXISTS mysql function.
|
||||
*/
|
||||
public function notExists($input): string
|
||||
{
|
||||
return $this->func('NOT EXISTS', $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a functional expression.
|
||||
*
|
||||
* @param string $func any function to be applied on $x
|
||||
* @param mixed $x the left expression
|
||||
* @param string|array $y the placeholder or the array of values to be used by IN() comparison
|
||||
*/
|
||||
public function func($func, $x, $y = null): string
|
||||
{
|
||||
$functionArguments = func_get_args();
|
||||
$additionArguments = array_splice($functionArguments, 2);
|
||||
|
||||
foreach ($additionArguments as $k=> $v) {
|
||||
$additionArguments[$k] = is_numeric($v) && intval($v) === $v ? $v : $this->literal($v);
|
||||
}
|
||||
|
||||
return $func.'('.$x.(count($additionArguments) ? ', ' : '').join(',', $additionArguments).')';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query\Filter;
|
||||
|
||||
use Mautic\LeadBundle\Event\SegmentOperatorQueryBuilderEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
use Mautic\LeadBundle\Segment\RandomParameterName;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class BaseFilterQueryBuilder implements FilterQueryBuilderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private RandomParameterName $parameterNameGenerator,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getServiceId(): string
|
||||
{
|
||||
return 'mautic.lead.query.builder.basic';
|
||||
}
|
||||
|
||||
public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter): QueryBuilder
|
||||
{
|
||||
// Check if the column exists in the table
|
||||
$filter->getColumn();
|
||||
|
||||
$filterParameters = $filter->getParameterValue();
|
||||
|
||||
if (is_array($filterParameters)) {
|
||||
$parameters = [];
|
||||
foreach ($filterParameters as $filterParameter) {
|
||||
$parameters[] = $this->generateRandomParameterName();
|
||||
}
|
||||
} else {
|
||||
$parameters = $this->generateRandomParameterName();
|
||||
}
|
||||
|
||||
$event = new SegmentOperatorQueryBuilderEvent($queryBuilder, $filter, $filter->getParameterHolder($parameters));
|
||||
$this->dispatcher->dispatch($event, LeadEvents::LIST_FILTERS_OPERATOR_QUERYBUILDER_ON_GENERATE);
|
||||
|
||||
if (!$event->wasOperatorHandled()) {
|
||||
throw new \Exception('Dunno how to handle operator "'.$filter->getOperator().'"');
|
||||
}
|
||||
|
||||
$queryBuilder->setParametersPairs($parameters, $filterParameters);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RandomParameterName $parameterNameGenerator
|
||||
*
|
||||
* @return BaseFilterQueryBuilder
|
||||
*/
|
||||
public function setParameterNameGenerator($parameterNameGenerator)
|
||||
{
|
||||
$this->parameterNameGenerator = $parameterNameGenerator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function generateRandomParameterName(): string
|
||||
{
|
||||
return $this->parameterNameGenerator->generateRandomParameterName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query\Filter;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\Query\LeadBatchLimiterTrait;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
|
||||
class ChannelClickQueryBuilder extends BaseFilterQueryBuilder
|
||||
{
|
||||
use LeadBatchLimiterTrait;
|
||||
|
||||
public static function getServiceId(): string
|
||||
{
|
||||
return 'mautic.lead.query.builder.channel_click.value';
|
||||
}
|
||||
|
||||
public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter): QueryBuilder
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
||||
$filterOperator = $filter->getOperator();
|
||||
$filterChannel = $this->getChannel($filter->getField());
|
||||
$batchLimiters = $filter->getBatchLimiters();
|
||||
$filterParameters = $filter->getParameterValue();
|
||||
|
||||
if (is_array($filterParameters)) {
|
||||
$parameters = [];
|
||||
foreach ($filterParameters as $filterParameter) {
|
||||
$parameters[] = $this->generateRandomParameterName();
|
||||
}
|
||||
} else {
|
||||
$parameters = $this->generateRandomParameterName();
|
||||
}
|
||||
|
||||
$tableAlias = $this->generateRandomParameterName();
|
||||
|
||||
$subQb = $queryBuilder->createQueryBuilder();
|
||||
$expr = $subQb->expr()->and(
|
||||
$subQb->expr()->isNotNull($tableAlias.'.redirect_id'),
|
||||
$subQb->expr()->isNotNull($tableAlias.'.lead_id'),
|
||||
$subQb->expr()->eq($tableAlias.'.source', $subQb->expr()->literal($filterChannel))
|
||||
);
|
||||
|
||||
if ($this->isDateBased($filter->getField())) {
|
||||
$expr = $expr->with(
|
||||
$subQb->expr()->$filterOperator($tableAlias.'.date_hit', $filter->getParameterHolder($parameters))
|
||||
);
|
||||
}
|
||||
|
||||
$subQb->select($tableAlias.'.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits', $tableAlias)
|
||||
->where($expr);
|
||||
|
||||
$this->addLeadAndMinMaxLimiters($subQb, $batchLimiters, 'page_hits');
|
||||
|
||||
if ('empty' === $filterOperator && !$this->isDateBased($filter->getField())) {
|
||||
$queryBuilder->addLogic($queryBuilder->expr()->notIn($leadsTableAlias.'.id', $subQb->getSQL()), $filter->getGlue());
|
||||
} else {
|
||||
$queryBuilder->addLogic($queryBuilder->expr()->in($leadsTableAlias.'.id', $subQb->getSQL()), $filter->getGlue());
|
||||
}
|
||||
|
||||
$queryBuilder->setParametersPairs($parameters, $filterParameters);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function getChannel(string $name): string
|
||||
{
|
||||
if ('email_id' === $name) {
|
||||
// BC for existing filter
|
||||
return 'email';
|
||||
}
|
||||
|
||||
return str_replace(['_clicked_link', '_date'], '', $name);
|
||||
}
|
||||
|
||||
private function isDateBased(string $name): bool
|
||||
{
|
||||
return str_contains($name, '_date');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query\Filter;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
|
||||
/**
|
||||
* Used to connect foreign tables using third table.
|
||||
*
|
||||
* Currently only company decorator uses this functionality but it may be used by plugins in the future
|
||||
*
|
||||
* filter decorator must implement methods:
|
||||
* $filter->getRelationJoinTable()
|
||||
* $filter->getRelationJoinTableField()
|
||||
*
|
||||
* @see \Mautic\LeadBundle\Segment\Decorator\CompanyDecorator
|
||||
*/
|
||||
class ComplexRelationValueFilterQueryBuilder extends BaseFilterQueryBuilder
|
||||
{
|
||||
public static function getServiceId(): string
|
||||
{
|
||||
return 'mautic.lead.query.builder.complex_relation.value';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter): QueryBuilder
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
||||
$filterOperator = $filter->getOperator();
|
||||
|
||||
$filterParameters = $filter->getParameterValue();
|
||||
|
||||
if (is_array($filterParameters)) {
|
||||
$parameters = [];
|
||||
foreach ($filterParameters as $filterParameter) {
|
||||
$parameters[] = $this->generateRandomParameterName();
|
||||
}
|
||||
} else {
|
||||
$parameters = $this->generateRandomParameterName();
|
||||
}
|
||||
|
||||
$filterParametersHolder = $filter->getParameterHolder($parameters);
|
||||
|
||||
$tableAlias = $queryBuilder->getTableAlias($filter->getTable());
|
||||
|
||||
if (!$tableAlias) {
|
||||
$tableAlias = $this->generateRandomParameterName();
|
||||
|
||||
$relTable = $this->generateRandomParameterName();
|
||||
$queryBuilder->leftJoin($leadsTableAlias, $filter->getRelationJoinTable(), $relTable, $relTable.'.lead_id = '.$leadsTableAlias.'.id');
|
||||
$queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.'
|
||||
.$filter->getRelationJoinTableField());
|
||||
}
|
||||
|
||||
switch ($filterOperator) {
|
||||
case 'empty':
|
||||
$expression = new CompositeExpression(CompositeExpression::TYPE_OR,
|
||||
[
|
||||
$queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()),
|
||||
$queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), $queryBuilder->expr()->literal('')),
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'notEmpty':
|
||||
$expression = new CompositeExpression(CompositeExpression::TYPE_AND,
|
||||
[
|
||||
$queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()),
|
||||
$queryBuilder->expr()->neq($tableAlias.'.'.$filter->getField(), $queryBuilder->expr()->literal('')),
|
||||
]
|
||||
);
|
||||
|
||||
break;
|
||||
case 'neq':
|
||||
$expression = $queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()),
|
||||
$queryBuilder->expr()->$filterOperator(
|
||||
$tableAlias.'.'.$filter->getField(),
|
||||
$filterParametersHolder
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'startsWith':
|
||||
case 'endsWith':
|
||||
case 'gt':
|
||||
case 'eq':
|
||||
case 'gte':
|
||||
case 'like':
|
||||
case 'lt':
|
||||
case 'lte':
|
||||
case 'in':
|
||||
case 'between': // Used only for date with week combination (EQUAL [this week, next week, last week])
|
||||
case 'regexp':
|
||||
case 'notRegexp': // Different behaviour from 'notLike' because of BC (do not use condition for NULL). Could be changed in Mautic 3.
|
||||
$expression = $queryBuilder->expr()->$filterOperator(
|
||||
$tableAlias.'.'.$filter->getField(),
|
||||
$filterParametersHolder
|
||||
);
|
||||
break;
|
||||
case 'notLike':
|
||||
case 'notBetween': // Used only for date with week combination (NOT EQUAL [this week, next week, last week])
|
||||
case 'notIn':
|
||||
$expression = $queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->$filterOperator($tableAlias.'.'.$filter->getField(), $filterParametersHolder),
|
||||
$queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField())
|
||||
);
|
||||
break;
|
||||
case 'multiselect':
|
||||
case '!multiselect':
|
||||
$operator = 'multiselect' === $filterOperator ? 'regexp' : 'notRegexp';
|
||||
$expressions = [];
|
||||
foreach ($filterParametersHolder as $parameter) {
|
||||
$expressions[] = $queryBuilder->expr()->$operator($tableAlias.'.'.$filter->getField(), $parameter);
|
||||
}
|
||||
|
||||
$expression = $queryBuilder->expr()->and(...$expressions);
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"');
|
||||
}
|
||||
|
||||
$queryBuilder->addLogic($expression, $filter->getGlue());
|
||||
|
||||
$queryBuilder->setParametersPairs($parameters, $filterParameters);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query\Filter;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\Query\LeadBatchLimiterTrait;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryException;
|
||||
|
||||
class DoNotContactFilterQueryBuilder extends BaseFilterQueryBuilder
|
||||
{
|
||||
use LeadBatchLimiterTrait;
|
||||
|
||||
public static function getServiceId(): string
|
||||
{
|
||||
return 'mautic.lead.query.builder.special.dnc';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws QueryException
|
||||
*/
|
||||
public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter): QueryBuilder
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
||||
$doNotContactParts = $filter->getDoNotContactParts();
|
||||
$batchLimiters = $filter->getBatchLimiters();
|
||||
$expr = $queryBuilder->expr();
|
||||
$queryAlias = $this->generateRandomParameterName();
|
||||
$reasonParameter = "{$queryAlias}reason";
|
||||
$channelParameter = "{$queryAlias}channel";
|
||||
|
||||
$queryBuilder->setParameter($reasonParameter, $doNotContactParts->getParameterType());
|
||||
$queryBuilder->setParameter($channelParameter, $doNotContactParts->getChannel());
|
||||
|
||||
$filterQueryBuilder = $queryBuilder->createQueryBuilder()
|
||||
->select($queryAlias.'.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $queryAlias)
|
||||
->andWhere($expr->eq($queryAlias.'.reason', ':'.$reasonParameter))
|
||||
->andWhere($expr->eq($queryAlias.'.channel', ':'.$channelParameter));
|
||||
|
||||
$this->addLeadAndMinMaxLimiters($filterQueryBuilder, $batchLimiters, 'lead_donotcontact');
|
||||
|
||||
if ('eq' === $filter->getOperator() xor !$filter->getParameterValue()) {
|
||||
$expression = $expr->in($leadsTableAlias.'.id', $filterQueryBuilder->getSQL());
|
||||
} else {
|
||||
$expression = $expr->notIn($leadsTableAlias.'.id', $filterQueryBuilder->getSQL());
|
||||
}
|
||||
|
||||
$queryBuilder->addLogic($expression, $filter->getGlue());
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query\Filter;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
|
||||
interface FilterQueryBuilderInterface
|
||||
{
|
||||
/**
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter);
|
||||
|
||||
/**
|
||||
* @return string returns the service id in the DIC container
|
||||
*/
|
||||
public static function getServiceId();
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query\Filter;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\Exception\FieldNotFoundException;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryException;
|
||||
|
||||
class ForeignFuncFilterQueryBuilder extends BaseFilterQueryBuilder
|
||||
{
|
||||
public static function getServiceId(): string
|
||||
{
|
||||
return 'mautic.lead.query.builder.foreign.func';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FieldNotFoundException
|
||||
* @throws QueryException
|
||||
*/
|
||||
public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter): QueryBuilder
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
||||
$filterOperator = $filter->getOperator();
|
||||
$filterAggr = $filter->getAggregateFunction();
|
||||
|
||||
$filterParameters = $filter->getParameterValue();
|
||||
|
||||
if (is_array($filterParameters)) {
|
||||
$parameters = [];
|
||||
foreach ($filterParameters as $filterParameter) {
|
||||
$parameters[] = $this->generateRandomParameterName();
|
||||
}
|
||||
} else {
|
||||
$parameters = $this->generateRandomParameterName();
|
||||
}
|
||||
|
||||
// Check if the column exists in the table
|
||||
$filter->getColumn();
|
||||
|
||||
$filterParametersHolder = $filter->getParameterHolder($parameters);
|
||||
|
||||
$tableAlias = $queryBuilder->getTableAlias($filter->getTable());
|
||||
|
||||
// for aggregate function we need to create new alias and not reuse the old one
|
||||
if ($filterAggr) {
|
||||
$tableAlias = false;
|
||||
}
|
||||
|
||||
if (!$tableAlias) {
|
||||
$tableAlias = $this->generateRandomParameterName();
|
||||
if ($filterAggr) {
|
||||
// No join needed, it is placed in exist/not exists
|
||||
} else {
|
||||
if ('companies' == $filter->getTable()) {
|
||||
$relTable = $this->generateRandomParameterName();
|
||||
$queryBuilder->leftJoin($leadsTableAlias, MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = '.$leadsTableAlias.'.id');
|
||||
$queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id');
|
||||
} else { // This should never happen
|
||||
$queryBuilder->leftJoin(
|
||||
$leadsTableAlias,
|
||||
$filter->getTable(),
|
||||
$tableAlias,
|
||||
sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch ($filterOperator) {
|
||||
case 'empty':
|
||||
$expression = $queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()),
|
||||
$queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), ':'.$emptyParameter = $this->generateRandomParameterName())
|
||||
);
|
||||
$queryBuilder->setParameter($emptyParameter, '');
|
||||
break;
|
||||
case 'notEmpty':
|
||||
$expression = $queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()),
|
||||
$queryBuilder->expr()->neq($tableAlias.'.'.$filter->getField(), ':'.$emptyParameter = $this->generateRandomParameterName())
|
||||
);
|
||||
$queryBuilder->setParameter($emptyParameter, '');
|
||||
break;
|
||||
default:
|
||||
if ($filterAggr) {
|
||||
if (!is_null($filter)) {
|
||||
if ('sum' === $filterAggr) {
|
||||
$expressionArg = $queryBuilder->expr()->func('COALESCE',
|
||||
$queryBuilder->expr()->func('SUM', $tableAlias.'.'.$filter->getField()),
|
||||
0
|
||||
);
|
||||
$expression = $queryBuilder->expr()->$filterOperator($expressionArg,
|
||||
$filterParametersHolder);
|
||||
} else {
|
||||
$expressionArg = sprintf('%s(DISTINCT %s)', $filterAggr, $tableAlias.'.'
|
||||
.$filter->getField());
|
||||
$expression = $queryBuilder->expr()->$filterOperator(
|
||||
$expressionArg,
|
||||
$filterParametersHolder
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$expressionArg = $queryBuilder->expr()->func($filterAggr, $tableAlias.'.'.$filter->getField());
|
||||
$expression = $queryBuilder->expr()->$filterOperator(
|
||||
$expressionArg,
|
||||
$filterParametersHolder
|
||||
);
|
||||
}
|
||||
} else { // This should never happen
|
||||
$expression = $queryBuilder->expr()->$filterOperator(
|
||||
$tableAlias.'.'.$filter->getField(),
|
||||
$filterParametersHolder
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($queryBuilder->isJoinTable($filter->getTable()) && !$filterAggr) { // This should never happen
|
||||
$queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')');
|
||||
$queryBuilder->addGroupBy($leadsTableAlias.'.id');
|
||||
} else {
|
||||
if ($filterAggr) {
|
||||
$expression = $queryBuilder->expr()->exists('SELECT '.$expressionArg.' FROM '.$filter->getTable().' '.
|
||||
$tableAlias.' WHERE '.$leadsTableAlias.'.id='.$tableAlias.'.lead_id HAVING '.$expression);
|
||||
} else { // This should never happen
|
||||
$queryBuilder->addGroupBy($leadsTableAlias.'.id');
|
||||
}
|
||||
|
||||
$queryBuilder->addLogic($expression, $filter->getGlue());
|
||||
}
|
||||
|
||||
$queryBuilder->setParametersPairs($parameters, $filterParameters);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query\Filter;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\OperatorOptions;
|
||||
use Mautic\LeadBundle\Segment\Query\LeadBatchLimiterTrait;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
|
||||
class ForeignValueFilterQueryBuilder extends BaseFilterQueryBuilder
|
||||
{
|
||||
use LeadBatchLimiterTrait;
|
||||
|
||||
public static function getServiceId(): string
|
||||
{
|
||||
return 'mautic.lead.query.builder.foreign.value';
|
||||
}
|
||||
|
||||
public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter): QueryBuilder
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
||||
$filterOperator = $filter->getOperator();
|
||||
$batchLimiters = $filter->getBatchLimiters();
|
||||
$filterParameters = $filter->getParameterValue();
|
||||
|
||||
// allow use of `contact_id` column instead of deprecated `lead_id`
|
||||
$foreignContactColumn = $filter->getForeignContactColumn();
|
||||
|
||||
if (is_array($filterParameters)) {
|
||||
$parameters = [];
|
||||
foreach ($filterParameters as $filterParameter) {
|
||||
$parameters[] = $this->generateRandomParameterName();
|
||||
}
|
||||
} else {
|
||||
$parameters = $this->generateRandomParameterName();
|
||||
}
|
||||
|
||||
$filterParametersHolder = $filter->getParameterHolder($parameters);
|
||||
|
||||
$tableAlias = $this->generateRandomParameterName();
|
||||
|
||||
$subQueryBuilder = $queryBuilder->createQueryBuilder();
|
||||
|
||||
if (!is_null($filter->getWhere())) {
|
||||
$subQueryBuilder->andWhere(str_replace(str_replace(MAUTIC_TABLE_PREFIX, '', $filter->getTable()).'.', $tableAlias.'.', $filter->getWhere()));
|
||||
}
|
||||
|
||||
switch ($filterOperator) {
|
||||
case 'empty':
|
||||
$subQueryBuilder->select($tableAlias.'.'.$foreignContactColumn)->from($filter->getTable(), $tableAlias);
|
||||
$queryBuilder->addLogic($queryBuilder->expr()->notIn($leadsTableAlias.'.id', $subQueryBuilder->getSQL()), $filter->getGlue());
|
||||
break;
|
||||
case 'notEmpty':
|
||||
$subQueryBuilder->select($tableAlias.'.'.$foreignContactColumn)->from($filter->getTable(), $tableAlias);
|
||||
|
||||
$this->addLeadAndMinMaxLimiters($subQueryBuilder, $batchLimiters, str_replace(MAUTIC_TABLE_PREFIX, '', $filter->getTable()), $foreignContactColumn);
|
||||
|
||||
$queryBuilder->addLogic(
|
||||
$queryBuilder->expr()->in($leadsTableAlias.'.id', $subQueryBuilder->getSQL()),
|
||||
$filter->getGlue()
|
||||
);
|
||||
break;
|
||||
case 'notIn':
|
||||
$subQueryBuilder
|
||||
->select('NULL')->from($filter->getTable(), $tableAlias)
|
||||
->andWhere($tableAlias.'.'.$foreignContactColumn.' = '.$leadsTableAlias.'.id');
|
||||
|
||||
// The use of NOT EXISTS here requires the use of IN instead of NOT IN to prevent a "double negative."
|
||||
// We are not using EXISTS...NOT IN because it results in including everyone who has at least one entry that doesn't
|
||||
// match the criteria. For example, with tags, if the contact has the tag in the filter but also another tag, they'll
|
||||
// be included in the results which is not what we want.
|
||||
$expression = $subQueryBuilder->expr()->in(
|
||||
$tableAlias.'.'.$filter->getField(),
|
||||
$filterParametersHolder
|
||||
);
|
||||
|
||||
$subQueryBuilder->andWhere($expression);
|
||||
$queryBuilder->addLogic($queryBuilder->expr()->notExists($subQueryBuilder->getSQL()), $filter->getGlue());
|
||||
break;
|
||||
case 'neq':
|
||||
$subQueryBuilder
|
||||
->select('NULL')->from($filter->getTable(), $tableAlias)
|
||||
->andWhere($tableAlias.'.'.$foreignContactColumn.' = '.$leadsTableAlias.'.id');
|
||||
|
||||
$expression = $subQueryBuilder->expr()->or(
|
||||
$subQueryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), $filterParametersHolder),
|
||||
$subQueryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField())
|
||||
);
|
||||
|
||||
$subQueryBuilder->andWhere($expression);
|
||||
|
||||
$queryBuilder->addLogic($queryBuilder->expr()->notExists($subQueryBuilder->getSQL()), $filter->getGlue());
|
||||
break;
|
||||
case 'notLike':
|
||||
$subQueryBuilder
|
||||
->select('NULL')->from($filter->getTable(), $tableAlias)
|
||||
->andWhere($tableAlias.'.'.$foreignContactColumn.' = '.$leadsTableAlias.'.id');
|
||||
|
||||
$expression = $subQueryBuilder->expr()->or(
|
||||
$subQueryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()),
|
||||
$subQueryBuilder->expr()->like($tableAlias.'.'.$filter->getField(), $filterParametersHolder)
|
||||
);
|
||||
|
||||
$subQueryBuilder->andWhere($expression);
|
||||
|
||||
$queryBuilder->addLogic($queryBuilder->expr()->notExists($subQueryBuilder->getSQL()), $filter->getGlue());
|
||||
break;
|
||||
case 'regexp':
|
||||
case 'notRegexp':
|
||||
$subQueryBuilder->select($tableAlias.'.'.$foreignContactColumn)
|
||||
->from($filter->getTable(), $tableAlias);
|
||||
|
||||
$this->addLeadAndMinMaxLimiters($subQueryBuilder, $batchLimiters, str_replace(MAUTIC_TABLE_PREFIX, '', $filter->getTable()), $foreignContactColumn);
|
||||
|
||||
$not = ('notRegexp' === $filterOperator) ? ' NOT' : '';
|
||||
$expression = $tableAlias.'.'.$filter->getField().$not.' REGEXP '.$filterParametersHolder;
|
||||
|
||||
$subQueryBuilder->andWhere($expression);
|
||||
|
||||
$queryBuilder->addLogic(
|
||||
$queryBuilder->expr()->in($leadsTableAlias.'.id', $subQueryBuilder->getSQL()),
|
||||
$filter->getGlue()
|
||||
);
|
||||
break;
|
||||
case OperatorOptions::INCLUDING_ALL:
|
||||
$subQueryBuilder->select($tableAlias.'.'.$foreignContactColumn)
|
||||
->from($filter->getTable(), $tableAlias);
|
||||
|
||||
$this->addLeadAndMinMaxLimiters($subQueryBuilder, $batchLimiters, str_replace(MAUTIC_TABLE_PREFIX, '', $filter->getTable()), $foreignContactColumn);
|
||||
|
||||
$expression = $subQueryBuilder->expr()->in(
|
||||
$tableAlias.'.'.$filter->getField(),
|
||||
$filterParametersHolder
|
||||
);
|
||||
$subQueryBuilder->andWhere($expression);
|
||||
|
||||
$subQueryBuilder->groupBy($tableAlias.'.'.$foreignContactColumn);
|
||||
$subQueryBuilder->having('COUNT(DISTINCT '.$tableAlias.'.'.$filter->getField().') = '.count($filterParametersHolder));
|
||||
|
||||
$queryBuilder->addLogic($queryBuilder->expr()->in($leadsTableAlias.'.id', $subQueryBuilder->getSQL()), $filter->getGlue());
|
||||
break;
|
||||
case OperatorOptions::EXCLUDING_ALL:
|
||||
$subQueryBuilder
|
||||
->select('NULL')->from($filter->getTable(), $tableAlias)
|
||||
->andWhere($tableAlias.'.'.$foreignContactColumn.' = '.$leadsTableAlias.'.id');
|
||||
|
||||
// The use of NOT EXISTS here requires the use of IN instead of NOT IN to prevent a "double negative."
|
||||
// We are not using EXISTS...NOT IN because it results in including everyone who has at least one entry that doesn't
|
||||
// match the criteria. For example, with tags, if the contact has the tag in the filter but also another tag, they'll
|
||||
// be included in the results which is not what we want.
|
||||
$expression = $subQueryBuilder->expr()->in(
|
||||
$tableAlias.'.'.$filter->getField(),
|
||||
$filterParametersHolder
|
||||
);
|
||||
|
||||
$subQueryBuilder->andWhere($expression);
|
||||
|
||||
$subQueryBuilder->groupBy($tableAlias.'.'.$foreignContactColumn);
|
||||
$subQueryBuilder->having('COUNT(DISTINCT '.$tableAlias.'.'.$filter->getField().') = '.count($filterParametersHolder));
|
||||
|
||||
$queryBuilder->addLogic($queryBuilder->expr()->notExists($subQueryBuilder->getSQL()), $filter->getGlue());
|
||||
break;
|
||||
default:
|
||||
$subQueryBuilder->select($tableAlias.'.'.$foreignContactColumn)
|
||||
->from($filter->getTable(), $tableAlias);
|
||||
|
||||
$this->addLeadAndMinMaxLimiters($subQueryBuilder, $batchLimiters, str_replace(MAUTIC_TABLE_PREFIX, '', $filter->getTable()), $foreignContactColumn);
|
||||
|
||||
$expression = $subQueryBuilder->expr()->$filterOperator(
|
||||
$tableAlias.'.'.$filter->getField(),
|
||||
$filterParametersHolder
|
||||
);
|
||||
$subQueryBuilder->andWhere($expression);
|
||||
|
||||
$queryBuilder->addLogic($queryBuilder->expr()->in($leadsTableAlias.'.id', $subQueryBuilder->getSQL()), $filter->getGlue());
|
||||
}
|
||||
|
||||
$queryBuilder->setParametersPairs($parameters, $filterParameters);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query\Filter;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryException;
|
||||
|
||||
class IntegrationCampaignFilterQueryBuilder extends BaseFilterQueryBuilder
|
||||
{
|
||||
public static function getServiceId(): string
|
||||
{
|
||||
return 'mautic.lead.query.builder.special.integration';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws QueryException
|
||||
*/
|
||||
public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter): QueryBuilder
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
||||
$integrationCampaignParts = $filter->getIntegrationCampaignParts();
|
||||
|
||||
$integrationNameParameter = $this->generateRandomParameterName();
|
||||
$campaignIdParameter = $this->generateRandomParameterName();
|
||||
|
||||
$tableAlias = $this->generateRandomParameterName();
|
||||
|
||||
$queryBuilder->leftJoin(
|
||||
$leadsTableAlias,
|
||||
MAUTIC_TABLE_PREFIX.'integration_entity',
|
||||
$tableAlias,
|
||||
$tableAlias.'.integration_entity = "CampaignMember" AND '.
|
||||
$tableAlias.".internal_entity = 'lead' AND ".
|
||||
$tableAlias.'.internal_entity_id = '.$leadsTableAlias.'.id'
|
||||
);
|
||||
|
||||
$expression = $queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->eq($tableAlias.'.integration', ":$integrationNameParameter"),
|
||||
$queryBuilder->expr()->eq($tableAlias.'.integration_entity_id', ":$campaignIdParameter")
|
||||
);
|
||||
|
||||
$queryBuilder->addJoinCondition($tableAlias, $expression);
|
||||
|
||||
if ('eq' === $filter->getOperator()) {
|
||||
$queryType = $filter->getParameterValue() ? 'isNotNull' : 'isNull';
|
||||
} else {
|
||||
$queryType = $filter->getParameterValue() ? 'isNull' : 'isNotNull';
|
||||
}
|
||||
|
||||
$queryBuilder->addLogic($queryBuilder->expr()->$queryType($tableAlias.'.id'), $filter->getGlue());
|
||||
|
||||
$queryBuilder->setParameter($integrationNameParameter, $integrationCampaignParts->getIntegrationName());
|
||||
$queryBuilder->setParameter($campaignIdParameter, $integrationCampaignParts->getCampaignId());
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query\Filter;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
|
||||
class SessionsFilterQueryBuilder extends BaseFilterQueryBuilder
|
||||
{
|
||||
public static function getServiceId(): string
|
||||
{
|
||||
return 'mautic.lead.query.builder.special.sessions';
|
||||
}
|
||||
|
||||
public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter): QueryBuilder
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads');
|
||||
$pageHitsAlias = $this->generateRandomParameterName();
|
||||
$exclusionAlias = $this->generateRandomParameterName();
|
||||
$expressionValueAlias = $this->generateRandomParameterName();
|
||||
|
||||
$expressionOperator = $filter->getOperator();
|
||||
$expression = $queryBuilder->expr()->$expressionOperator('count(id)',
|
||||
$filter->getParameterHolder($expressionValueAlias));
|
||||
|
||||
$queryBuilder->setParameter($expressionValueAlias, (int) $filter->getParameterValue());
|
||||
|
||||
$exclusionQueryBuilder = $queryBuilder->createQueryBuilder();
|
||||
$exclusionQueryBuilder
|
||||
->select($exclusionAlias.'.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits', $exclusionAlias)
|
||||
->where(
|
||||
$queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->eq($leadsTableAlias.'.id', $exclusionAlias.'.lead_id'),
|
||||
$queryBuilder->expr()->gt(
|
||||
$exclusionAlias.'.date_hit',
|
||||
$pageHitsAlias.'.date_hit - INTERVAL 30 MINUTE'
|
||||
),
|
||||
$queryBuilder->expr()->lt($exclusionAlias.'.date_hit', $pageHitsAlias.'.date_hit')
|
||||
)
|
||||
);
|
||||
|
||||
$sessionQueryBuilder = $queryBuilder->createQueryBuilder();
|
||||
$sessionQueryBuilder
|
||||
->select('count(id)')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits', $pageHitsAlias)
|
||||
->where(
|
||||
$queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->eq($leadsTableAlias.'.id', $pageHitsAlias.'.lead_id'),
|
||||
$queryBuilder->expr()->isNull($pageHitsAlias.'.email_id'),
|
||||
$queryBuilder->expr()->isNull($pageHitsAlias.'.redirect_id'),
|
||||
$queryBuilder->expr()->notExists(
|
||||
$exclusionQueryBuilder->getSQL()
|
||||
)
|
||||
)
|
||||
)
|
||||
->having($expression);
|
||||
|
||||
$glue = $filter->getGlue().'Where';
|
||||
$queryBuilder->$glue($queryBuilder->expr()->exists($sessionQueryBuilder->getSQL()));
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query;
|
||||
|
||||
trait LeadBatchLimiterTrait
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $batchLimiters
|
||||
*/
|
||||
private function addMinMaxLimiters(QueryBuilder $queryBuilder, array $batchLimiters, string $tableName, string $columnName = 'lead_id'): void
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.$tableName);
|
||||
|
||||
if (!empty($batchLimiters['minId']) && !empty($batchLimiters['maxId'])) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->comparison($leadsTableAlias.'.'.$columnName, 'BETWEEN', "{$batchLimiters['minId']} and {$batchLimiters['maxId']}")
|
||||
);
|
||||
} elseif (!empty($batchLimiters['maxId'])) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->lte($leadsTableAlias.'.'.$columnName, (int) $batchLimiters['maxId'])
|
||||
);
|
||||
} elseif (!empty($batchLimiters['minId'])) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->gte($leadsTableAlias.'.'.$columnName, (int) $batchLimiters['minId'])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $batchLimiters
|
||||
*/
|
||||
private function addLeadLimiter(QueryBuilder $queryBuilder, array $batchLimiters, string $tableName, string $columnName = 'lead_id'): void
|
||||
{
|
||||
$leadsTableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.$tableName);
|
||||
|
||||
if (empty($batchLimiters['lead_id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder->andWhere($leadsTableAlias.'.'.$columnName.' = '.$batchLimiters['lead_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $batchLimiters
|
||||
*/
|
||||
private function addLeadAndMinMaxLimiters(QueryBuilder $queryBuilder, array $batchLimiters, string $tableName, string $columnName = 'lead_id'): void
|
||||
{
|
||||
$this->addLeadLimiter($queryBuilder, $batchLimiters, $tableName, $columnName);
|
||||
$this->addMinMaxLimiters($queryBuilder, $batchLimiters, $tableName, $columnName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Doctrine\DBAL\Query\QueryBuilder as BaseQueryBuilder;
|
||||
use Mautic\LeadBundle\Segment\Query\Expression\ExpressionBuilder;
|
||||
|
||||
class QueryBuilder extends BaseQueryBuilder
|
||||
{
|
||||
private ?ExpressionBuilder $_expr = null;
|
||||
|
||||
/**
|
||||
* Unprocessed logic for segment processing.
|
||||
*/
|
||||
private array $logicStack = [];
|
||||
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
parent::__construct($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExpressionBuilder
|
||||
*/
|
||||
public function expr()
|
||||
{
|
||||
if (!is_null($this->_expr)) {
|
||||
return $this->_expr;
|
||||
}
|
||||
|
||||
$this->_expr = new ExpressionBuilder($this->connection);
|
||||
|
||||
return $this->_expr;
|
||||
}
|
||||
|
||||
public function setParameter($key, $value, $type = null)
|
||||
{
|
||||
if (str_starts_with($key, ':')) {
|
||||
// For consistency sake, remove the :
|
||||
$key = substr($key, 1);
|
||||
@\trigger_error('Using query key with ":" is deprecated. Use key without ":" instead.', \E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
$value = (int) $value;
|
||||
}
|
||||
|
||||
return parent::setParameter($key, $value, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $queryPartName
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setQueryPart($queryPartName, $value)
|
||||
{
|
||||
$this->resetQueryPart($queryPartName);
|
||||
$this->add($queryPartName, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSQL()
|
||||
{
|
||||
$sql = &$this->parentProperty('sql');
|
||||
$state = &$this->parentProperty('state');
|
||||
|
||||
if (null !== $sql && 1 /* self::STATE_CLEAN */ === $state) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$sql = match ($this->getType()) { /** @phpstan-ignore-line this method is deprecated. We'll have to find a way how to refactor this method. */
|
||||
3 /* self::INSERT */ => $this->parentMethod('getSQLForInsert'),
|
||||
1 /* self::DELETE */ => $this->parentMethod('getSQLForDelete'),
|
||||
2 /* self::UPDATE */ => $this->parentMethod('getSQLForUpdate'),
|
||||
default => $this->getSQLForSelect(),
|
||||
};
|
||||
|
||||
$state = 1 /* self::STATE_CLEAN */;
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function getSQLForSelect(): string
|
||||
{
|
||||
$sqlParts = $this->getQueryParts();
|
||||
|
||||
$query = 'SELECT '.($sqlParts['distinct'] ? 'DISTINCT ' : '').
|
||||
implode(', ', $sqlParts['select']);
|
||||
|
||||
$query .= ($sqlParts['from'] ? ' FROM '.implode(', ', $this->getFromClauses()) : '')
|
||||
.(null !== $sqlParts['where'] ? ' WHERE '.($sqlParts['where']) : '')
|
||||
.($sqlParts['groupBy'] ? ' GROUP BY '.implode(', ', $sqlParts['groupBy']) : '')
|
||||
.(null !== $sqlParts['having'] ? ' HAVING '.($sqlParts['having']) : '')
|
||||
.($sqlParts['orderBy'] ? ' ORDER BY '.implode(', ', $sqlParts['orderBy']) : '');
|
||||
|
||||
if ($this->parentMethod('isLimitQuery')) {
|
||||
return $this->connection->getDatabasePlatform()->modifyLimitQuery(
|
||||
$query,
|
||||
$this->getMaxResults(),
|
||||
$this->getFirstResult()
|
||||
);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getFromClauses(): array
|
||||
{
|
||||
$fromClauses = [];
|
||||
$knownAliases = [];
|
||||
|
||||
// Loop through all FROM clauses
|
||||
foreach ($this->getQueryParts()['from'] as $from) {
|
||||
if (null === $from['alias']) {
|
||||
$tableSql = $from['table'];
|
||||
$tableReference = $from['table'];
|
||||
} else {
|
||||
$tableSql = $from['table'].' '.$from['alias'];
|
||||
$tableReference = $from['alias'];
|
||||
}
|
||||
|
||||
if (isset($from['hint'])) {
|
||||
$tableSql .= ' '.$from['hint'];
|
||||
}
|
||||
|
||||
$knownAliases[$tableReference] = true;
|
||||
|
||||
$fromClauses[$tableReference] = $tableSql.\Closure::bind(
|
||||
fn ($tableReference, &$knownAliases) => $this->{'getSQLForJoins'}($tableReference, $knownAliases),
|
||||
$this,
|
||||
parent::class
|
||||
)($tableReference, $knownAliases);
|
||||
}
|
||||
|
||||
$this->parentMethod('verifyAllAliasesAreKnown', $knownAliases);
|
||||
|
||||
return $fromClauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $alias
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function getJoinCondition($alias)
|
||||
{
|
||||
$parts = $this->getQueryParts();
|
||||
foreach ($parts['join']['l'] as $joinedTable) {
|
||||
if ($joinedTable['joinAlias'] == $alias) {
|
||||
return $joinedTable['joinCondition'];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add AND condition to existing table alias.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws QueryException
|
||||
*/
|
||||
public function addJoinCondition($alias, $expr)
|
||||
{
|
||||
$result = $parts = $this->getQueryPart('join');
|
||||
|
||||
foreach ($parts as $tbl => $joins) {
|
||||
foreach ($joins as $key => $join) {
|
||||
if ($join['joinAlias'] == $alias) {
|
||||
$result[$tbl][$key]['joinCondition'] = $join['joinCondition'].' and ('.$expr.')';
|
||||
$inserted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($inserted)) {
|
||||
throw new QueryException('Inserting condition to nonexistent join '.$alias);
|
||||
}
|
||||
|
||||
$this->setQueryPart('join', $result);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function replaceJoinCondition($alias, $expr)
|
||||
{
|
||||
$parts = $this->getQueryPart('join');
|
||||
foreach ($parts['l'] as $key => $part) {
|
||||
if ($part['joinAlias'] == $alias) {
|
||||
$parts['l'][$key]['joinCondition'] = $expr;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setQueryPart('join', $parts);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function setParametersPairs($parameters, $filterParameters)
|
||||
{
|
||||
if (!is_array($parameters)) {
|
||||
return $this->setParameter($parameters, $filterParameters);
|
||||
}
|
||||
|
||||
foreach ($parameters as $parameter) {
|
||||
$parameterValue = array_shift($filterParameters);
|
||||
$this->setParameter($parameter, $parameterValue);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|bool|string
|
||||
*/
|
||||
public function getTableAlias(string $table, $joinType = null)
|
||||
{
|
||||
if (is_null($joinType)) {
|
||||
$tables = $this->getTableAliases();
|
||||
|
||||
return $tables[$table] ?? false;
|
||||
}
|
||||
|
||||
$tableJoins = $this->getTableJoins($table);
|
||||
|
||||
foreach ($tableJoins as $tableJoin) {
|
||||
if ($tableJoin['joinType'] == $joinType) {
|
||||
return $tableJoin['joinAlias'];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getTableJoins(string $tableName): array
|
||||
{
|
||||
$found = [];
|
||||
foreach ($this->getQueryParts()['join'] as $join) {
|
||||
foreach ($join as $joinPart) {
|
||||
if ($tableName == $joinPart['joinTable']) {
|
||||
$found[] = $joinPart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count($found) ? $found : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions returns either the 'lead.id' or the primary key from right joined table.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function guessPrimaryLeadContactIdColumn()
|
||||
{
|
||||
$parts = $this->getQueryParts();
|
||||
$leadTable = $parts['from'][0]['alias'];
|
||||
|
||||
if ('orp' === $leadTable) {
|
||||
return 'orp.lead_id';
|
||||
}
|
||||
|
||||
if (!isset($parts['join'][$leadTable])) {
|
||||
return $leadTable.'.id';
|
||||
}
|
||||
|
||||
$joins = $parts['join'][$leadTable];
|
||||
|
||||
foreach ($joins as $join) {
|
||||
if ('right' == $join['joinType']) {
|
||||
$matches = null;
|
||||
if (preg_match('/'.$leadTable.'\.id \= ([^\ ]+)/i', $join['joinCondition'], $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $leadTable.'.id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return aliases of all currently registered tables.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTableAliases()
|
||||
{
|
||||
$queryParts = $this->getQueryParts();
|
||||
$tables = array_reduce($queryParts['from'], function ($result, $item) {
|
||||
$result[$item['table']] = $item['alias'];
|
||||
|
||||
return $result;
|
||||
}, []);
|
||||
|
||||
foreach ($queryParts['join'] as $join) {
|
||||
foreach ($join as $joinPart) {
|
||||
$tables[$joinPart['joinTable']] = $joinPart['joinAlias'];
|
||||
}
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $table
|
||||
*/
|
||||
public function isJoinTable($table): bool
|
||||
{
|
||||
$queryParts = $this->getQueryParts();
|
||||
|
||||
foreach ($queryParts['join'] as $join) {
|
||||
foreach ($join as $joinPart) {
|
||||
if ($joinPart['joinTable'] == $table) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|string
|
||||
*
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
*/
|
||||
public function getDebugOutput()
|
||||
{
|
||||
$params = $this->getParameters();
|
||||
$sql = $this->getSQL();
|
||||
foreach ($params as $key=>$val) {
|
||||
if (!is_int($val) && !is_float($val) && !is_array($val)) {
|
||||
$val = "'$val'";
|
||||
} elseif (is_array($val)) {
|
||||
if (ArrayParameterType::STRING === $this->getParameterType($key)) {
|
||||
$val = array_map(static fn ($value) => "'$value'", $val);
|
||||
}
|
||||
$val = implode(', ', $val);
|
||||
}
|
||||
$sql = str_replace(":{$key}", $val, $sql);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public function hasLogicStack(): bool
|
||||
{
|
||||
return count($this->logicStack) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getLogicStack()
|
||||
{
|
||||
return $this->logicStack;
|
||||
}
|
||||
|
||||
public function popLogicStack(): array
|
||||
{
|
||||
$stack = $this->logicStack;
|
||||
$this->logicStack = [];
|
||||
|
||||
return $stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
private function addLogicStack($expression)
|
||||
{
|
||||
$this->logicStack[] = $expression;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function assembles correct logic for segment processing, this is to replace andWhere and orWhere (virtualy
|
||||
* as they need to be kept). You may not use andWhere in filters!!!
|
||||
*/
|
||||
public function addLogic($expression, $glue): void
|
||||
{
|
||||
// little setup
|
||||
$glue = strtolower($glue);
|
||||
|
||||
// Different handling
|
||||
if ('or' == $glue) {
|
||||
// Is this the first condition in query builder?
|
||||
if (!is_null($this->getQueryPart('where'))) {
|
||||
// Are the any queued conditions?
|
||||
if ($this->hasLogicStack()) {
|
||||
// We need to apply current stack to the query builder
|
||||
$this->applyStackLogic();
|
||||
}
|
||||
// We queue current expression to stack
|
||||
$this->addLogicStack($expression);
|
||||
} else {
|
||||
$this->andWhere($expression);
|
||||
}
|
||||
} else {
|
||||
// Glue is AND
|
||||
if ($this->hasLogicStack()) {
|
||||
$this->addLogicStack($expression);
|
||||
} else {
|
||||
$this->andWhere($expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply content of stack.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function applyStackLogic()
|
||||
{
|
||||
if ($this->hasLogicStack()) {
|
||||
$stackGroupExpression = new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack());
|
||||
$this->orWhere($stackGroupExpression);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function createQueryBuilder(?Connection $connection = null): QueryBuilder
|
||||
{
|
||||
return new self($connection ?: $this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @noinspection PhpPassByRefInspection
|
||||
*/
|
||||
private function &parentProperty(string $property)
|
||||
{
|
||||
return \Closure::bind(function &() use ($property) {
|
||||
return $this->$property;
|
||||
}, $this, parent::class)();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed ...$arguments
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function parentMethod(string $method, ...$arguments)
|
||||
{
|
||||
return \Closure::bind(function () use ($method, $arguments) {
|
||||
return $this->$method(...$arguments);
|
||||
}, $this, parent::class)();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Query;
|
||||
|
||||
/**
|
||||
* @since 2.1.4
|
||||
*/
|
||||
class QueryException extends \Doctrine\DBAL\Exception
|
||||
{
|
||||
public static function unknownAlias($alias, $registeredAliases): self
|
||||
{
|
||||
return new self("The given alias '".$alias."' is not part of ".
|
||||
'any FROM or JOIN clause table. The currently registered '.
|
||||
'aliases are: '.implode(', ', $registeredAliases).'.');
|
||||
}
|
||||
|
||||
public static function nonUniqueAlias($alias, $registeredAliases): self
|
||||
{
|
||||
return new self("The given alias '".$alias."' is not unique ".
|
||||
'in FROM and JOIN clause table. The currently registered '.
|
||||
'aliases are: '.implode(', ', $registeredAliases).'.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user