Issues (3627)

Segment/Query/ContactSegmentQueryBuilder.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2018 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\LeadBundle\Segment\Query;
13
14
use Doctrine\DBAL\Connection;
15
use Doctrine\DBAL\Connections\MasterSlaveConnection;
16
use Doctrine\ORM\EntityManager;
17
use Mautic\LeadBundle\Entity\LeadList;
18
use Mautic\LeadBundle\Event\LeadListFilteringEvent;
19
use Mautic\LeadBundle\Event\LeadListQueryBuilderGeneratedEvent;
20
use Mautic\LeadBundle\LeadEvents;
21
use Mautic\LeadBundle\Segment\ContactSegmentFilter;
22
use Mautic\LeadBundle\Segment\Exception\PluginHandledFilterException;
23
use Mautic\LeadBundle\Segment\Exception\SegmentQueryException;
24
use Mautic\LeadBundle\Segment\RandomParameterName;
25
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
26
27
/**
28
 * Class ContactSegmentQueryBuilder is responsible for building queries for segments.
29
 */
30
class ContactSegmentQueryBuilder
31
{
32
    /** @var EntityManager */
33
    private $entityManager;
34
35
    /** @var RandomParameterName */
36
    private $randomParameterName;
37
38
    /** @var EventDispatcherInterface */
39
    private $dispatcher;
40
41
    /** @var array Contains segment edges mapping */
42
    private $dependencyMap = [];
43
44
    /**
45
     * ContactSegmentQueryBuilder constructor.
46
     */
47
    public function __construct(EntityManager $entityManager, RandomParameterName $randomParameterName, EventDispatcherInterface $dispatcher)
48
    {
49
        $this->entityManager       = $entityManager;
50
        $this->randomParameterName = $randomParameterName;
51
        $this->dispatcher          = $dispatcher;
52
    }
53
54
    /**
55
     * @param $segmentId
56
     * @param $segmentFilters
57
     *
58
     * @return QueryBuilder
59
     *
60
     * @throws SegmentQueryException
61
     */
62
    public function assembleContactsSegmentQueryBuilder($segmentId, $segmentFilters)
63
    {
64
        /** @var Connection $connection */
65
        $connection = $this->entityManager->getConnection();
66
        if ($connection instanceof MasterSlaveConnection) {
67
            // Prefer a slave connection if available.
68
            $connection->connect('slave');
69
        }
70
71
        /** @var QueryBuilder $queryBuilder */
72
        $queryBuilder = new QueryBuilder($connection);
73
74
        $queryBuilder->select('l.id')->from(MAUTIC_TABLE_PREFIX.'leads', 'l');
75
76
        /*
77
         * Validate the plan, check for circular dependencies.
78
         *
79
         * the bigger count($plan), the higher complexity of query
80
         */
81
        $this->getResolutionPlan($segmentId);
82
83
        /** @var ContactSegmentFilter $filter */
84
        foreach ($segmentFilters as $filter) {
85
            try {
86
                $this->dispatchPluginFilteringEvent($filter, $queryBuilder);
87
            } catch (PluginHandledFilterException $exception) {
88
                continue;
89
            }
90
91
            $queryBuilder = $filter->applyQuery($queryBuilder);
92
        }
93
94
        $queryBuilder->applyStackLogic();
95
96
        return $queryBuilder;
97
    }
98
99
    /**
100
     * @return QueryBuilder
101
     *
102
     * @throws \Doctrine\DBAL\DBALException
103
     */
104
    public function wrapInCount(QueryBuilder $qb)
105
    {
106
        /** @var Connection $connection */
107
        $connection = $this->entityManager->getConnection();
108
        if ($connection instanceof MasterSlaveConnection) {
109
            // Prefer a slave connection if available.
110
            $connection->connect('slave');
111
        }
112
113
        // Add count functions to the query
114
        $queryBuilder = new QueryBuilder($connection);
115
116
        //  If there is any right join in the query we need to select its it
117
        $primary = $qb->guessPrimaryLeadContactIdColumn();
118
119
        $currentSelects = [];
120
        foreach ($qb->getQueryParts()['select'] as $select) {
121
            if ($select != $primary) {
122
                $currentSelects[] = $select;
123
            }
124
        }
125
126
        $qb->select('DISTINCT '.$primary.' as leadIdPrimary');
127
        foreach ($currentSelects as $select) {
128
            $qb->addSelect($select);
129
        }
130
131
        $queryBuilder->select('count(leadIdPrimary) count, max(leadIdPrimary) maxId, min(leadIdPrimary) minId')
132
            ->from('('.$qb->getSQL().')', 'sss');
133
        $queryBuilder->setParameters($qb->getParameters());
134
135
        return $queryBuilder;
136
    }
137
138
    /**
139
     * Restrict the query to NEW members of segment.
140
     *
141
     * @param $segmentId
142
     * @param $batchRestrictions
143
     *
144
     * @return QueryBuilder
145
     *
146
     * @throws QueryException
147
     */
148
    public function addNewContactsRestrictions(QueryBuilder $queryBuilder, $segmentId, $batchRestrictions)
0 ignored issues
show
The parameter $batchRestrictions is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

148
    public function addNewContactsRestrictions(QueryBuilder $queryBuilder, $segmentId, /** @scrutinizer ignore-unused */ $batchRestrictions)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
149
    {
150
        $parts     = $queryBuilder->getQueryParts();
151
        $setHaving = (count($parts['groupBy']) || !is_null($parts['having']));
152
153
        $tableAlias = $this->generateRandomParameterName();
154
        $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias, $tableAlias.'.lead_id = l.id');
155
        $queryBuilder->addSelect($tableAlias.'.lead_id AS '.$tableAlias.'_lead_id');
156
157
        $expression = $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $segmentId);
