Issues (3627)

app/bundles/LeadBundle/Entity/LeadRepository.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2014 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\Entity;
13
14
use Doctrine\Common\Collections\ArrayCollection;
15
use Doctrine\DBAL\Connection;
16
use Doctrine\DBAL\Exception\DriverException;
17
use Doctrine\DBAL\Query\QueryBuilder;
18
use Mautic\CoreBundle\Entity\CommonRepository;
19
use Mautic\CoreBundle\Helper\DateTimeHelper;
20
use Mautic\CoreBundle\Helper\SearchStringHelper;
21
use Mautic\LeadBundle\Event\LeadBuildSearchEvent;
22
use Mautic\LeadBundle\LeadEvents;
23
use Mautic\PointBundle\Model\TriggerModel;
24
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
25
26
class LeadRepository extends CommonRepository implements CustomFieldRepositoryInterface
27
{
28
    use CustomFieldRepositoryTrait {
29
        prepareDbalFieldsForSave as defaultPrepareDbalFieldsForSave;
30
    }
31
32
    use ExpressionHelperTrait;
33
    use OperatorListTrait;
34
35
    /**
36
     * @var EventDispatcherInterface
37
     */
38
    protected $dispatcher;
39
40
    /**
41
     * @var array
42
     */
43
    private $availableSocialFields = [];
44
45
    /**
46
     * @var array
47
     */
48
    private $availableSearchFields = [];
49
50
    /**
51
     * Required to get the color based on a lead's points.
52
     *
53
     * @var TriggerModel
54
     */
55
    private $triggerModel;
56
57
    /**
58
     * Used by search functions to search social profiles.
59
     */
60
    public function setAvailableSocialFields(array $fields)
61
    {
62
        $this->availableSocialFields = $fields;
63
    }
64
65
    /**
66
     * Used by search functions to search using aliases as commands.
67
     */
68
    public function setAvailableSearchFields(array $fields)
69
    {
70
        $this->availableSearchFields = $fields;
71
    }
72
73
    /**
74
     * Sets trigger model.
75
     */
76
    public function setTriggerModel(TriggerModel $triggerModel)
77
    {
78
        $this->triggerModel = $triggerModel;
79
    }
80
81
    public function setDispatcher(EventDispatcherInterface $dispatcher)
82
    {
83
        $this->dispatcher = $dispatcher;
84
    }
85
86
    /**
87
     * Get a list of leads based on field value.
88
     *
89
     * @param string $field
90
     * @param string $value
91
     * @param ?int   $ignoreId
92
     * @param bool   $indexByColumn
93
     *
94
     * @return array
95
     */
96
    public function getLeadsByFieldValue($field, $value, $ignoreId = null, $indexByColumn = false)
97
    {
98
        $results = $this->getEntities([
99
            'qb'               => $this->buildQueryForGetLeadsByFieldValue($field, $value, $ignoreId),
100
            'ignore_paginator' => true,
101
        ]);
102
103
        if (!$indexByColumn) {
104
            return $results;
105
        }
106
107
        return array_combine(array_map(function (Lead $lead) use ($field) {
108
            return $lead->getFieldValue($field);
109
        }, $results), $results);
110
    }
111
112
    /**
113
     * Builds the query for the getLeadsByFieldValue method.
114
     *
115
     * @internal
116
     *
117
     * @param string $field
118
     * @param string $value
119
     * @param ?int   $ignoreId
120
     *
121
     * @return QueryBuilder
122
     */
123
    protected function buildQueryForGetLeadsByFieldValue($field, $value, $ignoreId = null)
124
    {
125
        $col = 'l.'.$field;
126
127
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder()
128
            ->select('l.id')
129
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l');
130
131
        if ($ignoreId) {
132
            $q->where('l.id != :ignoreId')
133
                ->setParameter('ignoreId', $ignoreId);
134
        }
135
136
        if (is_array($value)) {
137
            /**
138
             * Bind each value to specific named parameters.
139
             *
140
             * @see https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/query-builder.html#line-number-0a267d5a2c69797a7656aae33fcc140d16b0a566-72
141
             */
142
            $valueParams = [];
143
            for ($i = 0; $i < count($value); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
144
                $valueParams[':'.$this->generateRandomParameterName()] = $value[$i];
145
            }
146
147
            $q->andWhere(
148
                $q->expr()->in($col, array_keys($valueParams))
149
            );
150
151
            foreach ($valueParams as $param => $value) {
152
                $q->setParameter(ltrim($param, ':'), $value);
153
            }
154
155
            return $q;
156
        }
157
158
        $q->andWhere("$col = :search")->setParameter('search', $value);
159
160
        return $q;
161
    }
162
163
    /**
164
     * @param $email
165
     *
166
     * @return Lead[]
167
     */
168
    public function getContactsByEmail($email)
169
    {
170
        $contacts = $this->getLeadsByFieldValue('email', $email);
171
172
        // Attempt to search for contacts without a + suffix
173
        if (empty($contacts) && preg_match('#^(.*?)\+(.*?)@(.*?)$#', $email, $parts)) {
174
            $email    = $parts[1].'@'.$parts[3];
175
            $contacts = $this->getLeadsByFieldValue('email', $email);
176
        }
177
178
        return $contacts;
179
    }
180
181
    /**
182
     * @param string[] $emails
183
     *
184
     * @return int[]|array
185
     */
186
    public function getContactIdsByEmails(array $emails): array
187
    {
188
        $result = $this->getEntityManager()
189
            ->createQuery("
190
                SELECT c.id 
191
                FROM Mautic\LeadBundle\Entity\Lead c
192
                WHERE c.email IN (:emails)
193
            ")
194
            ->setParameter(':emails', $emails, Connection::PARAM_STR_ARRAY)
195
            ->getArrayResult();
196
197
        return array_map(
198
            function ($row) {
199
                return (int) $row['id'];
200
            },
201
            $result
202
        );
203
    }
204
205
    /**
206
     * Get a list of lead entities.
207
     *
208
     * @param     $uniqueFieldsWithData
209
     * @param int $leadId
210
     * @param int $limit
211
     *
212
     * @return array
213
     */
214
    public function getLeadsByUniqueFields($uniqueFieldsWithData, $leadId = null, $limit = null)
215
    {
216
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder()
217
            ->select('l.*')
218
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l');
219
220
        // loop through the fields and
221
        foreach ($uniqueFieldsWithData as $col => $val) {
222
            $q->orWhere("l.$col = :".$col)
223
                ->setParameter($col, $val);
224
        }
225
226
        // if we have a lead ID lets use it
227
        if (!empty($leadId)) {
228
            // make sure that its not the id we already have
229
            $q->andWhere('l.id != :leadId')
230
                ->setParameter('leadId', $leadId);
231
        }
232
233
        if ($limit) {
234
            $q->setMaxResults($limit);
235
        }
236
237
        $results = $q->execute()->fetchAll();
238
239
        // Collect the IDs
240
        $leads = [];
241
        foreach ($results as $r) {
242
            $leads[$r['id']] = $r;
243
        }
244
245
        // Get entities
246
        $q = $this->getEntityManager()->createQueryBuilder()
247
            ->select('l')
248
            ->from(Lead::class, 'l');
249
250
        $q->where(
251
            $q->expr()->in('l.id', ':ids')
252
        )
253
            ->setParameter('ids', array_keys($leads))
254
            ->orderBy('l.dateAdded', 'DESC')
255
            ->addOrderBy('l.id', 'DESC');
256
        $entities = $q->getQuery()
257
            ->getResult();
258
259
        /** @var Lead $lead */
260
        foreach ($entities as $lead) {
261
            $lead->setAvailableSocialFields($this->availableSocialFields);
262
            if (!empty($this->triggerModel)) {
263
                $lead->setColor($this->triggerModel->getColorForLeadPoints($lead->getPoints()));
264
            }
265
266
            $lead->setFields(
267
                $this->formatFieldValues($leads[$lead->getId()])
268
            );
269
        }
270
271
        return $entities;
272
    }
273
274
    /**
275
     * Get list of lead Ids by unique field data.
276
     *
277
     * @param $uniqueFieldsWithData is an array of columns & values to filter by
278
     * @param int $leadId is the current lead id. Added to query to skip and find other leads
279
     *
280
     * @return array
281
     */
282
    public function getLeadIdsByUniqueFields($uniqueFieldsWithData, $leadId = null)
283
    {
284
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder()
285
            ->select('l.id')
286
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l');
287
288
        // loop through the fields and
289
        foreach ($uniqueFieldsWithData as $col => $val) {
290
            $q->orWhere("l.$col = :".$col)
291
                ->setParameter($col, $val);
292
        }
293
294
        // if we have a lead ID lets use it
295
        if (!empty($leadId)) {
296
            // make sure that its not the id we already have
297
            $q->andWhere('l.id != '.$leadId);
298
        }
299
300
        return $q->execute()->fetchAll();
301
    }
302
303
    /**
304
     * @param string $email
305
     * @param bool   $all   Set to true to return all matching lead id's
306
     *
307
     * @return array|null
308
     */
309
    public function getLeadByEmail($email, $all = false)
310
    {
311
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder()
312
            ->select('l.id')
313
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
314
            ->where('email = :search')
315
            ->setParameter('search', $email);
316
317
        $result = $q->execute()->fetchAll();
318
319
        if (count($result)) {
320
            return $all ? $result : $result[0];
321
        } else {
322
            return;
323
        }
324
    }
325
326
    /**
327
     * Get leads by IP address.
328
     *
329
     * @param      $ip
330
     * @param bool $byId
331
     *
332
     * @return array
333
     */
334
    public function getLeadsByIp($ip, $byId = false)
335
    {
336
        $q = $this->createQueryBuilder('l')
337
            ->leftJoin('l.ipAddresses', 'i');
338
        $col = ($byId) ? 'i.id' : 'i.ipAddress';
339
        $q->where($col.' = :ip')
340
            ->setParameter('ip', $ip)
341
            ->orderBy('l.dateAdded', 'DESC');
342
        $results = $q->getQuery()->getResult();
343
344
        /** @var Lead $lead */
345
        foreach ($results as $lead) {
346
            $lead->setAvailableSocialFields($this->availableSocialFields);
347
        }
348
349
        return $results;
350
    }
351
352
    /**
353
     * @param $id
354
     *
355
     * @return array
356
     */
357
    public function getLead($id)
358
    {
359
        $fq = $this->getEntityManager()->getConnection()->createQueryBuilder();
360
        $fq->select('l.*')
361
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
362
            ->where('l.id = '.$id);
363
        $results = $fq->execute()->fetchAll();
364
365
        return (isset($results[0])) ? $results[0] : [];
366
    }
367
368
    /**
369
     * {@inheritdoc}
370
     *
371
     * @param int $id
372
     *
373
     * @return mixed|null
374
     */
375
    public function getEntity($id = 0)
376
    {
377
        try {
378
            $q = $this->createQueryBuilder($this->getTableAlias());
379
            if (is_array($id)) {
380
                $this->buildSelectClause($q, $id);
381
                $contactId = (int) $id['id'];
382
            } else {
383
                $q->select('l, u, i')
384
                    ->leftJoin('l.ipAddresses', 'i')
385
                    ->leftJoin('l.owner', 'u');
386
                $contactId = $id;
387
            }
388
            $q->andWhere($this->getTableAlias().'.id = :id')
389
                ->setParameter('id', (int) $contactId);
390
            $entity = $q->getQuery()->getSingleResult();
391
        } catch (\Exception $e) {
392
            $entity = null;
393
        }
394
395
        if (null === $entity) {
396
            return $entity;
397
        }
398
399
        if ($entity->getFields()) {
400
            // Pulled from Doctrine memory so don't make unnecessary queries as this has already happened
401
            return $entity;
402
        }
403
404
        if (!empty($this->triggerModel)) {
405
            $entity->setColor($this->triggerModel->getColorForLeadPoints($entity->getPoints()));
406
        }
407
408
        $fieldValues = $this->getFieldValues($id);
409
        $entity->setFields($fieldValues);
410
411
        $entity->setAvailableSocialFields($this->availableSocialFields);
412
413
        return $entity;
414
    }
415
416
    /**
417
     * Get a contact entity with the primary company data populated.
418
     *
419
     * The primary company data will be a flat array on the entity
420
     * with a key of `primaryCompany`
421
     *
422
     * @param mixed $entity
423
     *
424
     * @return mixed|null
425
     */
426
    public function getEntityWithPrimaryCompany($entity)
427
    {
428
        if (is_int($entity)) {
429
            $entity = $this->getEntity($entity);
430
        }
431
432
        if ($entity instanceof Lead) {
433
            $id        = $entity->getId();
434
            $companies = $this->getEntityManager()->getRepository(Company::class)->getCompaniesForContacts([$id]);
435
436
            if (!empty($companies[$id])) {
437
                $primary = null;
438
439
                foreach ($companies as $company) {
440
                    if (isset($company['is_primary']) && 1 == $company['is_primary']) {
441
                        $primary = $company;
442
                    }
443
                }
444
445
                if (empty($primary)) {
446
                    $primary = $companies[$id][0];
447
                }
448
449
                $entity->setPrimaryCompany($primary);
450
            }
451
        }
452
453
        return $entity;
454
    }
455
456
    /**
457
     * Get a list of leads.
458
     *
459
     * @return array
460
     */
461
    public function getEntities(array $args = [])
462
    {
463
        $contacts = $this->getEntitiesWithCustomFields(
464
            'lead',
465
            $args,
466
            function ($r) {
467
                if (!empty($this->triggerModel)) {
468
                    $r->setColor($this->triggerModel->getColorForLeadPoints($r->getPoints()));
469
                }
470
                $r->setAvailableSocialFields($this->availableSocialFields);
471
            }
472
        );
473
474
        $contactCount = isset($contacts['results']) ? count($contacts['results']) : count($contacts);
475
        if ($contactCount && (!empty($args['withPrimaryCompany']) || !empty($args['withChannelRules']))) {
476
            $withTotalCount = (array_key_exists('withTotalCount', $args) && $args['withTotalCount']);
477
            /** @var Lead[] $tmpContacts */
478
            $tmpContacts = ($withTotalCount) ? $contacts['results'] : $contacts;
479
480
            $withCompanies   = !empty($args['withPrimaryCompany']);
481
            $withPreferences = !empty($args['withChannelRules']);
482
            $contactIds      = array_keys($tmpContacts);
483
484
            if ($withCompanies) {
485
                $companies = $this->getEntityManager()->getRepository(Company::class)->getCompaniesForContacts($contactIds);
486
            }
487
488
            if ($withPreferences) {
489
                /** @var FrequencyRuleRepository $frequencyRepo */
490
                $frequencyRepo  = $this->getEntityManager()->getRepository(FrequencyRule::class);
491
                $frequencyRules = $frequencyRepo->getFrequencyRules(null, $contactIds);
492
493
                /** @var DoNotContactRepository $dncRepository */
494
                $dncRepository = $this->getEntityManager()->getRepository(DoNotContact::class);
495
                $dncRules      = $dncRepository->getChannelList(null, $contactIds);
496
            }
497
498
            foreach ($contactIds as $id) {
499
                if ($withCompanies && isset($companies[$id]) && !empty($companies[$id])) {
500
                    $primary = null;
501
502
                    // Try to find the primary company
503
                    foreach ($companies[$id] as $company) {
504
                        if (1 == $company['is_primary']) {
505
                            $primary = $company;
506
                        }
507
                    }
508
509
                    // If no primary was found, just grab the first
510
                    if (empty($primary)) {
511
                        $primary = $companies[$id][0];
512
                    }
513
514
                    if (is_array($tmpContacts[$id])) {
515
                        $tmpContacts[$id]['primaryCompany'] = $primary;
516
                    } elseif ($tmpContacts[$id] instanceof Lead) {
517
                        $tmpContacts[$id]->setPrimaryCompany($primary);
518
                    }
519
                }
520
521
                if ($withPreferences) {
522
                    $contactFrequencyRules = (isset($frequencyRules[$id])) ? $frequencyRules[$id] : [];
523
                    $contactDncRules       = (isset($dncRules[$id])) ? $dncRules[$id] : [];
524
525
                    $channelRules = Lead::generateChannelRules($contactFrequencyRules, $contactDncRules);
526
                    if (is_array($tmpContacts[$id])) {
527
                        $tmpContacts[$id]['channelRules'] = $channelRules;
528
                    } elseif ($tmpContacts[$id] instanceof Lead) {
529
                        $tmpContacts[$id]->setChannelRules($channelRules);
530
                    }
531
                }
532
            }
533
534
            if ($withTotalCount) {
535
                $contacts['results'] = $tmpContacts;
536
            } else {
537
                $contacts = $tmpContacts;
538
            }
539
        }
540
541
        return $contacts;
542
    }
543
544
    /**
545
     * @return array
546
     */
547
    public function getFieldGroups()
548
    {
549
        return ['core', 'social', 'personal', 'professional'];
550
    }
551
552
    /**
553
     * @return \Doctrine\DBAL\Query\QueryBuilder
554
     */
555
    public function getEntitiesDbalQueryBuilder()
556
    {
557
        $alias = $this->getTableAlias();
558
559
        return $this->getEntityManager()->getConnection()->createQueryBuilder()
560
            ->from(MAUTIC_TABLE_PREFIX.'leads', $alias)
561
            ->leftJoin($alias, MAUTIC_TABLE_PREFIX.'users', 'u', 'u.id = '.$alias.'.owner_id');
562
    }
563
564
    /**
565
     * @param $order
566
     *
567
     * @return \Doctrine\ORM\QueryBuilder
568
     */
569
    public function getEntitiesOrmQueryBuilder($order)
570
    {
571
        $alias = $this->getTableAlias();
572
        $q     = $this->getEntityManager()->createQueryBuilder();
573
        $q->select($alias.', u, i,'.$order)
574
            ->from('MauticLeadBundle:Lead', $alias, $alias.'.id')
575
            ->leftJoin($alias.'.ipAddresses', 'i')
576
            ->leftJoin($alias.'.owner', 'u')
577
            ->indexBy($alias, $alias.'.id');
578
579
        return $q;
580
    }
581
582
    /**
583
     * Get contacts for a specific channel entity.
584
     *
585
     * @param $args - same as getEntity/getEntities
586
     * @param        $joinTable
587
     * @param        $entityId
588
     * @param array  $filters
589
     * @param string $entityColumnName
590
     * @param array  $additionalJoins  [ ['type' => 'join|leftJoin', 'from_alias' => '', 'table' => '', 'condition' => ''], ... ]
591
     *
592
     * @return array
593
     */
594
    public function getEntityContacts($args, $joinTable, $entityId, $filters = [], $entityColumnName = 'id', array $additionalJoins = null, $contactColumnName = 'lead_id')
595
    {
596
        $qb = $this->getEntitiesDbalQueryBuilder();
597
598
        if (empty($contactColumnName)) {
599
            $contactColumnName = 'lead_id';
600
        }
601
602
        $joinCondition = $qb->expr()->andX(
603
            $qb->expr()->eq($this->getTableAlias().'.id', 'entity.'.$contactColumnName)
604
        );
605
606
        if ($entityId && $entityColumnName) {
607
            $joinCondition->add(
608
                $qb->expr()->eq("entity.{$entityColumnName}", (int) $entityId)
609
            );
610
        }
611
612
        if (!empty($joinTable)) {
613
            $qb->join(
614
                $this->getTableAlias(),
615
                MAUTIC_TABLE_PREFIX.$joinTable,
616
                'entity',
617
                $joinCondition
618
            );
619
        }
620
621
        if (is_array($additionalJoins)) {
622
            foreach ($additionalJoins as $t) {
623
                $qb->{$t['type']}(
624
                    $t['from_alias'],
625
                    MAUTIC_TABLE_PREFIX.$t['table'],
626
                    $t['alias'],
627
                    $t['condition']
628
                );
629
            }
630
        }
631
632
        if ($filters) {
633
            $expr = $qb->expr()->andX();
634
            foreach ($filters as $column => $value) {
635
                if (is_array($value)) {
636
                    $this->buildWhereClauseFromArray($qb, [$value]);
637
                } else {
638
                    if (false === strpos($column, '.')) {
639
                        $column = "entity.$column";
640
                    }
641
642
                    $expr->add(
643
                        $qb->expr()->eq($column, $qb->createNamedParameter($value))
644
                    );
645
                    $qb->andWhere($expr);
646
                }
647
            }
648
        }
649
650
        $args['qb'] = $qb;
651
652
        return $this->getEntities($args);
653
    }
654
655
    /**
656
     * Adds the "catch all" where clause to the QueryBuilder.
657
     *
658
     * @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
659
     * @param                                                              $filter
660
     *
661
     * @return array
662
     */
663
    protected function addCatchAllWhereClause($q, $filter)
664
    {
665
        $columns = array_merge(
666
            [
667
                'l.firstname',
668
                'l.lastname',
669
                'l.email',
670
                'l.company',
671
                'l.city',
672
                'l.state',
673
                'l.zipcode',
674
                'l.country',
675
            ],
676
            $this->availableSocialFields
677
        );
678
679
        return $this->addStandardCatchAllWhereClause($q, $filter, $columns);
680
    }
681
682
    /**
683
     * Adds the command where clause to the QueryBuilder.
684
     *
685
     * @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
686
     * @param                                                              $filter
687
     *
688
     * @return array
689
     */
690
    protected function addSearchCommandWhereClause($q, $filter)
691
    {
692
        $command                 = $filter->command;
693
        $string                  = $filter->string;
694
        $unique                  = $this->generateRandomParameterName();
695
        $returnParameter         = false; //returning a parameter that is not used will lead to a Doctrine error
696
        list($expr, $parameters) = parent::addSearchCommandWhereClause($q, $filter);
697
698
        //DBAL QueryBuilder does not have an expr()->not() function; boo!!
699
700
        // This will be switched by some commands that use join tables as NOT EXISTS queries will be used
701
        $exprType = ($filter->not) ? 'negate_expr' : 'expr';
702
703
        $operators = $this->getFilterExpressionFunctions();
704
        $operators = array_merge($operators, [
705
            'x' => [
706
                'expr'        => 'andX',
707
                'negate_expr' => 'orX',
708
            ],
709
            'null' => [
710
                'expr'        => 'isNull',
711
                'negate_expr' => 'isNotNull',
712
            ],
713
        ]);
714
715
        $innerJoinTables = (isset($this->advancedFilterCommands[$command])
716
            && SearchStringHelper::COMMAND_NEGATE !== $this->advancedFilterCommands[$command]);
717
        $likeExpr = $operators['like'][$exprType];
718
        $eqExpr   = $operators['='][$exprType];
719
        $nullExpr = $operators['null'][$exprType];
720
        $inExpr   = $operators['in'][$exprType];
721
        $xExpr    = $operators['x'][$exprType];
722
723
        switch ($command) {
724
            case $this->translator->trans('mautic.lead.lead.searchcommand.isanonymous'):
725
            case $this->translator->trans('mautic.lead.lead.searchcommand.isanonymous', [], null, 'en_US'):
726
                $expr = $q->expr()->$nullExpr('l.date_identified');
727
                break;
728
            case $this->translator->trans('mautic.core.searchcommand.ismine'):
729
            case $this->translator->trans('mautic.core.searchcommand.ismine', [], null, 'en_US'):
730
                $expr = $q->expr()->$eqExpr('l.owner_id', $this->currentUser->getId());
731
                break;
732
            case $this->translator->trans('mautic.lead.lead.searchcommand.isunowned'):
733
            case $this->translator->trans('mautic.lead.lead.searchcommand.isunowned', [], null, 'en_US'):
734
                $expr = $q->expr()->orX(
735
                    $q->expr()->$eqExpr('l.owner_id', 0),
736
                    $q->expr()->$nullExpr('l.owner_id')
737
                );
738
                break;
739
            case $this->translator->trans('mautic.lead.lead.searchcommand.owner'):
740
            case $this->translator->trans('mautic.lead.lead.searchcommand.owner', [], null, 'en_US'):
741
                $expr = $q->expr()->orX(
742
                    $q->expr()->$likeExpr('u.first_name', ':'.$unique),
743
                    $q->expr()->$likeExpr('u.last_name', ':'.$unique)
744
                );
745
                $returnParameter = true;
746
                break;
747
            case $this->translator->trans('mautic.core.searchcommand.name'):
748
            case $this->translator->trans('mautic.core.searchcommand.name', [], null, 'en_US'):
749
                $expr = $q->expr()->orX(
750
                    $q->expr()->$likeExpr('l.firstname', ":$unique"),
751
                    $q->expr()->$likeExpr('l.lastname', ":$unique")
752
                );
753
                $returnParameter = true;
754
                break;
755
            case $this->translator->trans('mautic.core.searchcommand.email'):
756
            case $this->translator->trans('mautic.core.searchcommand.email', [], null, 'en_US'):
757
                $expr            = $q->expr()->$likeExpr('l.email', ":$unique");
758
                $returnParameter = true;
759
                break;
760
            case $this->translator->trans('mautic.lead.lead.searchcommand.list'):
761
            case $this->translator->trans('mautic.lead.lead.searchcommand.list', [], null, 'en_US'):
762
                $this->applySearchQueryRelationship(
763
                    $q,
764
                    [
765
                        [
766
                            'from_alias' => 'l',
767
                            'table'      => 'lead_lists_leads',
768
                            'alias'      => 'list_lead',
769
                            'condition'  => 'l.id = list_lead.lead_id',
770
                        ],
771
                        [
772
                            'from_alias' => 'list_lead',
773
                            'table'      => 'lead_lists',
774
                            'alias'      => 'list',
775
                            'condition'  => 'list_lead.leadlist_id = list.id',
776
                        ],
777
                    ],
778
                    $innerJoinTables,
779
                    $this->generateFilterExpression($q, 'list.alias', $eqExpr, $unique, ($filter->not) ? true : null,
780
                        // orX for filter->not either manuall removed or is null
781
                        $q->expr()->$xExpr(
782
                            $q->expr()->$eqExpr('list_lead.manually_removed', 0)
783
                        )
784
                    )
785
                );
786
                $filter->strict  = true;
787
                $returnParameter = true;
788
789
                break;
790
            case $this->translator->trans('mautic.core.searchcommand.ip'):
791
            case $this->translator->trans('mautic.core.searchcommand.ip', [], null, 'en_US'):
792
                $this->applySearchQueryRelationship(
793
                    $q,
794
                    [
795
                        [
796
                            'from_alias' => 'l',
797
                            'table'      => 'lead_ips_xref',
798
                            'alias'      => 'ip_lead',
799
                            'condition'  => 'l.id = ip_lead.lead_id',
800
                        ],
801
                        [
802
                            'from_alias' => 'ip_lead',
803
                            'table'      => 'ip_addresses',
804
                            'alias'      => 'ip',
805
                            'condition'  => 'ip_lead.ip_id = ip.id',
806
                        ],
807
                    ],
808
                    $innerJoinTables,
809
                    $this->generateFilterExpression($q, 'ip.ip_address', $likeExpr, $unique, null)
810
                );
811
                $returnParameter = true;
812
813
                break;
814
            case $this->translator->trans('mautic.lead.lead.searchcommand.duplicate'):
815
            case $this->translator->trans('mautic.lead.lead.searchcommand.duplicate', [], null, 'en_US'):
816
                $prateek  = explode('+', $string);
817
                $imploder = [];
818
819
                foreach ($prateek as $value) {
820
                    $list       = $this->getEntityManager()->getRepository(LeadList::class)->findOneByAlias($value);
821
                    $imploder[] = ((!empty($list)) ? (int) $list->getId() : 0);
822
                }
823
824
                //logic. In query, Sum(manually_removed) should be less than the current)
825
                $pluck    = count($imploder);
826
                $imploder = (string) (implode(',', $imploder));
827
828
                $sq = $this->getEntityManager()->getConnection()->createQueryBuilder();
829
                $sq->select('duplicate.lead_id')
830
                    ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'duplicate')
831
                    ->where(
832
                        $q->expr()->andX(
833
                            $q->expr()->in('duplicate.leadlist_id', $imploder),
834
                            $q->expr()->eq('duplicate.manually_removed', 0)
835
                        )
836
                    )
837
                    ->groupBy('duplicate.lead_id')
838
                    ->having("COUNT(duplicate.lead_id) = $pluck");
839
840
                $expr            = $q->expr()->$inExpr('l.id', sprintf('(%s)', $sq->getSQL()));
841
                $returnParameter = true;
842
843
                break;
844
            case $this->translator->trans('mautic.lead.lead.searchcommand.tag'):
845
            case $this->translator->trans('mautic.lead.lead.searchcommand.tag', [], null, 'en_US'):
846
                $this->applySearchQueryRelationship(
847
                    $q,
848
                    [
849
                        [
850
                            'from_alias' => 'l',
851
                            'table'      => 'lead_tags_xref',
852
                            'alias'      => 'xtag',
853
                            'condition'  => 'l.id = xtag.lead_id',
854
                        ],
855
                        [
856
                            'from_alias' => 'xtag',
857
                            'table'      => 'lead_tags',
858
                            'alias'      => 'tag',
859
                            'condition'  => 'xtag.tag_id = tag.id',
860
                        ],
861
                    ],
862
                    $innerJoinTables,
863
                    $this->generateFilterExpression($q, 'tag.tag', $likeExpr, $unique, null)
864
                );
865
                $returnParameter = true;
866
                break;
867
            case $this->translator->trans('mautic.lead.lead.searchcommand.company'):
868
            case $this->translator->trans('mautic.lead.lead.searchcommand.company', [], null, 'en_US'):
869
                $this->applySearchQueryRelationship(
870
                    $q,
871
                    [
872
                        [
873
                            'from_alias' => 'l',
874
                            'table'      => 'companies_leads',
875
                            'alias'      => 'comp_lead',
876
                            'condition'  => 'l.id = comp_lead.lead_id',
877
                        ],
878
                        [
879
                            'from_alias' => 'comp_lead',
880
                            'table'      => 'companies',
881
                            'alias'      => 'comp',
882
                            'condition'  => 'comp_lead.company_id = comp.id',
883
                        ],
884
                    ],
885
                    $innerJoinTables,
886
                    $this->generateFilterExpression($q, 'comp.companyname', $likeExpr, $unique, null)
887
                );
888
                $returnParameter = true;
889
                break;
890
            case $this->translator->trans('mautic.lead.lead.searchcommand.stage'):
891
            case $this->translator->trans('mautic.lead.lead.searchcommand.stage', [], null, 'en_US'):
892
                $this->applySearchQueryRelationship(
893
                    $q,
894
                    [
895
                        [
896
                            'from_alias' => 'l',
897
                            'table'      => 'stages',
898
                            'alias'      => 's',
899
                            'condition'  => 'l.stage_id = s.id',
900
                        ],
901
                    ],
902
                    $innerJoinTables,
903
                    $this->generateFilterExpression($q, 's.name', $likeExpr, $unique, null)
904
                );
905
                $returnParameter = true;
906
                break;
907
            default:
908
                if (in_array($command, $this->availableSearchFields)) {
909
                    $expr = $q->expr()->$likeExpr("l.$command", ":$unique");
910
                }
911
                $returnParameter = true;
912
                break;
913
        }
914
915
        if ($this->dispatcher) {
916
            $event = new LeadBuildSearchEvent($filter->string, $filter->command, $unique, $filter->not, $q);
917
            $this->dispatcher->dispatch(LeadEvents::LEAD_BUILD_SEARCH_COMMANDS, $event);
918
            if ($event->isSearchDone()) {
919
                $returnParameter = $event->getReturnParameters();
920
                $filter->strict  = $event->getStrict();
921
                $expr            = $event->getSubQuery();
922
                $parameters      = array_merge($parameters, $event->getParameters());
923
            }
924
        }
925
926
        if ($returnParameter) {
927
            $string              = ($filter->strict) ? $filter->string : "{$filter->string}%";
928
            $parameters[$unique] = $string;
929
        }
930
931
        return [
932
            $expr,
933
            $parameters,
934
        ];
935
    }
936
937
    /**
938
     * Returns the array of search commands.
939
     *
940
     * @return array
941
     */
942
    public function getSearchCommands()
943
    {
944
        $commands = [
945
            'mautic.lead.lead.searchcommand.isanonymous',
946
            'mautic.core.searchcommand.ismine',
947
            'mautic.lead.lead.searchcommand.isunowned',
948
            'mautic.lead.lead.searchcommand.list',
949
            'mautic.core.searchcommand.name',
950
            'mautic.lead.lead.searchcommand.company',
951
            'mautic.core.searchcommand.email',
952
            'mautic.lead.lead.searchcommand.owner',
953
            'mautic.core.searchcommand.ip',
954
            'mautic.lead.lead.searchcommand.tag',
955
            'mautic.lead.lead.searchcommand.stage',
956
            'mautic.lead.lead.searchcommand.duplicate',
957
            'mautic.lead.lead.searchcommand.email_sent',
958
            'mautic.lead.lead.searchcommand.email_read',
959
            'mautic.lead.lead.searchcommand.email_queued',
960
            'mautic.lead.lead.searchcommand.email_pending',
961
            'mautic.lead.lead.searchcommand.page_source',
962
            'mautic.lead.lead.searchcommand.page_source_id',
963
            'mautic.lead.lead.searchcommand.page_id',
964
            'mautic.lead.lead.searchcommand.sms_sent',
965
            'mautic.lead.lead.searchcommand.web_sent',
966
            'mautic.lead.lead.searchcommand.mobile_sent',
967
        ];
968
969
        if (!empty($this->availableSearchFields)) {
970
            $commands = array_merge($commands, $this->availableSearchFields);
971
        }
972
973
        return array_merge($commands, parent::getSearchCommands());
974
    }
975
976
    /**
977
     * Returns the array of columns with the default order.
978
     *
979
     * @return array
980
     */
981
    protected function getDefaultOrder()
982
    {
983
        return [
984
            ['l.last_active', 'DESC'],
985
        ];
986
    }
987
988
    /**
989
     * Updates lead's lastActive with now date/time.
990
     *
991
     * @param int $leadId
992
     */
993
    public function updateLastActive($leadId)
994
    {
995
        $dt     = new DateTimeHelper();
996
        $fields = ['last_active' => $dt->toUtcString()];
997
998
        $this->getEntityManager()->getConnection()->update(MAUTIC_TABLE_PREFIX.'leads', $fields, ['id' => $leadId]);
999
    }
1000
1001
    /**
1002
     * Gets the ID of the latest ID.
1003
     *
1004
     * @return int
1005
     */
1006
    public function getMaxLeadId()
1007
    {
1008
        $result = $this->getEntityManager()->getConnection()->createQueryBuilder()
1009
            ->select('max(id) as max_lead_id')
1010
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
1011
            ->execute()->fetchAll();
1012
1013
        return $result[0]['max_lead_id'];
1014
    }
1015
1016
    /**
1017
     * Gets names, signature and email of the user(lead owner).
1018
     *
1019
     * @param int $ownerId
1020
     *
1021
     * @return array|false
1022
     */
1023
    public function getLeadOwner($ownerId)
1024
    {
1025
        if (!$ownerId) {
1026
            return false;
1027
        }
1028
1029
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder()
1030
            ->select('u.id, u.first_name, u.last_name, u.email, u.position, u.signature')
1031
            ->from(MAUTIC_TABLE_PREFIX.'users', 'u')
1032
            ->where('u.id = :ownerId')
1033
            ->setParameter('ownerId', (int) $ownerId);
1034
1035
        $result = $q->execute()->fetch();
1036
1037
        // Fix the HTML markup
1038
        if (is_array($result)) {
1039
            foreach ($result as &$field) {
1040
                $field = html_entity_decode($field);
1041
            }
1042
        }
1043
1044
        return $result;
1045
    }
1046
1047
    /**
1048
     * Check lead owner.
1049
     *
1050
     * @param array $ownerIds
1051
     *
1052
     * @return array|false
1053
     */
1054
    public function checkLeadOwner(Lead $lead, $ownerIds = [])
1055
    {
1056
        if (empty($ownerIds)) {
1057
            return false;
1058
        }
1059
1060
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder();
1061
        $q->select('u.id')
1062
            ->from(MAUTIC_TABLE_PREFIX.'users', 'u')
1063
            ->join('u', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.owner_id = u.id')
1064
            ->where(
1065
                $q->expr()->andX(
1066
                    $q->expr()->in('u.id', ':ownerIds'),
1067
                    $q->expr()->eq('l.id', ':leadId')
1068
                )
1069
            )
1070
            ->setParameter('ownerIds', implode(',', $ownerIds))
1071
            ->setParameter('leadId', $lead->getId());
1072
1073
        return (bool) $q->execute()->fetchColumn();
1074
    }
1075
1076
    /**
1077
     * @return array
1078
     */
1079
    public function getContacts(array $contactIds)
1080
    {
1081
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
1082
1083
        $qb->select('l.*')->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
1084
            ->where(
1085
                $qb->expr()->in('l.id', $contactIds)
1086
            );
1087
1088
        $results = $qb->execute()->fetchAll();
1089
1090
        if ($results) {
1091
            $contacts = [];
1092
            foreach ($results as $result) {
1093
                $contacts[$result['id']] = $result;
1094
            }
1095
1096
            return $contacts;
1097
        }
1098
1099
        return [];
1100
    }
1101
1102
    /**
1103
     * @return ArrayCollection
1104
     */
1105
    public function getContactCollection(array $ids)
1106
    {
1107
        if (empty($ids)) {
1108
            return new ArrayCollection();
1109
        }
1110
1111
        $contacts = $this->getEntities(
1112
            [
1113
                'filter'             => [
1114
                    'force' => [
1115
                        [
1116
                            'column' => 'l.id',
1117
                            'expr'   => 'in',
1118
                            'value'  => $ids,
1119
                        ],
1120
                    ],
1121
                ],
1122
                'orderBy'            => 'l.id',
1123
                'orderByDir'         => 'asc',
1124
                'withPrimaryCompany' => true,
1125
                'withChannelRules'   => true,
1126
            ]
1127
        );
1128
1129
        return new ArrayCollection($contacts);
1130
    }
1131
1132
    /**
1133
     * @return string
1134
     */
1135
    public function getTableAlias()
1136
    {
1137
        return 'l';
1138
    }
1139
1140
    /**
1141
     * Get the count of identified contacts.
1142
     *
1143
     * @return int
1144
     */
1145
    public function getIdentifiedContactCount()
1146
    {
1147
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
1148
            ->select('count(*)')
1149
            ->from($this->getTableName(), $this->getTableAlias());
1150
1151
        $qb->where(
1152
            $qb->expr()->isNotNull($this->getTableAlias().'.date_identified')
1153
        );
1154
1155
        return (int) $qb->execute()->fetchColumn();
1156
    }
1157
1158
    /**
1159
     * Get the next contact after an specific ID; mainly used in deduplication.
1160
     *
1161
     * @return Lead
1162
     */
1163
    public function getNextIdentifiedContact($lastId)
1164
    {
1165
        $alias = $this->getTableAlias();
1166
        $qb    = $this->getEntityManager()->getConnection()->createQueryBuilder()
1167
            ->select("$alias.id")
1168
            ->from($this->getTableName(), $this->getTableAlias());
1169
1170
        $qb->where(
1171
            $qb->expr()->andX(
1172
                $qb->expr()->gt("$alias.id", (int) $lastId),
1173
                $qb->expr()->isNotNull("$alias.date_identified")
1174
            )
1175
        )
1176
            ->orderBy("$alias.id")
1177
            ->setMaxResults(1);
1178
1179
        $next = $qb->execute()->fetchColumn();
1180
1181
        return ($next) ? $this->getEntity($next) : null;
1182
    }
1183
1184
    /**
1185
     * @param array $tables          $tables[0] should be primary table
1186
     * @param bool  $innerJoinTables
1187
     * @param null  $whereExpression
1188
     * @param null  $having
1189
     */
1190
    public function applySearchQueryRelationship(QueryBuilder $q, array $tables, $innerJoinTables, $whereExpression = null, $having = null)
1191
    {
1192
        $primaryTable = $tables[0];
1193
        unset($tables[0]);
1194
1195
        $joinType = ($innerJoinTables) ? 'join' : 'leftJoin';
1196
1197
        $this->useDistinctCount = true;
1198
        $joins                  = $q->getQueryPart('join');
1199
        if (!preg_match('/"'.preg_quote($primaryTable['alias'], '/').'"/i', json_encode($joins))) {
1200
            $q->$joinType(
1201
                $primaryTable['from_alias'],
1202
                MAUTIC_TABLE_PREFIX.$primaryTable['table'],
1203
                $primaryTable['alias'],
1204
                $primaryTable['condition']
1205
            );
1206
        }
1207
        foreach ($tables as $table) {
1208
            $q->$joinType($table['from_alias'], MAUTIC_TABLE_PREFIX.$table['table'], $table['alias'], $table['condition']);
1209
        }
1210
1211
        if ($whereExpression) {
1212
            $q->andWhere($whereExpression);
1213
        }
1214
1215
        if ($having) {
1216
            $q->andHaving($having);
1217
        }
1218
        $q->groupBy('l.id');
1219
    }
1220
1221
    /**
1222
     * @param     $id
1223
     * @param int $tries
1224
     */
1225
    protected function updateContactPoints(array $changes, $id, $tries = 1)
1226
    {
1227
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
1228
            ->update(MAUTIC_TABLE_PREFIX.'leads')
1229
            ->where('id = '.$id);
1230
1231
        $ph = 0;
1232
        // Keep operator in same order as was used in Lead::adjustPoints() in order to be congruent with what was calculated in PHP
1233
        // Again ignoring Aunt Sally here (PEMDAS)
1234
        foreach ($changes as $operator => $points) {
1235
            $qb->set('points', 'points '.$operator.' :points'.$ph)
1236
                ->setParameter('points'.$ph, $points, \PDO::PARAM_INT);
1237
1238
            ++$ph;
1239
        }
1240
1241
        try {
1242
            $qb->execute();
1243
        } catch (DriverException $exception) {
1244
            $message = $exception->getMessage();
1245
1246
            if (false !== strpos($message, 'Deadlock') && $tries <= 3) {
1247
                ++$tries;
1248
1249
                $this->updateContactPoints($changes, $id, $tries);
1250
            }
1251
        }
1252
1253
        // Query new points
1254
        return (int) $this->getEntityManager()->getConnection()->createQueryBuilder()
1255
            ->select('l.points')
1256
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
1257
            ->where('l.id = '.$id)
1258
            ->execute()
1259
            ->fetchColumn();
1260
    }
1261
1262
    /**
1263
     * @param Lead $entity
1264
     */
1265
    protected function postSaveEntity($entity)
1266
    {
1267
        // Check if points need to be appended
1268
        if ($entity->getPointChanges()) {
1269
            $newPoints = $this->updateContactPoints($entity->getPointChanges(), $entity->getId());
1270
1271
            // Set actual points so that code using getPoints knows the true value
1272
            $entity->setActualPoints($newPoints);
1273
1274
            $changes = $entity->getChanges();
1275
1276
            if (isset($changes['points'])) {
1277
                // Let's adjust the points to be more accurate in the change log
1278
                $changes['points'][1] = $newPoints;
1279
                $entity->setChanges($changes);
1280
            }
1281
        }
1282
    }
1283
1284
    /**
1285
     * @param $fields
1286
     */
1287
    protected function prepareDbalFieldsForSave(&$fields)
1288
    {
1289
        // Do not save points as they are handled by postSaveEntity
1290
        unset($fields['points']);
1291
1292
        $this->defaultPrepareDbalFieldsForSave($fields);
1293
    }
1294
}
1295