Completed
Pull Request — develop (#694)
by Alejandro
09:24
created

VisitRepository::findVisitsForQuery()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 20
ccs 12
cts 12
cp 1
rs 9.8666
cc 4
nc 4
nop 2
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Shlinkio\Shlink\Core\Repository;
6
7
use Doctrine\ORM\EntityRepository;
8
use Doctrine\ORM\QueryBuilder;
9
use Shlinkio\Shlink\Common\Util\DateRange;
10
use Shlinkio\Shlink\Core\Entity\Visit;
11
12
class VisitRepository extends EntityRepository implements VisitRepositoryInterface
13
{
14
    /**
15
     * @return iterable|Visit[]
16
     */
17 10
    public function findUnlocatedVisits(int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable
18
    {
19 10
        $qb = $this->getEntityManager()->createQueryBuilder();
20 10
        $qb->select('v')
21 10
           ->from(Visit::class, 'v')
22 10
           ->where($qb->expr()->isNull('v.visitLocation'));
23
24 10
        return $this->findVisitsForQuery($qb, $blockSize);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->findVisitsForQuery($qb, $blockSize) returns the type Generator which is incompatible with the documented return type Shlinkio\Shlink\Core\Entity\Visit[]|iterable.
Loading history...
25
    }
26
27
    /**
28
     * @return iterable|Visit[]
29
     */
30 10
    public function findVisitsWithEmptyLocation(int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable
31
    {
32 10
        $qb = $this->getEntityManager()->createQueryBuilder();
33 10
        $qb->select('v')
34 10
           ->from(Visit::class, 'v')
35 10
           ->join('v.visitLocation', 'vl')
36 10
           ->where($qb->expr()->isNotNull('v.visitLocation'))
37 10
           ->andWhere($qb->expr()->eq('vl.isEmpty', ':isEmpty'))
38 10
           ->setParameter('isEmpty', true);
39
40 10
        return $this->findVisitsForQuery($qb, $blockSize);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->findVisitsForQuery($qb, $blockSize) returns the type Generator which is incompatible with the documented return type Shlinkio\Shlink\Core\Entity\Visit[]|iterable.
Loading history...
41
    }
42
43 10
    public function findAllVisits(int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable
44
    {
45 10
        $qb = $this->getEntityManager()->createQueryBuilder();
46 10
        $qb->select('v')
47 10
           ->from(Visit::class, 'v');
48
49 10
        return $this->findVisitsForQuery($qb, $blockSize);
50
    }
51
52 10
    private function findVisitsForQuery(QueryBuilder $qb, int $blockSize): iterable
53
    {
54 10
        $originalQueryBuilder = $qb->setMaxResults($blockSize)
55 10
                                   ->orderBy('v.id', 'ASC');
56 10
        $lastId = '0';
57
58
        do {
59 10
            $qb = (clone $originalQueryBuilder)->andWhere($qb->expr()->gt('v.id', $lastId));
60 10
            $iterator = $qb->getQuery()->iterate();
61 10
            $resultsFound = false;
62
63
            /** @var Visit $visit */
64 10
            foreach ($iterator as $key => [$visit]) {
65 10
                $resultsFound = true;
66 10
                yield $key => $visit;
67
            }
68
69
            // As the query is ordered by ID, we can take the last one every time in order to exclude the whole list
70 10
            $lastId = isset($visit) ? $visit->getId() : $lastId;
71 10
        } while ($resultsFound);
72
    }
73
74
    /**
75
     * @return Visit[]
76
     */
77 1
    public function findVisitsByShortCode(
78
        string $shortCode,
79
        ?string $domain = null,
80
        ?DateRange $dateRange = null,
81
        ?int $limit = null,
82
        ?int $offset = null
83
    ): array {
84 1
        $qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange);
85 1
        $qb->select('v')
86 1
           ->orderBy('v.date', 'DESC');
87
88 1
        if ($limit !== null) {
89 1
            $qb->setMaxResults($limit);
90
        }
91 1
        if ($offset !== null) {
92 1
            $qb->setFirstResult($offset);
93
        }
94
95 1
        return $qb->getQuery()->getResult();
96
    }
97
98 1
    public function countVisitsByShortCode(string $shortCode, ?string $domain = null, ?DateRange $dateRange = null): int
99
    {
100 1
        $qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange);
101 1
        $qb->select('COUNT(DISTINCT v.id)');
102
103 1
        return (int) $qb->getQuery()->getSingleScalarResult();
104
    }
105
106 2
    private function createVisitsByShortCodeQueryBuilder(
107
        string $shortCode,
108
        ?string $domain,
109
        ?DateRange $dateRange
110
    ): QueryBuilder {
111 2
        $qb = $this->getEntityManager()->createQueryBuilder();
112 2
        $qb->from(Visit::class, 'v')
113 2
           ->join('v.shortUrl', 'su')
114 2
           ->where($qb->expr()->eq('su.shortCode', ':shortCode'))
115 2
           ->setParameter('shortCode', $shortCode);
116
117
        // Apply domain filtering
118 2
        if ($domain !== null) {
119 2
            $qb->join('su.domain', 'd')
120 2
               ->andWhere($qb->expr()->eq('d.authority', ':domain'))
121 2
               ->setParameter('domain', $domain);
122
        } else {
123 2
            $qb->andWhere($qb->expr()->isNull('su.domain'));
124
        }
125
126
        // Apply date range filtering
127 2
        if ($dateRange !== null && $dateRange->getStartDate() !== null) {
128 2
            $qb->andWhere($qb->expr()->gte('v.date', ':startDate'))
129 2
               ->setParameter('startDate', $dateRange->getStartDate());
130
        }
131 2
        if ($dateRange !== null && $dateRange->getEndDate() !== null) {
132 2
            $qb->andWhere($qb->expr()->lte('v.date', ':endDate'))
133 2
               ->setParameter('endDate', $dateRange->getEndDate());
134
        }
135
136 2
        return $qb;
137
    }
138
}
139