Passed
Pull Request — master (#500)
by Alejandro
06:23
created

ShortUrlRepository::slugIsInUse()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 14
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 19
ccs 14
cts 14
cp 1
crap 2
rs 9.7998
1
<?php
2
declare(strict_types=1);
3
4
namespace Shlinkio\Shlink\Core\Repository;
5
6
use Cake\Chronos\Chronos;
7
use Doctrine\ORM\EntityRepository;
8
use Doctrine\ORM\QueryBuilder;
9
use Shlinkio\Shlink\Core\Entity\ShortUrl;
10
11
use function array_column;
12
use function array_key_exists;
13
use function Functional\contains;
14
use function is_array;
15
use function key;
16
17
class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface
18
{
19
    /**
20
     * @param string[] $tags
21
     * @param string|array|null $orderBy
22
     * @return ShortUrl[]
23
     */
24 2
    public function findList(
25
        ?int $limit = null,
26
        ?int $offset = null,
27
        ?string $searchTerm = null,
28
        array $tags = [],
29
        $orderBy = null
30
    ): array {
31 2
        $qb = $this->createListQueryBuilder($searchTerm, $tags);
32 2
        $qb->select('DISTINCT s');
33
34
        // Set limit and offset
35 2
        if ($limit !== null) {
36 1
            $qb->setMaxResults($limit);
37
        }
38 2
        if ($offset !== null) {
39 1
            $qb->setFirstResult($offset);
40
        }
41
42
        // In case the ordering has been specified, the query could be more complex. Process it
43 2
        if ($orderBy !== null) {
44 2
            return $this->processOrderByForList($qb, $orderBy);
45
        }
46
47
        // With no order by, order by date and just return the list of ShortUrls
48 1
        $qb->orderBy('s.dateCreated');
49 1
        return $qb->getQuery()->getResult();
50
    }
51
52 2
    private function processOrderByForList(QueryBuilder $qb, $orderBy): array
53
    {
54
        // Map public field names to column names
55
        $fieldNameMap = [
56 2
            'originalUrl' => 'longUrl',
57
            'longUrl' => 'longUrl',
58
            'shortCode' => 'shortCode',
59
            'dateCreated' => 'dateCreated',
60
        ];
61 2
        $fieldName = is_array($orderBy) ? key($orderBy) : $orderBy;
62 2
        $order = is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
63
64 2
        if (contains(['visits', 'visitsCount', 'visitCount'], $fieldName)) {
65 1
            $qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
66 1
               ->leftJoin('s.visits', 'v')
67 1
               ->groupBy('s')
68 1
               ->orderBy('totalVisits', $order);
69
70 1
            return array_column($qb->getQuery()->getResult(), 0);
71
        }
72
73 1
        if (array_key_exists($fieldName, $fieldNameMap)) {
74 1
            $qb->orderBy('s.' . $fieldNameMap[$fieldName], $order);
75
        }
76 1
        return $qb->getQuery()->getResult();
77
    }
78
79 1
    public function countList(?string $searchTerm = null, array $tags = []): int
80
    {
81 1
        $qb = $this->createListQueryBuilder($searchTerm, $tags);
82 1
        $qb->select('COUNT(DISTINCT s)');
83
84 1
        return (int) $qb->getQuery()->getSingleScalarResult();
85
    }
86
87 3
    private function createListQueryBuilder(?string $searchTerm = null, array $tags = []): QueryBuilder
88
    {
89 3
        $qb = $this->getEntityManager()->createQueryBuilder();
90 3
        $qb->from(ShortUrl::class, 's');
91 3
        $qb->where('1=1');
92
93
        // Apply search term to every searchable field if not empty
94 3
        if (! empty($searchTerm)) {
95
            // Left join with tags only if no tags were provided. In case of tags, an inner join will be done later
96 1
            if (empty($tags)) {
97
                $qb->leftJoin('s.tags', 't');
98
            }
99
100
            $conditions = [
101 1
                $qb->expr()->like('s.longUrl', ':searchPattern'),
102 1
                $qb->expr()->like('s.shortCode', ':searchPattern'),
103 1
                $qb->expr()->like('t.name', ':searchPattern'),
104
            ];
105
106
            // Unpack and apply search conditions
107 1
            $qb->andWhere($qb->expr()->orX(...$conditions));
108 1
            $qb->setParameter('searchPattern', '%' . $searchTerm . '%');
109
        }
110
111
        // Filter by tags if provided
112 3
        if (! empty($tags)) {
113 1
            $qb->join('s.tags', 't')
114 1
               ->andWhere($qb->expr()->in('t.name', $tags));
115
        }
116
117 3
        return $qb;
118
    }
119
120 1
    public function findOneByShortCode(string $shortCode): ?ShortUrl
121
    {
122
        $dql= <<<DQL
123 1
            SELECT s
124
              FROM Shlinkio\Shlink\Core\Entity\ShortUrl AS s
125
             WHERE s.shortCode = :shortCode
126
               AND (s.validSince <= :now OR s.validSince IS NULL)
127
               AND (s.validUntil >= :now OR s.validUntil IS NULL)
128
DQL;
129
130 1
        $query = $this->getEntityManager()->createQuery($dql);
131 1
        $query->setMaxResults(1)
132 1
              ->setParameters([
133 1
                  'shortCode' => $shortCode,
134 1
                  'now' => Chronos::now(),
135
              ]);
136
137
        /** @var ShortUrl|null $result */
138 1
        $result = $query->getOneOrNullResult();
139 1
        return $result === null || $result->maxVisitsReached() ? null : $result;
140
    }
141
142 1
    public function slugIsInUse(string $slug, ?string $domain = null): bool
143
    {
144 1
        $qb = $this->getEntityManager()->createQueryBuilder();
145 1
        $qb->select('COUNT(DISTINCT s.id)')
146 1
           ->from(ShortUrl::class, 's')
147 1
           ->where($qb->expr()->isNotNull('s.shortCode'))
148 1
           ->andWhere($qb->expr()->eq('s.shortCode', ':slug'))
149 1
           ->setParameter('slug', $slug);
150
151 1
        if ($domain !== null) {
152 1
            $qb->join('s.domain', 'd')
153 1
               ->andWhere($qb->expr()->eq('d.authority', ':authority'))
154 1
               ->setParameter('authority', $domain);
155
        } else {
156 1
            $qb->andWhere($qb->expr()->isNull('s.domain'));
157
        }
158
159 1
        $result = (int) $qb->getQuery()->getSingleScalarResult();
160 1
        return $result > 0;
161
    }
162
}
163