158
159
        $queryBuilder->addJoinCondition($tableAlias, $expression);
160
161
        if ($setHaving) {
162
            $restrictionExpression = $queryBuilder->expr()->isNull($tableAlias.'_lead_id');
163
            $queryBuilder->andHaving($restrictionExpression);
164
        } else {
165
            $restrictionExpression = $queryBuilder->expr()->isNull($tableAlias.'.lead_id');
166
            $queryBuilder->andWhere($restrictionExpression);
167
        }
168
169
        return $queryBuilder;
170
    }
171
172
    /**
173
     * @param $leadListId
174
     *
175
     * @return QueryBuilder
176
     *
177
     * @throws QueryException
178
     */
179
    public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, $leadListId)
180
    {
181
        $tableAlias = $this->generateRandomParameterName();
182
183
        $existsQueryBuilder = $queryBuilder->getConnection()->createQueryBuilder();
184
185
        $existsQueryBuilder
186
            ->select($tableAlias.'.lead_id')
187
            ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias)
188
            ->andWhere($queryBuilder->expr()->eq($tableAlias.'.leadlist_id', intval($leadListId)))
189
            ->andWhere(
190
                $queryBuilder->expr()->orX(
191
                    $queryBuilder->expr()->eq($tableAlias.'.manually_added', 1),
192
                    $queryBuilder->expr()->eq($tableAlias.'.manually_removed', $queryBuilder->expr()->literal(''))
193
                )
194
            );
195
196
        $queryBuilder->orWhere(
197
            $queryBuilder->expr()->in('l.id', $existsQueryBuilder->getSQL())
198
        );
199
200
        return $queryBuilder;
201
    }
202
203
    /**
204
     * @param $leadListId
205
     *
206
     * @return QueryBuilder
207
     *
208
     * @throws QueryException
209
     */
210
    public function addManuallyUnsubscribedQuery(QueryBuilder $queryBuilder, $leadListId)
