Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment;
|
||||
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Mautic\LeadBundle\Segment\Decorator\ContactDecoratorForeignInterface;
|
||||
use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface;
|
||||
use Mautic\LeadBundle\Segment\DoNotContact\DoNotContactParts;
|
||||
use Mautic\LeadBundle\Segment\Exception\FieldNotFoundException;
|
||||
use Mautic\LeadBundle\Segment\Exception\TableNotFoundException;
|
||||
use Mautic\LeadBundle\Segment\IntegrationCampaign\IntegrationCampaignParts;
|
||||
use Mautic\LeadBundle\Segment\Query\Filter\FilterQueryBuilderInterface;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
|
||||
/**
|
||||
* Used for accessing $filter as an object and to keep logic in an object.
|
||||
*/
|
||||
class ContactSegmentFilter implements \Stringable
|
||||
{
|
||||
/**
|
||||
* @var ContactSegmentFilterCrate
|
||||
*/
|
||||
public $contactSegmentFilterCrate;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $batchLimiters
|
||||
*/
|
||||
public function __construct(
|
||||
ContactSegmentFilterCrate $contactSegmentFilterCrate,
|
||||
private FilterDecoratorInterface $filterDecorator,
|
||||
private TableSchemaColumnsCache $schemaCache,
|
||||
private FilterQueryBuilderInterface $filterQueryBuilder,
|
||||
private array $batchLimiters = [],
|
||||
) {
|
||||
$this->contactSegmentFilterCrate = $contactSegmentFilterCrate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Column
|
||||
*
|
||||
* @throws FieldNotFoundException
|
||||
*/
|
||||
public function getColumn()
|
||||
{
|
||||
$currentDBName = $this->schemaCache->getCurrentDatabaseName();
|
||||
|
||||
$table = preg_replace("/^{$currentDBName}\./", '', $this->getTable());
|
||||
|
||||
if (!$table) {
|
||||
throw new TableNotFoundException('Segment Query is missing table, perhaps incorrectly registered custom Query Builder. ');
|
||||
}
|
||||
|
||||
$columns = $this->schemaCache->getColumns($table);
|
||||
|
||||
if (!isset($columns[$this->getField()])) {
|
||||
throw new FieldNotFoundException(sprintf('Database schema does not contain field %s.%s', $this->getTable(), $this->getField()));
|
||||
}
|
||||
|
||||
return $columns[$this->getField()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getQueryType()
|
||||
{
|
||||
return $this->filterDecorator->getQueryType($this->contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOperator()
|
||||
{
|
||||
return $this->filterDecorator->getOperator($this->contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getField()
|
||||
{
|
||||
return $this->filterDecorator->getField($this->contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->contactSegmentFilterCrate->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getTable()
|
||||
{
|
||||
return $this->filterDecorator->getTable($this->contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getForeignContactColumn(): ?string
|
||||
{
|
||||
if ($this->filterDecorator instanceof ContactDecoratorForeignInterface) {
|
||||
return $this->filterDecorator->getForeignContactColumn($this->contactSegmentFilterCrate);
|
||||
} else {
|
||||
return 'lead_id';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getParameterHolder($argument)
|
||||
{
|
||||
return $this->filterDecorator->getParameterHolder($this->contactSegmentFilterCrate, $argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getParameterValue()
|
||||
{
|
||||
return $this->filterDecorator->getParameterValue($this->contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getWhere()
|
||||
{
|
||||
return $this->filterDecorator->getWhere($this->contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGlue()
|
||||
{
|
||||
return $this->contactSegmentFilterCrate->getGlue();
|
||||
}
|
||||
|
||||
public function getAggregateFunction(): string|bool
|
||||
{
|
||||
return $this->filterDecorator->getAggregateFunc($this->contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getFilterQueryBuilder(): FilterQueryBuilderInterface
|
||||
{
|
||||
return $this->filterQueryBuilder;
|
||||
}
|
||||
|
||||
public function applyQuery(QueryBuilder $queryBuilder): QueryBuilder
|
||||
{
|
||||
return $this->filterQueryBuilder->applyQuery($queryBuilder, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the filter references another ContactSegment.
|
||||
*/
|
||||
public function isContactSegmentReference(): bool
|
||||
{
|
||||
return 'leadlist' === $this->getField();
|
||||
}
|
||||
|
||||
public function isColumnTypeBoolean(): bool
|
||||
{
|
||||
return $this->contactSegmentFilterCrate->isBooleanType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getNullValue()
|
||||
{
|
||||
return $this->contactSegmentFilterCrate->getNullValue();
|
||||
}
|
||||
|
||||
public function getDoNotContactParts(): DoNotContactParts
|
||||
{
|
||||
return new DoNotContactParts($this->contactSegmentFilterCrate->getField());
|
||||
}
|
||||
|
||||
public function getIntegrationCampaignParts(): IntegrationCampaignParts
|
||||
{
|
||||
return new IntegrationCampaignParts($this->getParameterValue());
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'table: %s, %s on %s %s %s',
|
||||
$this->getTable(),
|
||||
$this->getField(),
|
||||
$this->getQueryType(),
|
||||
$this->getOperator(),
|
||||
json_encode($this->getParameterValue())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRelationJoinTable()
|
||||
{
|
||||
return method_exists($this->filterDecorator, 'getRelationJoinTable') ? $this->filterDecorator->getRelationJoinTable() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRelationJoinTableField()
|
||||
{
|
||||
return method_exists($this->filterDecorator, 'getRelationJoinTableField') ?
|
||||
$this->filterDecorator->getRelationJoinTableField() : null;
|
||||
}
|
||||
|
||||
public function doesColumnSupportEmptyValue(): bool
|
||||
{
|
||||
return !in_array($this->contactSegmentFilterCrate->getType(), ['date', 'datetime'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getBatchLimiters(): array
|
||||
{
|
||||
return $this->batchLimiters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment;
|
||||
|
||||
class ContactSegmentFilterCrate
|
||||
{
|
||||
public const CONTACT_OBJECT = 'lead';
|
||||
|
||||
public const COMPANY_OBJECT = 'company';
|
||||
|
||||
public const BEHAVIORS_OBJECT = 'behaviors';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $glue;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $field;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $object;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var string|array|bool|float|null
|
||||
*/
|
||||
private $filter;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $operator;
|
||||
|
||||
private array $sourceArray;
|
||||
|
||||
private $nullValue;
|
||||
|
||||
/**
|
||||
* @var array|mixed[]
|
||||
*/
|
||||
private array $mergedProperty;
|
||||
|
||||
public function __construct(array $filter)
|
||||
{
|
||||
$bcFilter = $filter['filter'] ?? null;
|
||||
$this->glue = $filter['glue'] ?? null;
|
||||
$this->field = $filter['field'] ?? null;
|
||||
$this->object = $filter['object'] ?? self::CONTACT_OBJECT;
|
||||
$this->type = $filter['type'] ?? null;
|
||||
$this->filter = $filter['properties']['filter'] ?? $bcFilter;
|
||||
$this->nullValue = $filter['null_value'] ?? null;
|
||||
$this->mergedProperty = $filter['merged_property'] ?? [];
|
||||
$this->sourceArray = $filter;
|
||||
|
||||
$this->setOperator($filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGlue()
|
||||
{
|
||||
return $this->glue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getField()
|
||||
{
|
||||
return $this->field;
|
||||
}
|
||||
|
||||
public function isContactType(): bool
|
||||
{
|
||||
return self::CONTACT_OBJECT === $this->object;
|
||||
}
|
||||
|
||||
public function isCompanyType(): bool
|
||||
{
|
||||
return self::COMPANY_OBJECT === $this->object;
|
||||
}
|
||||
|
||||
public function isBehaviorsType(): bool
|
||||
{
|
||||
return self::BEHAVIORS_OBJECT === $this->object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|array|bool|float|null
|
||||
*/
|
||||
public function getFilter()
|
||||
{
|
||||
$excludeTypecastOperators = [
|
||||
OperatorOptions::INCLUDING_ANY,
|
||||
OperatorOptions::EXCLUDING_ANY,
|
||||
OperatorOptions::INCLUDING_ALL,
|
||||
OperatorOptions::EXCLUDING_ALL,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
];
|
||||
|
||||
if (!in_array($this->operator, $excludeTypecastOperators, true)) {
|
||||
switch ($this->getType()) {
|
||||
case 'number':
|
||||
return (float) $this->filter;
|
||||
case 'boolean':
|
||||
return (bool) $this->filter;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOperator()
|
||||
{
|
||||
return $this->operator;
|
||||
}
|
||||
|
||||
public function isBooleanType(): bool
|
||||
{
|
||||
return 'boolean' === $this->getType();
|
||||
}
|
||||
|
||||
public function isNumberType(): bool
|
||||
{
|
||||
return 'number' === $this->getType();
|
||||
}
|
||||
|
||||
public function isDateType(): bool
|
||||
{
|
||||
return 'date' === $this->getType() || $this->hasTimeParts();
|
||||
}
|
||||
|
||||
public function hasTimeParts(): bool
|
||||
{
|
||||
return 'datetime' === $this->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter value could be used directly - no modification (like regex etc.) needed.
|
||||
*/
|
||||
public function filterValueDoNotNeedAdjustment(): bool
|
||||
{
|
||||
return $this->isNumberType() || $this->isBooleanType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getArray()
|
||||
{
|
||||
return $this->sourceArray;
|
||||
}
|
||||
|
||||
private function setOperator(array $filter): void
|
||||
{
|
||||
$operator = $filter['operator'] ?? null;
|
||||
|
||||
if ('multiselect' === $this->getType() && in_array($operator, [OperatorOptions::INCLUDING_ANY, OperatorOptions::EXCLUDING_ANY, OperatorOptions::INCLUDING_ALL, OperatorOptions::EXCLUDING_ALL])) {
|
||||
$neg = !str_contains($operator, '!') ? '' : '!';
|
||||
$this->operator = $neg.$this->getType();
|
||||
|
||||
return;
|
||||
}
|
||||
if ('page_id' === $this->getField() || 'email_id' === $this->getField() || 'redirect_id' === $this->getField() || 'notification' === $this->getField()) {
|
||||
$operator = ('=' === $operator) === $this->getFilter() ? 'notEmpty' : 'empty';
|
||||
}
|
||||
|
||||
if ('=' === $operator && is_array($this->getFilter())) { // Fix for old segments which can have stored = instead on in operator
|
||||
$operator = 'in';
|
||||
}
|
||||
|
||||
$this->operator = $operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getNullValue()
|
||||
{
|
||||
return $this->nullValue;
|
||||
}
|
||||
|
||||
public function getObject(): ?string
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|mixed[]
|
||||
*/
|
||||
public function getMergedProperty(): array
|
||||
{
|
||||
return $this->mergedProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|mixed[] $mergedProperty
|
||||
*/
|
||||
public function setMergedProperty(array $mergedProperty): void
|
||||
{
|
||||
$this->mergedProperty = $mergedProperty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Event\LeadListMergeFiltersEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Segment\Decorator\DecoratorFactory;
|
||||
use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface;
|
||||
use Mautic\LeadBundle\Segment\Query\Filter\FilterQueryBuilderInterface;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class ContactSegmentFilterFactory
|
||||
{
|
||||
public const CUSTOM_OPERATOR = 'custom_operator';
|
||||
|
||||
/**
|
||||
* @var array|string[]
|
||||
*/
|
||||
private array $operatorsWithEmptyValuesAllowed = ['empty', '!empty', self::CUSTOM_OPERATOR];
|
||||
|
||||
public function __construct(
|
||||
private TableSchemaColumnsCache $schemaCache,
|
||||
private Container $container,
|
||||
private DecoratorFactory $decoratorFactory,
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $batchLimiters
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getSegmentFilters(LeadList $leadList, array $batchLimiters = []): ContactSegmentFilters
|
||||
{
|
||||
$contactSegmentFilters = new ContactSegmentFilters();
|
||||
|
||||
$filters = $this->mergeFilters($leadList->getFilters());
|
||||
$event = new LeadListMergeFiltersEvent($filters);
|
||||
$this->eventDispatcher->dispatch($event, LeadEvents::LIST_FILTERS_MERGE);
|
||||
$filters = $event->getFilters();
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if (self::CUSTOM_OPERATOR === $filter['operator']) {
|
||||
$mergedProperty = $filter['merged_property'];
|
||||
$factorSegmentFilter = null;
|
||||
foreach ($filter['properties'] as $index => $nestedFilter) {
|
||||
if (!in_array($nestedFilter['operator'], $this->operatorsWithEmptyValuesAllowed) && empty($nestedFilter['filter']) && !is_numeric($nestedFilter['filter'])) {
|
||||
continue; // If no value set for the filter, don't consider it
|
||||
}
|
||||
$factorSegmentFilter = $this->factorSegmentFilter($nestedFilter, $batchLimiters);
|
||||
$mergedProperty[$index] = [
|
||||
'filter_value' => $factorSegmentFilter->getParameterValue(),
|
||||
'operator' => $factorSegmentFilter->getOperator(),
|
||||
'field' => $factorSegmentFilter->getField(),
|
||||
'type' => $factorSegmentFilter->getType(),
|
||||
'filter' => $factorSegmentFilter,
|
||||
];
|
||||
}
|
||||
if ($factorSegmentFilter) {
|
||||
$factorSegmentFilter->contactSegmentFilterCrate->setMergedProperty($mergedProperty);
|
||||
$contactSegmentFilters->addContactSegmentFilter($factorSegmentFilter);
|
||||
}
|
||||
} else {
|
||||
$contactSegmentFilters->addContactSegmentFilter($this->factorSegmentFilter($filter, $batchLimiters));
|
||||
}
|
||||
}
|
||||
|
||||
return $contactSegmentFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $filter
|
||||
* @param array<string, mixed> $batchLimiters
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function factorSegmentFilter(array $filter, array $batchLimiters = []): ContactSegmentFilter
|
||||
{
|
||||
$contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter);
|
||||
|
||||
$decorator = $this->decoratorFactory->getDecoratorForFilter($contactSegmentFilterCrate);
|
||||
|
||||
$filterQueryBuilder = $this->getQueryBuilderForFilter($decorator, $contactSegmentFilterCrate);
|
||||
|
||||
return new ContactSegmentFilter($contactSegmentFilterCrate, $decorator, $this->schemaCache, $filterQueryBuilder, $batchLimiters);
|
||||
}
|
||||
|
||||
private function getQueryBuilderForFilter(FilterDecoratorInterface $decorator, ContactSegmentFilterCrate $contactSegmentFilterCrate): FilterQueryBuilderInterface
|
||||
{
|
||||
$qbServiceId = $decorator->getQueryType($contactSegmentFilterCrate);
|
||||
|
||||
return $this->container->get($qbServiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge multiple filters of same field with OR.
|
||||
*
|
||||
* @param mixed[] $filters
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function mergeFilters(array $filters): array
|
||||
{
|
||||
$shrinkedFilters = [];
|
||||
$arrStacks = []; // Put the same filters from array into Stacks
|
||||
|
||||
$previousKey = ''; // preserve the key from previous iteration
|
||||
// replace filters with glue OR and operator = , with IN operator
|
||||
foreach ($filters as $filter) {
|
||||
// easy to compare
|
||||
$key = implode('_', [
|
||||
$filter['object'] ?? '',
|
||||
$filter['field'],
|
||||
$filter['glue'],
|
||||
$filter['operator'],
|
||||
]);
|
||||
|
||||
if ('or' === strtolower($filter['glue']) && '=' === $filter['operator']) {
|
||||
if (!isset($arrStacks[$key])) {
|
||||
$arrStacks[$key] = [];
|
||||
}
|
||||
|
||||
array_push($arrStacks[$key], $filter);
|
||||
} else { // glue = and
|
||||
// if 'or' followed by 'and', it becomes - or (cond1 and cond2)
|
||||
if (isset($arrStacks[$previousKey]) && count($arrStacks[$previousKey]) > 0) { /** @phpstan-ignore-line `Comparison operation ">" between 0 and 0 is always false.` I don't see anything wrong. Seems to be a PHPSTAN issue https://github.com/phpstan/phpstan/issues/3831 */
|
||||
$previousFilter = array_pop($arrStacks[$previousKey]);
|
||||
array_push($shrinkedFilters, $previousFilter);
|
||||
}
|
||||
|
||||
array_push($shrinkedFilters, $filter);
|
||||
}
|
||||
|
||||
$previousKey = $key;
|
||||
}
|
||||
|
||||
// add all grouped conditions back
|
||||
foreach ($arrStacks as $stack) {
|
||||
$groupedFilter = $this->groupFilters($stack);
|
||||
if (!empty($groupedFilter)) {
|
||||
$shrinkedFilters[] = $groupedFilter;
|
||||
}
|
||||
}
|
||||
|
||||
return $shrinkedFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $stack
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function groupFilters(array $stack): array
|
||||
{
|
||||
if (empty($stack)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count($stack) <= 1) {
|
||||
return $stack[0];
|
||||
}
|
||||
|
||||
$filter = $stack[0];
|
||||
$filter['operator'] = 'in';
|
||||
$filter['properties']['filter'] = $filter['filter'] = array_map(function ($ele) {
|
||||
return $ele['filter'];
|
||||
}, $stack);
|
||||
|
||||
return $filter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment;
|
||||
|
||||
use Mautic\LeadBundle\Provider\FilterOperatorProviderInterface;
|
||||
|
||||
class ContactSegmentFilterOperator
|
||||
{
|
||||
public function __construct(
|
||||
private FilterOperatorProviderInterface $filterOperatorProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $operator
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function fixOperator($operator)
|
||||
{
|
||||
$options = $this->filterOperatorProvider->getAllOperators();
|
||||
|
||||
if (empty($options[$operator])) {
|
||||
return $operator;
|
||||
}
|
||||
|
||||
$operatorDetails = $options[$operator];
|
||||
|
||||
return $operatorDetails['expr'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment;
|
||||
|
||||
/**
|
||||
* Array object containing filters.
|
||||
*/
|
||||
class ContactSegmentFilters implements \Iterator, \Countable
|
||||
{
|
||||
private int $position = 0;
|
||||
|
||||
/**
|
||||
* @var array|ContactSegmentFilter[]
|
||||
*/
|
||||
private array $contactSegmentFilters = [];
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function addContactSegmentFilter(ContactSegmentFilter $contactSegmentFilter)
|
||||
{
|
||||
$this->contactSegmentFilters[] = $contactSegmentFilter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element.
|
||||
*
|
||||
* @see http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return ContactSegmentFilter
|
||||
*/
|
||||
public function current(): mixed
|
||||
{
|
||||
return $this->contactSegmentFilters[$this->position];
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element.
|
||||
*
|
||||
* @see http://php.net/manual/en/iterator.next.php
|
||||
*/
|
||||
public function next(): void
|
||||
{
|
||||
++$this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element.
|
||||
*
|
||||
* @see http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key(): mixed
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid.
|
||||
*
|
||||
* @see http://php.net/manual/en/iterator.valid.php
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return isset($this->contactSegmentFilters[$this->position]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element.
|
||||
*
|
||||
* @see http://php.net/manual/en/iterator.rewind.php
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count elements of an object.
|
||||
*
|
||||
* @see http://php.net/manual/en/countable.count.php
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->contactSegmentFilters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Mautic\LeadBundle\Entity\RegexTrait;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterOperator;
|
||||
use Mautic\LeadBundle\Segment\Query\Filter\BaseFilterQueryBuilder;
|
||||
|
||||
class BaseDecorator implements FilterDecoratorInterface
|
||||
{
|
||||
use RegexTrait;
|
||||
|
||||
public function __construct(
|
||||
protected ContactSegmentFilterOperator $contactSegmentFilterOperator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $contactSegmentFilterCrate->getField();
|
||||
}
|
||||
|
||||
public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
if ($contactSegmentFilterCrate->isContactType()) {
|
||||
return MAUTIC_TABLE_PREFIX.'leads';
|
||||
}
|
||||
|
||||
if ($contactSegmentFilterCrate->isCompanyType()) {
|
||||
return MAUTIC_TABLE_PREFIX.'companies';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
$operator = $this->contactSegmentFilterOperator->fixOperator($contactSegmentFilterCrate->getOperator());
|
||||
|
||||
return match ($operator) {
|
||||
'startsWith', 'endsWith', 'contains' => 'like',
|
||||
default => $operator,
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return BaseFilterQueryBuilder::getServiceId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $argument
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument)
|
||||
{
|
||||
if (is_array($argument)) {
|
||||
$result = [];
|
||||
foreach ($argument as $arg) {
|
||||
$result[] = $this->getParameterHolder($contactSegmentFilterCrate, $arg);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
return ':'.$argument;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|bool|float|string|null
|
||||
*/
|
||||
public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate): mixed
|
||||
{
|
||||
$filter = $contactSegmentFilterCrate->getFilter();
|
||||
|
||||
if ($contactSegmentFilterCrate->filterValueDoNotNeedAdjustment()) {
|
||||
return $filter;
|
||||
}
|
||||
|
||||
switch ($contactSegmentFilterCrate->getOperator()) {
|
||||
case 'in':
|
||||
case '!in':
|
||||
return !is_array($filter) ? explode('|', $filter) : $filter;
|
||||
case 'like':
|
||||
case '!like':
|
||||
return !str_contains($filter, '%') ? '%'.$filter.'%' : $filter;
|
||||
case 'contains':
|
||||
return '%'.$filter.'%';
|
||||
case 'startsWith':
|
||||
return $filter.'%';
|
||||
case 'endsWith':
|
||||
return '%'.$filter;
|
||||
case 'regexp':
|
||||
case '!regexp':
|
||||
return $this->prepareRegex($filter);
|
||||
case 'multiselect':
|
||||
case '!multiselect':
|
||||
$filter = (array) $filter;
|
||||
|
||||
foreach ($filter as $key => $value) {
|
||||
$filter[$key] = sprintf('(([|]|^)%s([|]|$))', preg_quote($value, '/'));
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate): bool|string
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate): CompositeExpression|string|null
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Query\Filter\ComplexRelationValueFilterQueryBuilder;
|
||||
|
||||
class CompanyDecorator extends BaseDecorator
|
||||
{
|
||||
public function getRelationJoinTable(): string
|
||||
{
|
||||
return MAUTIC_TABLE_PREFIX.'companies_leads';
|
||||
}
|
||||
|
||||
public function getRelationJoinTableField(): string
|
||||
{
|
||||
return 'company_id';
|
||||
}
|
||||
|
||||
public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return ComplexRelationValueFilterQueryBuilder::getServiceId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
|
||||
interface ContactDecoratorForeignInterface
|
||||
{
|
||||
/**
|
||||
* Returns the name of a foreign contact column used in JOIN condition (usually contact_id or lead_id).
|
||||
*/
|
||||
public function getForeignContactColumn(ContactSegmentFilterCrate $contactSegmentFilterCrate): string;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Mautic\LeadBundle\Exception\FilterNotFoundException;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterOperator;
|
||||
use Mautic\LeadBundle\Services\ContactSegmentFilterDictionary;
|
||||
|
||||
class CustomMappedDecorator extends BaseDecorator implements ContactDecoratorForeignInterface
|
||||
{
|
||||
public function __construct(
|
||||
ContactSegmentFilterOperator $contactSegmentFilterOperator,
|
||||
protected ContactSegmentFilterDictionary $dictionary,
|
||||
) {
|
||||
parent::__construct($contactSegmentFilterOperator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
$originalField = $contactSegmentFilterCrate->getField();
|
||||
|
||||
try {
|
||||
return $this->dictionary->getFilterProperty($originalField, 'field');
|
||||
} catch (FilterNotFoundException) {
|
||||
return parent::getField($contactSegmentFilterCrate);
|
||||
}
|
||||
}
|
||||
|
||||
public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
$originalField = $contactSegmentFilterCrate->getField();
|
||||
|
||||
try {
|
||||
return MAUTIC_TABLE_PREFIX.$this->dictionary->getFilterProperty($originalField, 'foreign_table');
|
||||
} catch (FilterNotFoundException) {
|
||||
return parent::getTable($contactSegmentFilterCrate);
|
||||
}
|
||||
}
|
||||
|
||||
public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
$originalField = $contactSegmentFilterCrate->getField();
|
||||
|
||||
try {
|
||||
return $this->dictionary->getFilterProperty($originalField, 'type');
|
||||
} catch (FilterNotFoundException) {
|
||||
return parent::getQueryType($contactSegmentFilterCrate);
|
||||
}
|
||||
}
|
||||
|
||||
public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate): string|bool
|
||||
{
|
||||
$originalField = $contactSegmentFilterCrate->getField();
|
||||
|
||||
try {
|
||||
return $this->dictionary->getFilterProperty($originalField, 'func');
|
||||
} catch (FilterNotFoundException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate): CompositeExpression|string|null
|
||||
{
|
||||
$originalField = $contactSegmentFilterCrate->getField();
|
||||
|
||||
try {
|
||||
return $this->dictionary->getFilterProperty($originalField, 'where');
|
||||
} catch (FilterNotFoundException) {
|
||||
return parent::getWhere($contactSegmentFilterCrate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get foreign table field used in JOIN condition.
|
||||
*/
|
||||
public function getForeignContactColumn(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
$originalField = $contactSegmentFilterCrate->getField();
|
||||
|
||||
try {
|
||||
return $this->dictionary->getFilterProperty($originalField, 'foreign_table_field');
|
||||
} catch (FilterNotFoundException) {
|
||||
return 'lead_id';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Decorator\DateDecorator;
|
||||
use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface;
|
||||
|
||||
abstract class DateOptionAbstract implements FilterDecoratorInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected DateDecorator $dateDecorator,
|
||||
protected DateOptionParameters $dateOptionParameters,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is responsible for setting date. $this->dateTimeHelper holds date with midnight today.
|
||||
* Eg. +1 day for "tomorrow", -1 for yesterday etc.
|
||||
*/
|
||||
abstract protected function modifyBaseDate(DateTimeHelper $dateTimeHelper);
|
||||
|
||||
/**
|
||||
* This function is responsible for date modification for between operator.
|
||||
* Eg. +1 day for "today", "tomorrow" and "yesterday", +1 week for "this week", "last week", "next week" etc.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getModifierForBetweenRange();
|
||||
|
||||
/**
|
||||
* This function returns a value if between range is needed. Could return string for like operator or array for between operator
|
||||
* Eg. //LIKE 2018-01-23% for today, //LIKE 2017-12-% for last month, //LIKE 2017-% for last year, array for this week.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
abstract protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper);
|
||||
|
||||
/**
|
||||
* This function returns an operator if between range is needed. Could return like or between.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getOperatorForBetweenRange(ContactSegmentFilterCrate $leadSegmentFilterCrate);
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getField($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getTable($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
if ($this->dateOptionParameters->isBetweenRequired()) {
|
||||
return $this->getOperatorForBetweenRange($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
return $this->dateDecorator->getOperator($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $argument
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument)
|
||||
{
|
||||
return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|bool|float|string|null
|
||||
*/
|
||||
public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate): mixed
|
||||
{
|
||||
$dateTimeHelper = $this->dateOptionParameters->getDefaultDate();
|
||||
|
||||
$this->modifyBaseDate($dateTimeHelper);
|
||||
|
||||
$dateFormat = $this->dateOptionParameters->hasTimePart() ? 'Y-m-d H:i:s' : 'Y-m-d';
|
||||
|
||||
if ($this->dateOptionParameters->isBetweenRequired()) {
|
||||
return $this->getValueForBetweenRange($dateTimeHelper);
|
||||
}
|
||||
|
||||
if ($this->dateOptionParameters->shouldUseLastDayOfRange()) {
|
||||
$modifier = $this->getModifierForBetweenRange();
|
||||
$modifier .= ' -1 second';
|
||||
$dateTimeHelper->modify($modifier);
|
||||
}
|
||||
|
||||
return $dateTimeHelper->toLocalString($dateFormat);
|
||||
}
|
||||
|
||||
public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return $this->dateDecorator->getQueryType($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate): string|bool
|
||||
{
|
||||
return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\DBAL\Query\Expression\CompositeExpression|string|null
|
||||
*/
|
||||
public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getWhere($contactSegmentFilterCrate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayToday;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayTomorrow;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayYesterday;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthLast;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthNext;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthThis;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateAnniversary;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateDefault;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearLast;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearNext;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearThis;
|
||||
use Mautic\LeadBundle\Segment\Decorator\DateDecorator;
|
||||
use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface;
|
||||
use Mautic\LeadBundle\Segment\RelativeDate;
|
||||
|
||||
class DateOptionFactory
|
||||
{
|
||||
public function __construct(
|
||||
private DateDecorator $dateDecorator,
|
||||
private RelativeDate $relativeDate,
|
||||
private TimezoneResolver $timezoneResolver,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getDateOption(ContactSegmentFilterCrate $leadSegmentFilterCrate): FilterDecoratorInterface
|
||||
{
|
||||
$originalValue = $leadSegmentFilterCrate->getFilter();
|
||||
$relativeDateStrings = $this->relativeDate->getRelativeDateStrings();
|
||||
$dateOptionParameters = new DateOptionParameters($leadSegmentFilterCrate, $relativeDateStrings, $this->timezoneResolver);
|
||||
$timeframe = $dateOptionParameters->getTimeframe();
|
||||
|
||||
if (!$timeframe) {
|
||||
return new DateDefault($this->dateDecorator, $originalValue);
|
||||
}
|
||||
|
||||
switch ($timeframe) {
|
||||
case 'birthday':
|
||||
case 'anniversary':
|
||||
case $timeframe && (
|
||||
str_contains($timeframe, 'anniversary')
|
||||
|| str_contains($timeframe, 'birthday')
|
||||
):
|
||||
return new DateAnniversary($this->dateDecorator, $dateOptionParameters);
|
||||
case 'today':
|
||||
return new DateDayToday($this->dateDecorator, $dateOptionParameters);
|
||||
case 'tomorrow':
|
||||
return new DateDayTomorrow($this->dateDecorator, $dateOptionParameters);
|
||||
case 'yesterday':
|
||||
return new DateDayYesterday($this->dateDecorator, $dateOptionParameters);
|
||||
case 'week_last':
|
||||
return new DateWeekLast($this->dateDecorator, $dateOptionParameters);
|
||||
case 'week_next':
|
||||
return new DateWeekNext($this->dateDecorator, $dateOptionParameters);
|
||||
case 'week_this':
|
||||
return new DateWeekThis($this->dateDecorator, $dateOptionParameters);
|
||||
case 'month_last':
|
||||
return new DateMonthLast($this->dateDecorator, $dateOptionParameters);
|
||||
case 'month_next':
|
||||
return new DateMonthNext($this->dateDecorator, $dateOptionParameters);
|
||||
case 'month_this':
|
||||
return new DateMonthThis($this->dateDecorator, $dateOptionParameters);
|
||||
case 'year_last':
|
||||
return new DateYearLast($this->dateDecorator, $dateOptionParameters);
|
||||
case 'year_next':
|
||||
return new DateYearNext($this->dateDecorator, $dateOptionParameters);
|
||||
case 'year_this':
|
||||
return new DateYearThis($this->dateDecorator, $dateOptionParameters);
|
||||
case $timeframe && (
|
||||
str_contains($timeframe[0], '-') // -5 days
|
||||
|| str_contains($timeframe[0], '+') // +5 days
|
||||
|| false !== $this->isRelativeFormatsPresent($timeframe)
|
||||
):
|
||||
return new DateRelativeInterval($this->dateDecorator, $originalValue, $dateOptionParameters);
|
||||
default:
|
||||
return new DateDefault($this->dateDecorator, $originalValue);
|
||||
}
|
||||
}
|
||||
|
||||
protected function isRelativeFormatsPresent(string $timeframe): bool
|
||||
{
|
||||
$notations = [
|
||||
'first day of ', // first day of January 2021
|
||||
'last day of ', // last day of January 2021
|
||||
' ago', // 5 days ago
|
||||
];
|
||||
|
||||
foreach ($notations as $notation) {
|
||||
if (str_contains($timeframe, $notation)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
|
||||
class DateOptionParameters
|
||||
{
|
||||
private bool $hasTimePart;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
private $timeframe;
|
||||
|
||||
private bool $requiresBetween;
|
||||
|
||||
private bool $shouldUseLastDayOfRange;
|
||||
|
||||
private DateTimeHelper $dateTimeHelper;
|
||||
|
||||
public function __construct(
|
||||
ContactSegmentFilterCrate $leadSegmentFilterCrate,
|
||||
array $relativeDateStrings,
|
||||
TimezoneResolver $timezoneResolver,
|
||||
) {
|
||||
$this->hasTimePart = $leadSegmentFilterCrate->hasTimeParts();
|
||||
$this->timeframe = $this->parseTimeFrame($leadSegmentFilterCrate, $relativeDateStrings);
|
||||
$this->requiresBetween = in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true);
|
||||
$this->shouldUseLastDayOfRange = in_array($leadSegmentFilterCrate->getOperator(), ['gt', 'lte'], true);
|
||||
|
||||
$this->setDateTimeHelper($timezoneResolver);
|
||||
}
|
||||
|
||||
public function hasTimePart(): bool
|
||||
{
|
||||
return $this->hasTimePart;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTimeframe()
|
||||
{
|
||||
return $this->timeframe;
|
||||
}
|
||||
|
||||
public function isBetweenRequired(): bool
|
||||
{
|
||||
return $this->requiresBetween;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function indicates that we need to modify date to the last date of range.
|
||||
* "Less than or equal" operator means that we need to include whole week / month / year > last day from range
|
||||
* "Grater than" needs same logic.
|
||||
*/
|
||||
public function shouldUseLastDayOfRange(): bool
|
||||
{
|
||||
return $this->shouldUseLastDayOfRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DateTimeHelper
|
||||
*/
|
||||
public function getDefaultDate()
|
||||
{
|
||||
return $this->dateTimeHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|mixed
|
||||
*/
|
||||
private function parseTimeFrame(ContactSegmentFilterCrate $leadSegmentFilterCrate, array $relativeDateStrings)
|
||||
{
|
||||
$key = array_search($leadSegmentFilterCrate->getFilter(), $relativeDateStrings, true);
|
||||
|
||||
if (false === $key) {
|
||||
// Time frame does not match any option from $relativeDateStrings, so return original value
|
||||
return $leadSegmentFilterCrate->getFilter();
|
||||
}
|
||||
|
||||
return str_replace('mautic.lead.list.', '', $key);
|
||||
}
|
||||
|
||||
private function setDateTimeHelper(TimezoneResolver $timezoneResolver): void
|
||||
{
|
||||
$this->dateTimeHelper = $timezoneResolver->getDefaultDate($this->hasTimePart());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Day;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract;
|
||||
|
||||
abstract class DateDayAbstract extends DateOptionAbstract
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getModifierForBetweenRange()
|
||||
{
|
||||
return '+1 day';
|
||||
}
|
||||
|
||||
protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
return $dateTimeHelper->toLocalString('Y-m-d%');
|
||||
}
|
||||
|
||||
protected function getOperatorForBetweenRange(ContactSegmentFilterCrate $leadSegmentFilterCrate)
|
||||
{
|
||||
return '!=' === $leadSegmentFilterCrate->getOperator() ? 'notLike' : 'like';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Day;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateDayToday extends DateDayAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->modify('midnight today');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Day;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateDayTomorrow extends DateDayAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->modify('midnight tomorrow');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Day;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateDayYesterday extends DateDayAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->modify('midnight yesterday');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Month;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract;
|
||||
|
||||
abstract class DateMonthAbstract extends DateOptionAbstract
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getModifierForBetweenRange()
|
||||
{
|
||||
return '+1 month';
|
||||
}
|
||||
|
||||
protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
return $dateTimeHelper->toLocalString('Y-m-%');
|
||||
}
|
||||
|
||||
protected function getOperatorForBetweenRange(ContactSegmentFilterCrate $leadSegmentFilterCrate)
|
||||
{
|
||||
return '!=' === $leadSegmentFilterCrate->getOperator() ? 'notLike' : 'like';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Month;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateMonthLast extends DateMonthAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->setDateTime('midnight first day of last month', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Month;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateMonthNext extends DateMonthAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->setDateTime('midnight first day of next month', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Month;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateMonthThis extends DateMonthAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->setDateTime('midnight first day of this month', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Other;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters;
|
||||
use Mautic\LeadBundle\Segment\Decorator\DateDecorator;
|
||||
use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface;
|
||||
|
||||
class DateAnniversary implements FilterDecoratorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DateDecorator $dateDecorator,
|
||||
private DateOptionParameters $dateOptionParameters,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getField($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return $this->dateDecorator->getTable($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return 'like';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $argument
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument)
|
||||
{
|
||||
return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|bool|float|string|null
|
||||
*/
|
||||
public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate): mixed
|
||||
{
|
||||
$date = $this->dateOptionParameters->getDefaultDate();
|
||||
$filter = $contactSegmentFilterCrate->getFilter();
|
||||
$relativeFilter = is_string($filter) ? trim(str_replace(['anniversary', 'birthday'], '', $filter)) : $filter;
|
||||
|
||||
if ($relativeFilter) {
|
||||
$date->modify($relativeFilter);
|
||||
}
|
||||
|
||||
return $date->toLocalString('%-m-d%');
|
||||
}
|
||||
|
||||
public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return $this->dateDecorator->getQueryType($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate): string|bool
|
||||
{
|
||||
return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate): CompositeExpression|string|null
|
||||
{
|
||||
return $this->dateDecorator->getWhere($contactSegmentFilterCrate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Other;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Decorator\DateDecorator;
|
||||
use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface;
|
||||
|
||||
class DateDefault implements FilterDecoratorInterface
|
||||
{
|
||||
/**
|
||||
* @param string $originalValue
|
||||
*/
|
||||
public function __construct(
|
||||
private DateDecorator $dateDecorator,
|
||||
private $originalValue,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getField($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return $this->dateDecorator->getTable($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getOperator($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $argument
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument)
|
||||
{
|
||||
return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|bool|float|string|null
|
||||
*/
|
||||
public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate): mixed
|
||||
{
|
||||
$filter = $this->originalValue;
|
||||
|
||||
return match ($contactSegmentFilterCrate->getOperator()) {
|
||||
'like', '!like' => !str_contains($filter, '%') ? '%'.$filter.'%' : $filter,
|
||||
'contains' => '%'.$filter.'%',
|
||||
'startsWith' => $filter.'%',
|
||||
'endsWith' => '%'.$filter,
|
||||
default => $this->originalValue,
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return $this->dateDecorator->getQueryType($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate): string|bool
|
||||
{
|
||||
return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate): CompositeExpression|string|null
|
||||
{
|
||||
return $this->dateDecorator->getWhere($contactSegmentFilterCrate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Other;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters;
|
||||
use Mautic\LeadBundle\Segment\Decorator\DateDecorator;
|
||||
use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface;
|
||||
|
||||
class DateRelativeInterval implements FilterDecoratorInterface
|
||||
{
|
||||
/**
|
||||
* @param string $originalValue
|
||||
*/
|
||||
public function __construct(
|
||||
private DateDecorator $dateDecorator,
|
||||
private $originalValue,
|
||||
private DateOptionParameters $dateOptionParameters,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getField($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return $this->dateDecorator->getTable($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
if ('=' === $contactSegmentFilterCrate->getOperator()) {
|
||||
return 'like';
|
||||
}
|
||||
if ('!=' === $contactSegmentFilterCrate->getOperator()) {
|
||||
return 'notLike';
|
||||
}
|
||||
|
||||
return $this->dateDecorator->getOperator($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $argument
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument)
|
||||
{
|
||||
return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|bool|float|string|null
|
||||
*/
|
||||
public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate): mixed
|
||||
{
|
||||
$date = $this->dateOptionParameters->getDefaultDate();
|
||||
$date->modify($this->originalValue);
|
||||
|
||||
$operator = $this->getOperator($contactSegmentFilterCrate);
|
||||
$format = 'Y-m-d';
|
||||
if ('like' === $operator || 'notLike' === $operator) {
|
||||
$format .= '%';
|
||||
}
|
||||
|
||||
return $date->toLocalString($format);
|
||||
}
|
||||
|
||||
public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return $this->dateDecorator->getQueryType($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate): string|bool
|
||||
{
|
||||
return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate): CompositeExpression|string|null
|
||||
{
|
||||
return $this->dateDecorator->getWhere($contactSegmentFilterCrate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class TimezoneResolver
|
||||
{
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $hasTimePart
|
||||
*/
|
||||
public function getDefaultDate($hasTimePart): DateTimeHelper
|
||||
{
|
||||
/**
|
||||
* $hasTimePart tells us if field in a database is date or datetime
|
||||
* All datetime fields are stored in UTC
|
||||
* Date field, however, is always stored in a local time (there is no time information, so it cannot be converted to UTC).
|
||||
*
|
||||
* We will generate default date according to this. We need midnight as a default date (for relative intervals like "today" or "-1 day"
|
||||
* 1) in UTC for datetime fields
|
||||
* 2) in the local timezone for date fields
|
||||
*
|
||||
* Later we use toLocalString() method - it gives us midnight in UTC for first condition and midnight in local timezone for second option.
|
||||
*/
|
||||
$timezone = $hasTimePart ? 'UTC' : $this->coreParametersHelper->getDefaultTimezone();
|
||||
|
||||
$date = new \DateTime('midnight today', new \DateTimeZone($timezone));
|
||||
|
||||
return new DateTimeHelper($date, null, $timezone);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Week;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract;
|
||||
|
||||
abstract class DateWeekAbstract extends DateOptionAbstract
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getModifierForBetweenRange()
|
||||
{
|
||||
return '+1 week';
|
||||
}
|
||||
|
||||
protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateFormat = $this->dateOptionParameters->hasTimePart() ? 'Y-m-d H:i:s' : 'Y-m-d';
|
||||
$startWith = $dateTimeHelper->toLocalString($dateFormat);
|
||||
|
||||
$modifier = $this->getModifierForBetweenRange().' -1 second';
|
||||
$dateTimeHelper->modify($modifier);
|
||||
$endWith = $dateTimeHelper->toLocalString($dateFormat);
|
||||
|
||||
return [$startWith, $endWith];
|
||||
}
|
||||
|
||||
protected function getOperatorForBetweenRange(ContactSegmentFilterCrate $leadSegmentFilterCrate)
|
||||
{
|
||||
return '!=' === $leadSegmentFilterCrate->getOperator() ? 'notBetween' : 'between';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Week;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateWeekLast extends DateWeekAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->setDateTime('midnight monday last week', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Week;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateWeekNext extends DateWeekAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->setDateTime('midnight monday next week', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Week;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateWeekThis extends DateWeekAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->setDateTime('midnight monday this week', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Year;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract;
|
||||
|
||||
abstract class DateYearAbstract extends DateOptionAbstract
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getModifierForBetweenRange()
|
||||
{
|
||||
return '+1 year';
|
||||
}
|
||||
|
||||
protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
return $dateTimeHelper->toLocalString('Y-%');
|
||||
}
|
||||
|
||||
protected function getOperatorForBetweenRange(ContactSegmentFilterCrate $leadSegmentFilterCrate)
|
||||
{
|
||||
return '!=' === $leadSegmentFilterCrate->getOperator() ? 'notLike' : 'like';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Year;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateYearLast extends DateYearAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->setDateTime('midnight first day of January last year', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Year;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateYearNext extends DateYearAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->setDateTime('midnight first day of January next year', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator\Date\Year;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
class DateYearThis extends DateYearAbstract
|
||||
{
|
||||
protected function modifyBaseDate(DateTimeHelper $dateTimeHelper)
|
||||
{
|
||||
$dateTimeHelper->setDateTime('midnight first day of January this year', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Query\Filter\ComplexRelationValueFilterQueryBuilder;
|
||||
|
||||
class DateCompanyDecorator implements FilterDecoratorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private FilterDecoratorInterface $dateDecorator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getField($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getTable($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getOperator($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $argument
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument)
|
||||
{
|
||||
return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|bool|float|string|null
|
||||
*/
|
||||
public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate): mixed
|
||||
{
|
||||
return $this->dateDecorator->getParameterValue($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate): string
|
||||
{
|
||||
return ComplexRelationValueFilterQueryBuilder::getServiceId();
|
||||
}
|
||||
|
||||
public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate): string|bool
|
||||
{
|
||||
return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\DBAL\Query\Expression\CompositeExpression|string|null
|
||||
*/
|
||||
public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
return $this->dateDecorator->getWhere($contactSegmentFilterCrate);
|
||||
}
|
||||
|
||||
public function getRelationJoinTable(): string
|
||||
{
|
||||
return MAUTIC_TABLE_PREFIX.'companies_leads';
|
||||
}
|
||||
|
||||
public function getRelationJoinTableField(): string
|
||||
{
|
||||
return 'company_id';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
|
||||
class DateDecorator extends CustomMappedDecorator
|
||||
{
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate): mixed
|
||||
{
|
||||
throw new \Exception('Instance of Date option needs to implement this function');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator;
|
||||
|
||||
use Mautic\LeadBundle\Event\LeadListFiltersDecoratorDelegateEvent;
|
||||
use Mautic\LeadBundle\Exception\FilterNotFoundException;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory;
|
||||
use Mautic\LeadBundle\Services\ContactSegmentFilterDictionary;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class DecoratorFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ContactSegmentFilterDictionary $contactSegmentFilterDictionary,
|
||||
private BaseDecorator $baseDecorator,
|
||||
private CustomMappedDecorator $customMappedDecorator,
|
||||
private DateOptionFactory $dateOptionFactory,
|
||||
private CompanyDecorator $companyDecorator,
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FilterDecoratorInterface
|
||||
*/
|
||||
public function getDecoratorForFilter(ContactSegmentFilterCrate $contactSegmentFilterCrate)
|
||||
{
|
||||
$decoratorEvent = new LeadListFiltersDecoratorDelegateEvent($contactSegmentFilterCrate);
|
||||
|
||||
$this->eventDispatcher->dispatch($decoratorEvent, LeadEvents::SEGMENT_ON_DECORATOR_DELEGATE);
|
||||
if ($decorator = $decoratorEvent->getDecorator()) {
|
||||
return $decorator;
|
||||
}
|
||||
|
||||
if ($contactSegmentFilterCrate->isDateType()) {
|
||||
$dateDecorator = $this->dateOptionFactory->getDateOption($contactSegmentFilterCrate);
|
||||
|
||||
if ($contactSegmentFilterCrate->isCompanyType()) {
|
||||
return new DateCompanyDecorator($dateDecorator);
|
||||
}
|
||||
|
||||
return $dateDecorator;
|
||||
}
|
||||
|
||||
$originalField = $contactSegmentFilterCrate->getField();
|
||||
|
||||
try {
|
||||
$this->contactSegmentFilterDictionary->getFilter($originalField);
|
||||
|
||||
return $this->customMappedDecorator;
|
||||
} catch (FilterNotFoundException) {
|
||||
if ($contactSegmentFilterCrate->isCompanyType()) {
|
||||
return $this->companyDecorator;
|
||||
}
|
||||
|
||||
return $this->baseDecorator;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Decorator;
|
||||
|
||||
use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate;
|
||||
|
||||
interface FilterDecoratorInterface
|
||||
{
|
||||
/**
|
||||
* Returns filter's field (usually a column name in DB).
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate);
|
||||
|
||||
/**
|
||||
* Returns DB table.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate);
|
||||
|
||||
/**
|
||||
* Returns a string operator (like, eq, neq, ...).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate);
|
||||
|
||||
/**
|
||||
* Returns an argument for QueryBuilder (usually ':arg' in case that $argument is equal to 'arg' string.
|
||||
*
|
||||
* @param array|string $argument
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument);
|
||||
|
||||
/**
|
||||
* Returns formatted value for QueryBuilder ('%value%' for 'like', '%value' for 'Ends with', SQL-formatted date etc.).
|
||||
*
|
||||
* @return array|bool|float|string|null
|
||||
*/
|
||||
public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate);
|
||||
|
||||
/**
|
||||
* Returns QueryBuilder's service name from the container.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate);
|
||||
|
||||
/**
|
||||
* Returns a name of aggregation function for SQL (SUM, COUNT etc.)
|
||||
* Returns false if no aggregation function is needed.
|
||||
*
|
||||
* @return string|bool if no func needed
|
||||
*/
|
||||
public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate): string|bool;
|
||||
|
||||
/**
|
||||
* Returns a special where condition which is needed to be added to QueryBuilder (like email_stats.is_read = 1 for 'Read emails')
|
||||
* Returns null if no special condition is needed.
|
||||
*
|
||||
* @return \Doctrine\DBAL\Query\Expression\CompositeExpression|string|null
|
||||
*/
|
||||
public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\DoNotContact;
|
||||
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
|
||||
class DoNotContactParts
|
||||
{
|
||||
private string $channel = 'email';
|
||||
|
||||
private int $type = DoNotContact::UNSUBSCRIBED;
|
||||
|
||||
public function __construct(?string $field)
|
||||
{
|
||||
if ($field && str_contains($field, '_manual')) {
|
||||
$this->type = DoNotContact::MANUAL;
|
||||
}
|
||||
|
||||
if ($field && str_contains($field, '_bounced')) {
|
||||
$this->type = DoNotContact::BOUNCED;
|
||||
}
|
||||
|
||||
if ($field && str_contains($field, '_sms')) {
|
||||
$this->channel = 'sms';
|
||||
}
|
||||
}
|
||||
|
||||
public function getChannel(): string
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
public function getParameterType(): int
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Exception;
|
||||
|
||||
class FieldNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Exception;
|
||||
|
||||
/**
|
||||
* This exception is risen if functionality requested does not belong to give FilterQueryBuilder.
|
||||
*/
|
||||
class InvalidUseException extends SegmentQueryException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Exception;
|
||||
|
||||
class PluginHandledFilterException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Exception;
|
||||
|
||||
class SegmentNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Exception;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryException;
|
||||
|
||||
class SegmentQueryException extends QueryException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Exception;
|
||||
|
||||
class TableNotFoundException extends SegmentQueryException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\IntegrationCampaign;
|
||||
|
||||
class IntegrationCampaignParts
|
||||
{
|
||||
private string $integrationName;
|
||||
|
||||
private string $campaignId;
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
*/
|
||||
public function __construct($field)
|
||||
{
|
||||
if (str_contains($field, '::')) {
|
||||
[$integrationName, $campaignId] = explode('::', $field);
|
||||
} else {
|
||||
// Assuming this is a Salesforce integration for BC with pre 2.11.0
|
||||
$integrationName = 'Salesforce';
|
||||
$campaignId = $field;
|
||||
}
|
||||
$this->integrationName = $integrationName;
|
||||
$this->campaignId = $campaignId;
|
||||
}
|
||||
|
||||
public function getIntegrationName(): string
|
||||
{
|
||||
return $this->integrationName;
|
||||
}
|
||||
|
||||
public function getCampaignId(): string
|
||||
{
|
||||
return $this->campaignId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment;
|
||||
|
||||
class OperatorOptions
|
||||
{
|
||||
public const EQUAL_TO = '=';
|
||||
|
||||
public const NOT_EQUAL_TO = '!=';
|
||||
|
||||
public const GREATER_THAN = 'gt';
|
||||
|
||||
public const GREATER_THAN_OR_EQUAL = 'gte';
|
||||
|
||||
public const LESS_THAN = 'lt';
|
||||
|
||||
public const LESS_THAN_OR_EQUAL = 'lte';
|
||||
|
||||
public const EMPTY = 'empty';
|
||||
|
||||
public const NOT_EMPTY = '!empty';
|
||||
|
||||
public const LIKE = 'like';
|
||||
|
||||
public const NOT_LIKE = '!like';
|
||||
|
||||
public const BETWEEN = 'between';
|
||||
|
||||
public const NOT_BETWEEN = '!between';
|
||||
|
||||
/**
|
||||
* @deprecated Use OperatorOptions::INCLUDING_ANY
|
||||
*/
|
||||
public const IN = 'in';
|
||||
|
||||
/**
|
||||
* @deprecated Use OperatorOptions::EXCLUDING_ANY
|
||||
*/
|
||||
public const NOT_IN = '!in';
|
||||
|
||||
public const INCLUDING_ANY = 'in';
|
||||
|
||||
public const EXCLUDING_ANY = '!in';
|
||||
|
||||
public const INCLUDING_ALL = 'in_all';
|
||||
|
||||
public const EXCLUDING_ALL = '!in_all';
|
||||
|
||||
public const REGEXP = 'regexp';
|
||||
|
||||
public const NOT_REGEXP = '!regexp';
|
||||
|
||||
public const DATE = 'date';
|
||||
|
||||
public const STARTS_WITH = 'startsWith';
|
||||
|
||||
public const ENDS_WITH = 'endsWith';
|
||||
|
||||
public const CONTAINS = 'contains';
|
||||
|
||||
/**
|
||||
* @var array<string,array<string,string|bool>>
|
||||
*/
|
||||
private static array $operatorOptions = [
|
||||
self::EQUAL_TO => [
|
||||
'label' => 'mautic.lead.list.form.operator.equals',
|
||||
'expr' => 'eq',
|
||||
'negate_expr' => 'neq',
|
||||
],
|
||||
self::NOT_EQUAL_TO => [
|
||||
'label' => 'mautic.lead.list.form.operator.notequals',
|
||||
'expr' => 'neq',
|
||||
'negate_expr' => 'eq',
|
||||
],
|
||||
self::GREATER_THAN => [
|
||||
'label' => 'mautic.lead.list.form.operator.greaterthan',
|
||||
'expr' => 'gt',
|
||||
'negate_expr' => 'lt',
|
||||
],
|
||||
self::GREATER_THAN_OR_EQUAL => [
|
||||
'label' => 'mautic.lead.list.form.operator.greaterthanequals',
|
||||
'expr' => 'gte',
|
||||
'negate_expr' => 'lt',
|
||||
],
|
||||
self::LESS_THAN => [
|
||||
'label' => 'mautic.lead.list.form.operator.lessthan',
|
||||
'expr' => 'lt',
|
||||
'negate_expr' => 'gt',
|
||||
],
|
||||
self::LESS_THAN_OR_EQUAL => [
|
||||
'label' => 'mautic.lead.list.form.operator.lessthanequals',
|
||||
'expr' => 'lte',
|
||||
'negate_expr' => 'gt',
|
||||
],
|
||||
self::EMPTY => [
|
||||
'label' => 'mautic.lead.list.form.operator.isempty',
|
||||
'expr' => 'empty', // special case
|
||||
'negate_expr' => 'notEmpty',
|
||||
],
|
||||
self::NOT_EMPTY => [
|
||||
'label' => 'mautic.lead.list.form.operator.isnotempty',
|
||||
'expr' => 'notEmpty', // special case
|
||||
'negate_expr' => 'empty',
|
||||
],
|
||||
self::LIKE => [
|
||||
'label' => 'mautic.lead.list.form.operator.islike',
|
||||
'expr' => 'like',
|
||||
'negate_expr' => 'notLike',
|
||||
],
|
||||
self::NOT_LIKE => [
|
||||
'label' => 'mautic.lead.list.form.operator.isnotlike',
|
||||
'expr' => 'notLike',
|
||||
'negate_expr' => 'like',
|
||||
],
|
||||
self::BETWEEN => [
|
||||
'label' => 'mautic.lead.list.form.operator.between',
|
||||
'expr' => 'between', // special case
|
||||
'negate_expr' => 'notBetween',
|
||||
'hide' => true,
|
||||
],
|
||||
self::NOT_BETWEEN => [
|
||||
'label' => 'mautic.lead.list.form.operator.notbetween',
|
||||
'expr' => 'notBetween', // special case
|
||||
'negate_expr' => 'between',
|
||||
'hide' => true,
|
||||
],
|
||||
self::INCLUDING_ANY => [
|
||||
'label' => 'mautic.lead.list.form.operator.in',
|
||||
'expr' => 'in',
|
||||
'negate_expr' => 'notIn',
|
||||
],
|
||||
self::EXCLUDING_ANY => [
|
||||
'label' => 'mautic.lead.list.form.operator.notin',
|
||||
'expr' => 'notIn',
|
||||
'negate_expr' => 'in',
|
||||
],
|
||||
self::INCLUDING_ALL => [
|
||||
'label' => 'mautic.lead.list.form.operator.in_all',
|
||||
'expr' => self::INCLUDING_ALL,
|
||||
'negate_expr' => self::EXCLUDING_ALL,
|
||||
],
|
||||
self::EXCLUDING_ALL => [
|
||||
'label' => 'mautic.lead.list.form.operator.notin_all',
|
||||
'expr' => self::EXCLUDING_ALL,
|
||||
'negate_expr' => self::INCLUDING_ALL,
|
||||
],
|
||||
self::REGEXP => [
|
||||
'label' => 'mautic.lead.list.form.operator.regexp',
|
||||
'expr' => 'regexp', // special case
|
||||
'negate_expr' => 'notRegexp',
|
||||
],
|
||||
self::NOT_REGEXP => [
|
||||
'label' => 'mautic.lead.list.form.operator.notregexp',
|
||||
'expr' => 'notRegexp', // special case
|
||||
'negate_expr' => 'regexp',
|
||||
],
|
||||
self::DATE => [
|
||||
'label' => 'mautic.lead.list.form.operator.date',
|
||||
'expr' => 'date', // special case
|
||||
'negate_expr' => 'date',
|
||||
'hide' => true,
|
||||
],
|
||||
self::STARTS_WITH => [
|
||||
'label' => 'mautic.core.operator.starts.with',
|
||||
'expr' => 'startsWith',
|
||||
'negate_expr' => 'startsWith',
|
||||
],
|
||||
self::ENDS_WITH => [
|
||||
'label' => 'mautic.core.operator.ends.with',
|
||||
'expr' => 'endsWith',
|
||||
'negate_expr' => 'endsWith',
|
||||
],
|
||||
self::CONTAINS => [
|
||||
'label' => 'mautic.core.operator.contains',
|
||||
'expr' => 'contains',
|
||||
'negate_expr' => 'contains',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<string,array<string,string>>
|
||||
*/
|
||||
public static function getFilterExpressionFunctions()
|
||||
{
|
||||
return self::$operatorOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array<string,string>>
|
||||
*/
|
||||
public function getFilterExpressionFunctionsNonStatic()
|
||||
{
|
||||
return self::$operatorOptions;
|
||||
}
|
||||
}
|
||||
@@ -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).'.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment;
|
||||
|
||||
class RandomParameterName
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $lastUsedParameterId = 0;
|
||||
|
||||
/**
|
||||
* Generate a unique parameter name from int using base conversion.
|
||||
* This eliminates chance for parameter name collision and provides unique result for each number.
|
||||
*
|
||||
* @see https://stackoverflow.com/questions/307486/short-unique-id-in-php/1516430#1516430
|
||||
*/
|
||||
public function generateRandomParameterName(): string
|
||||
{
|
||||
$value = base_convert((string) $this->lastUsedParameterId, 10, 36);
|
||||
|
||||
++$this->lastUsedParameterId;
|
||||
|
||||
return 'par'.$value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment;
|
||||
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class RelativeDate
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRelativeDateStrings(): array
|
||||
{
|
||||
$keys = $this->getRelativeDateTranslationKeys();
|
||||
|
||||
$strings = [];
|
||||
foreach ($keys as $key) {
|
||||
$strings[$key] = $this->translator->trans($key);
|
||||
}
|
||||
|
||||
return $strings;
|
||||
}
|
||||
|
||||
private function getRelativeDateTranslationKeys(): array
|
||||
{
|
||||
return [
|
||||
'mautic.lead.list.month_last',
|
||||
'mautic.lead.list.month_next',
|
||||
'mautic.lead.list.month_this',
|
||||
'mautic.lead.list.today',
|
||||
'mautic.lead.list.tomorrow',
|
||||
'mautic.lead.list.yesterday',
|
||||
'mautic.lead.list.week_last',
|
||||
'mautic.lead.list.week_next',
|
||||
'mautic.lead.list.week_this',
|
||||
'mautic.lead.list.year_last',
|
||||
'mautic.lead.list.year_next',
|
||||
'mautic.lead.list.year_this',
|
||||
'mautic.lead.list.anniversary',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Segment;
|
||||
|
||||
trait SegmentFilterIconTrait
|
||||
{
|
||||
public function getSegmentFilterIcon(string $filterType): string
|
||||
{
|
||||
return match ($filterType) {
|
||||
// lead
|
||||
'address1' => 'ri-home-2-line',
|
||||
'address2' => 'ri-home-3-line',
|
||||
'attribution' => 'ri-cash-line',
|
||||
'attribution_date' => 'ri-calendar-event-line',
|
||||
'dnc_bounced' => 'ri-mail-close-line',
|
||||
'dnc_bounced_sms' => 'ri-chat-delete-line',
|
||||
'campaign' => 'ri-megaphone-line',
|
||||
'city' => 'ri-building-2-line',
|
||||
'country' => 'ri-earth-line',
|
||||
'date_added' => 'ri-calendar-check-line',
|
||||
'date_identified' => 'ri-calendar-todo-line',
|
||||
'last_active' => 'ri-time-line',
|
||||
'device_brand' => 'ri-smartphone-line',
|
||||
'device_model' => 'ri-device-line',
|
||||
'device_os' => 'ri-window-2-line',
|
||||
'device_type' => 'ri-computer-line',
|
||||
'email' => 'ri-mail-line',
|
||||
'generated_email_domain' => 'ri-at-line',
|
||||
'facebook' => 'ri-facebook-box-line',
|
||||
'fax' => 'ri-printer-line',
|
||||
'firstname' => 'ri-user-line',
|
||||
'foursquare' => 'ri-map-pin-user-line',
|
||||
'instagram' => 'ri-instagram-line',
|
||||
'lastname' => 'ri-user-2-line',
|
||||
'mobile' => 'ri-smartphone-line',
|
||||
'date_modified' => 'ri-calendar-event-line',
|
||||
'owner_id' => 'ri-user-star-line',
|
||||
'phone' => 'ri-phone-line',
|
||||
'points' => 'ri-coins-line',
|
||||
'position' => 'ri-briefcase-4-line',
|
||||
'preferred_locale' => 'ri-translate-2',
|
||||
'timezone' => 'ri-time-zone-line',
|
||||
'company' => 'ri-building-4-line',
|
||||
'leadlist' => 'ri-list-check-2',
|
||||
'skype' => 'ri-skype-line',
|
||||
'stage' => 'ri-barricade-line',
|
||||
'state' => 'ri-map-pin-2-line',
|
||||
'globalcategory' => 'ri-folder-2-line',
|
||||
'tags' => 'ri-hashtag',
|
||||
'title' => 'ri-user-star-line',
|
||||
'twitter' => 'ri-twitter-x-line',
|
||||
'utm_campaign' => 'ri-bookmark-2-line',
|
||||
'utm_content' => 'ri-file-text-line',
|
||||
'utm_medium' => 'ri-share-line',
|
||||
'utm_source' => 'ri-link',
|
||||
'utm_term' => 'ri-hashtag',
|
||||
'dnc_unsubscribed' => 'ri-forbid-2-line',
|
||||
'dnc_unsubscribed_sms' => 'ri-forbid-2-line',
|
||||
'dnc_manual_email' => 'ri-mail-forbid-line',
|
||||
'dnc_manual_sms' => 'ri-chat-off-line',
|
||||
'website' => 'ri-global-line',
|
||||
'zipcode' => 'ri-mail-send-line',
|
||||
'linkedin' => 'ri-linkedin-box-line',
|
||||
|
||||
// company
|
||||
'companyaddress1' => 'ri-building-line',
|
||||
'companyaddress2' => 'ri-building-2-line',
|
||||
'companyannual_revenue' => 'ri-money-dollar-circle-line',
|
||||
'companycity' => 'ri-building-4-line',
|
||||
'companyemail' => 'ri-mail-line',
|
||||
'companyname' => 'ri-building-3-line',
|
||||
'companycountry' => 'ri-global-line',
|
||||
'companydescription' => 'ri-file-text-line',
|
||||
'companyfax' => 'ri-printer-line',
|
||||
'companyindustry' => 'ri-briefcase-4-line',
|
||||
'companynumber_of_employees' => 'ri-team-line',
|
||||
'companyphone' => 'ri-phone-line',
|
||||
'companystate' => 'ri-map-pin-2-line',
|
||||
'companywebsite' => 'ri-global-line',
|
||||
'companyzipcode' => 'ri-mail-send-line',
|
||||
|
||||
// behaviors
|
||||
'redirect_id' => 'ri-cursor-line',
|
||||
'email_id' => 'ri-cursor-line',
|
||||
'email_clicked_link_date' => 'ri-cursor-line',
|
||||
'sms_clicked_link' => 'ri-cursor-line',
|
||||
'sms_clicked_link_date' => 'ri-cursor-line',
|
||||
'lead_asset_download' => 'ri-download-2-line',
|
||||
'sessions' => 'ri-timer-line',
|
||||
'notification' => 'ri-notification-3-line',
|
||||
'lead_email_received' => 'ri-mail-open-line',
|
||||
'lead_email_read_date' => 'ri-mail-open-line',
|
||||
'lead_email_read_count' => 'ri-mail-open-line',
|
||||
'lead_email_sent_date' => 'ri-send-plane-2-line',
|
||||
'hit_url' => 'ri-external-link-line',
|
||||
'page_id' => 'ri-external-link-line',
|
||||
'hit_url_date' => 'ri-external-link-line',
|
||||
'hit_url_count' => 'ri-external-link-line',
|
||||
'referer' => 'ri-external-link-line',
|
||||
'source' => 'ri-external-link-line',
|
||||
'source_id' => 'ri-external-link-line',
|
||||
'url_title' => 'ri-external-link-line',
|
||||
'lead_email_sent' => 'ri-mail-send-line',
|
||||
default => 'ri-shapes-line',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Stat\ChartQuery;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Helper\ArrayHelper;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLog;
|
||||
use Mautic\LeadBundle\Segment\Exception\SegmentNotFoundException;
|
||||
|
||||
class SegmentContactsLineChartQuery extends ChartQuery
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $segmentId;
|
||||
|
||||
private ?array $addedEventLogStats = null;
|
||||
|
||||
private ?array $removedEventLogStats = null;
|
||||
|
||||
/**
|
||||
* @param string|null $unit
|
||||
*
|
||||
* @throws SegmentNotFoundException
|
||||
*/
|
||||
public function __construct(
|
||||
Connection $connection,
|
||||
\DateTime $dateFrom,
|
||||
\DateTime $dateTo,
|
||||
private array $filters = [],
|
||||
$unit = null,
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->dateFrom = $dateFrom;
|
||||
$this->dateTo = $dateTo;
|
||||
$this->unit = $unit;
|
||||
|
||||
if (!isset($this->filters['leadlist_id']['value'])) {
|
||||
throw new SegmentNotFoundException('Segment ID required');
|
||||
}
|
||||
$this->segmentId = $this->filters['leadlist_id']['value'];
|
||||
parent::__construct($connection, $dateFrom, $dateTo, $unit);
|
||||
}
|
||||
|
||||
public function setDateRange(\DateTimeInterface $dateFrom, \DateTimeInterface $dateTo): void
|
||||
{
|
||||
parent::setDateRange($dateFrom, $dateTo);
|
||||
$this->init();
|
||||
}
|
||||
|
||||
public function getTotalStats(int $total): array
|
||||
{
|
||||
$totalCountDateTo = $this->getTotalToDateRange($total);
|
||||
// count array SUM and then reverse
|
||||
// require start from end and substract added/removed logs
|
||||
$sums = array_reverse(ArrayHelper::sub($this->getAddedEventLogStats(), $this->getRemovedEventLogStats()));
|
||||
$totalSum = 0;
|
||||
$totals = array_map(function ($sum) use ($totalCountDateTo, &$totalSum) {
|
||||
$total = $totalCountDateTo - $totalSum;
|
||||
$totalSum += $sum;
|
||||
if ($total > -1) {
|
||||
return $total;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}, $sums);
|
||||
|
||||
return array_reverse($totals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return total of contact to date end of graph.
|
||||
*/
|
||||
private function getTotalToDateRange(int $total): int
|
||||
{
|
||||
$queryForTotal = clone $this;
|
||||
// try figure out total count in dateTo
|
||||
$queryForTotal->setDateRange($this->dateTo, new \DateTime());
|
||||
|
||||
return $total - array_sum(ArrayHelper::sub($queryForTotal->getAddedEventLogStats(), $queryForTotal->getRemovedEventLogStats()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data about add/remove from segment based on LeadEventLog.
|
||||
*
|
||||
* @param string $action
|
||||
*/
|
||||
public function getDataFromLeadEventLog($action): array
|
||||
{
|
||||
$qb = $this->prepareTimeDataQuery(
|
||||
'lead_event_log',
|
||||
'date_added',
|
||||
[
|
||||
'object' => 'segment',
|
||||
'bundle' => 'lead',
|
||||
'action' => $action,
|
||||
'object_id' => $this->segmentId,
|
||||
]
|
||||
);
|
||||
$qb = $this->optimizeSearchInLeadEventLog($qb);
|
||||
|
||||
return $this->loadAndBuildTimeData($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSegmentId()
|
||||
{
|
||||
return $this->segmentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getAddedEventLogStats()
|
||||
{
|
||||
return $this->addedEventLogStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getRemovedEventLogStats()
|
||||
{
|
||||
return $this->removedEventLogStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init basic stats.
|
||||
*/
|
||||
private function init(): void
|
||||
{
|
||||
$this->addedEventLogStats = $this->getDataFromLeadEventLog('added');
|
||||
$this->removedEventLogStats = $this->getDataFromLeadEventLog('removed');
|
||||
}
|
||||
|
||||
private function optimizeSearchInLeadEventLog(QueryBuilder $qb): QueryBuilder
|
||||
{
|
||||
$fromPart = $qb->getQueryPart('from');
|
||||
$fromPart[0]['alias'] = sprintf('%s USE INDEX (%s)', $fromPart[0]['alias'], MAUTIC_TABLE_PREFIX.LeadEventLog::INDEX_SEARCH);
|
||||
$qb->resetQueryPart('from');
|
||||
$qb->from($fromPart[0]['table'], $fromPart[0]['alias']);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Stat;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\CoreBundle\Helper\CacheStorageHelper;
|
||||
|
||||
class SegmentCampaignShare
|
||||
{
|
||||
public function __construct(
|
||||
private CampaignModel $campaignModel,
|
||||
private CacheStorageHelper $cacheStorageHelper,
|
||||
private EntityManager $entityManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $segmentId
|
||||
* @param array $campaignIds
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCampaignsSegmentShare($segmentId, $campaignIds = [])
|
||||
{
|
||||
$campaigns = $this->campaignModel->getRepository()->getCampaignsSegmentShare($segmentId, $campaignIds);
|
||||
foreach ($campaigns as $campaign) {
|
||||
$this->cacheStorageHelper->set($this->getCachedKey($segmentId, $campaign['id']), $campaign['segmentCampaignShare']);
|
||||
}
|
||||
|
||||
return $campaigns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $segmentId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCampaignList($segmentId)
|
||||
{
|
||||
$q = $this->entityManager->getConnection()->createQueryBuilder();
|
||||
$q->select('c.id, c.name, null as share')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaigns', 'c')
|
||||
->where($this->campaignModel->getRepository()->getPublishedByDateExpression($q))
|
||||
->orderBy('c.id', 'DESC');
|
||||
|
||||
$campaigns = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($campaigns as &$campaign) {
|
||||
// just load from cache If exists
|
||||
if ($share = $this->cacheStorageHelper->get($this->getCachedKey($segmentId, $campaign['id']))) {
|
||||
$campaign['share'] = $share;
|
||||
}
|
||||
}
|
||||
|
||||
return $campaigns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $segmentId
|
||||
* @param int $campaignId
|
||||
*/
|
||||
private function getCachedKey($segmentId, $campaignId): string
|
||||
{
|
||||
return sprintf('%s|%s|%s|%s|%s', 'campaign', $campaignId, 'segment', $segmentId, 'share');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Stat;
|
||||
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Segment\Stat\ChartQuery\SegmentContactsLineChartQuery;
|
||||
|
||||
class SegmentChartQueryFactory
|
||||
{
|
||||
public function getContactsTotal(SegmentContactsLineChartQuery $query, ListModel $listModel): array
|
||||
{
|
||||
$total = $listModel->getRepository()->getLeadCount($query->getSegmentId());
|
||||
|
||||
return $query->getTotalStats($total);
|
||||
}
|
||||
|
||||
public function getContactsAdded(SegmentContactsLineChartQuery $query): array
|
||||
{
|
||||
return $query->getAddedEventLogStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getContactsRemoved(SegmentContactsLineChartQuery $query)
|
||||
{
|
||||
return $query->getRemovedEventLogStats();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment\Stat;
|
||||
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\FormBundle\Model\ActionModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\PointBundle\Model\TriggerEventModel;
|
||||
use Mautic\ReportBundle\Model\ReportModel;
|
||||
|
||||
class SegmentDependencies
|
||||
{
|
||||
public function __construct(
|
||||
private EmailModel $emailModel,
|
||||
private CampaignModel $campaignModel,
|
||||
private ActionModel $actionModel,
|
||||
private ListModel $listModel,
|
||||
private TriggerEventModel $triggerEventModel,
|
||||
private ReportModel $reportModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getChannelsIds($segmentId): array
|
||||
{
|
||||
$usage = [];
|
||||
$usage[] = [
|
||||
'label' => 'mautic.email.emails',
|
||||
'route' => 'mautic_email_index',
|
||||
'ids' => $this->emailModel->getEmailsIdsWithDependenciesOnSegment($segmentId),
|
||||
];
|
||||
|
||||
$usage[] = [
|
||||
'label' => 'mautic.campaign.campaigns',
|
||||
'route' => 'mautic_campaign_index',
|
||||
'ids' => $this->campaignModel->getCampaignIdsWithDependenciesOnSegment($segmentId),
|
||||
];
|
||||
|
||||
$usage[] = [
|
||||
'label' => 'mautic.lead.lead.lists',
|
||||
'route' => 'mautic_segment_index',
|
||||
'ids' => $this->listModel->getSegmentsWithDependenciesOnSegment($segmentId, 'id'),
|
||||
];
|
||||
|
||||
$usage[] = [
|
||||
'label' => 'mautic.report.reports',
|
||||
'route' => 'mautic_report_index',
|
||||
'ids' => $this->reportModel->getReportsIdsWithDependenciesOnSegment($segmentId),
|
||||
];
|
||||
|
||||
$usage[] = [
|
||||
'label' => 'mautic.form.forms',
|
||||
'route' => 'mautic_form_index',
|
||||
'ids' => $this->actionModel->getFormsIdsWithDependenciesOnSegment($segmentId),
|
||||
];
|
||||
|
||||
$usage[] = [
|
||||
'label' => 'mautic.point.trigger.header.index',
|
||||
'route' => 'mautic_pointtrigger_index',
|
||||
'ids' => $this->triggerEventModel->getReportIdsWithDependenciesOnSegment($segmentId),
|
||||
];
|
||||
|
||||
return $usage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Segment;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
class TableSchemaColumnsCache
|
||||
{
|
||||
private array $cache;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
) {
|
||||
$this->cache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|false
|
||||
*/
|
||||
public function getColumns($tableName)
|
||||
{
|
||||
if (!isset($this->cache[$tableName])) {
|
||||
$columns = $this->entityManager->getConnection()->createSchemaManager()->listTableColumns($tableName);
|
||||
$this->cache[$tableName] = $columns ?: [];
|
||||
}
|
||||
|
||||
return $this->cache[$tableName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
$this->cache = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCurrentDatabaseName()
|
||||
{
|
||||
return $this->entityManager->getConnection()->getDatabase();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user