211
    {
212
        $tableAlias = $this->generateRandomParameterName();
213
        $queryBuilder->leftJoin(
214
            'l',
215
            MAUTIC_TABLE_PREFIX.'lead_lists_leads',
216
            $tableAlias,
217
            'l.id = '.$tableAlias.'.lead_id and '.$tableAlias.'.leadlist_id = '.intval($leadListId)
218
        );
219
        $queryBuilder->addJoinCondition($tableAlias, $queryBuilder->expr()->eq($tableAlias.'.manually_removed', 1));
220
        $queryBuilder->andWhere($queryBuilder->expr()->isNull($tableAlias.'.lead_id'));
221
222
        return $queryBuilder;
223
    }
224
225
    public function queryBuilderGenerated(LeadList $segment, QueryBuilder $queryBuilder)
226
    {
227
        if (!$this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_QUERYBUILDER_GENERATED)) {
228
            return;
229
        }
230
231
        $event = new LeadListQueryBuilderGeneratedEvent($segment, $queryBuilder);
232
        $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_QUERYBUILDER_GENERATED, $event);
233
    }
234
235
    /**
236
     * Generate a unique parameter name.
237
     *
238
     * @return string
239
     */
240
    private function generateRandomParameterName()
241
    {
242
        return $this->randomParameterName->generateRandomParameterName();
243
    }
244
245
    /**
246
     * @throws PluginHandledFilterException
247
     */
248
    private function dispatchPluginFilteringEvent(ContactSegmentFilter $filter, QueryBuilder $queryBuilder)
249
    {
250
        if ($this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_ON_FILTERING)) {
251
            //  This has to run for every filter
252
            $filterCrate = $filter->contactSegmentFilterCrate->getArray();
253
254
            $alias = $this->generateRandomParameterName();
255
            $event = new LeadListFilteringEvent($filterCrate, null, $alias, $filterCrate['operator'], $queryBuilder, $this->entityManager);
256
            $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_ON_FILTERING, $event);
257
            if ($event->isFilteringDone()) {
258
                $queryBuilder->addLogic($event->getSubQuery(), $filter->getGlue());
259
260
                throw new PluginHandledFilterException();
261
            }
262
        }
263
    }
264
265
    /**
266
     * Returns array with plan for processing.
267
     *
268
     * @param int   $segmentId
269
     * @param array $seen
270
     * @param array $resolved
271
     *
272
     * @return array
273
     *
274
     * @throws SegmentQueryException
275
     */
276
    private function getResolutionPlan($segmentId, $seen = [], &$resolved = [])
277
    {
278
        $seen[] = $segmentId;
279
280
        if (!isset($this->dependencyMap[$segmentId])) {
281
            $this->dependencyMap[$segmentId] = $this->getSegmentEdges($segmentId);
282
        }
283
284
        $edges = $this->dependencyMap[$segmentId];
285
286
        foreach ($edges as $edge) {
287
            if (!in_array($edge, $resolved)) {
288
                if (in_array($edge, $seen)) {
289
                    throw new SegmentQueryException('Circular reference detected.');
290
                }
291
                $this->getResolutionPlan($edge, $seen, $resolved);
292
            }
293
        }
294
295
        $resolved[] = $segmentId;
296
297
        return $resolved;
298
    }
299
300
    /**
301
     * @param int $segmentId
302
     *
303
     * @return array
304
     */
305
    private function getSegmentEdges($segmentId)
306
    {
307
        $segment = $this->entityManager->getRepository('MauticLeadBundle:LeadList')->find($segmentId);
308
        if (null === $segment) {
309
            return [];
310
        }
311
312
        $segmentFilters = $segment->getFilters();
313
        $segmentEdges   = [];
314
315
        foreach ($segmentFilters as $segmentFilter) {
316
            if (isset($segmentFilter['field']) && 'leadlist' === $segmentFilter['field']) {
317
                $filterEdges  = $segmentFilter['filter'];
318
                $segmentEdges = array_merge($segmentEdges, $filterEdges);
319
            }
320
        }
321
322
        return $segmentEdges;
323
    }
324
}
325