Completed
Pull Request — develop (#645)
by Alejandro
05:15
created

ShortUrl   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 190
Duplicated Lines 0 %

Test Coverage

Coverage 96.43%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 76
c 1
b 0
f 0
dl 0
loc 190
rs 9.2
ccs 81
cts 84
cp 0.9643
wmc 40

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getShortCode() 0 3 1
A getLongUrl() 0 3 1
A __construct() 0 17 1
A getDateCreated() 0 3 1
A setVisits() 0 4 1
A getDomain() 0 3 1
A getValidSince() 0 3 1
A regenerateShortCode() 0 14 3
A setTags() 0 4 1
A getVisitsCount() 0 3 1
A getValidUntil() 0 3 1
A toString() 0 5 1
A getTags() 0 3 1
A updateMeta() 0 10 4
B matchesCriteria() 0 20 11
A getMaxVisits() 0 3 1
A resolveDomain() 0 7 2
B isEnabled() 0 19 7

How to fix   Complexity   

Complex Class

Complex classes like ShortUrl often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ShortUrl, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Shlinkio\Shlink\Core\Entity;
6
7
use Cake\Chronos\Chronos;
8
use Doctrine\Common\Collections\ArrayCollection;
9
use Doctrine\Common\Collections\Collection;
10
use Laminas\Diactoros\Uri;
11
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
12
use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface;
13
use Shlinkio\Shlink\Core\Domain\Resolver\SimpleDomainResolver;
14
use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
15
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
16
17
use function array_reduce;
18
use function count;
19
use function Functional\contains;
20
use function Functional\invoke;
21
use function Shlinkio\Shlink\Core\generateRandomShortCode;
22
23
class ShortUrl extends AbstractEntity
24
{
25
    private string $longUrl;
26
    private string $shortCode;
27
    private Chronos $dateCreated;
28
    /** @var Collection|Visit[] */
29
    private Collection $visits;
30
    /** @var Collection|Tag[] */
31
    private Collection $tags;
32
    private ?Chronos $validSince = null;
33
    private ?Chronos $validUntil = null;
34
    private ?int $maxVisits = null;
35
    private ?Domain $domain;
36
    private bool $customSlugWasProvided;
37
38 56
    public function __construct(
39
        string $longUrl,
40
        ?ShortUrlMeta $meta = null,
41
        ?DomainResolverInterface $domainResolver = null
42
    ) {
43 56
        $meta = $meta ?? ShortUrlMeta::createEmpty();
44
45 56
        $this->longUrl = $longUrl;
46 56
        $this->dateCreated = Chronos::now();
47 56
        $this->visits = new ArrayCollection();
48 56
        $this->tags = new ArrayCollection();
49 56
        $this->validSince = $meta->getValidSince();
50 56
        $this->validUntil = $meta->getValidUntil();
51 56
        $this->maxVisits = $meta->getMaxVisits();
52 56
        $this->customSlugWasProvided = $meta->hasCustomSlug();
53 56
        $this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode();
54 56
        $this->domain = ($domainResolver ?? new SimpleDomainResolver())->resolveDomain($meta->getDomain());
55
    }
56
57 22
    public function getLongUrl(): string
58
    {
59 22
        return $this->longUrl;
60
    }
61
62 28
    public function getShortCode(): string
63
    {
64 28
        return $this->shortCode;
65
    }
66
67 12
    public function getDateCreated(): Chronos
68
    {
69 12
        return $this->dateCreated;
70
    }
71
72 12
    public function getDomain(): ?Domain
73
    {
74 12
        return $this->domain;
75
    }
76
77
    /**
78
     * @return Collection|Tag[]
79
     */
80 22
    public function getTags(): Collection
81
    {
82 22
        return $this->tags;
83
    }
84
85
    /**
86
     * @param Collection|Tag[] $tags
87
     */
88 6
    public function setTags(Collection $tags): self
89
    {
90 6
        $this->tags = $tags;
91 6
        return $this;
92
    }
93
94 1
    public function updateMeta(ShortUrlMeta $shortCodeMeta): void
95
    {
96 1
        if ($shortCodeMeta->hasValidSince()) {
97 1
            $this->validSince = $shortCodeMeta->getValidSince();
98
        }
99 1
        if ($shortCodeMeta->hasValidUntil()) {
100 1
            $this->validUntil = $shortCodeMeta->getValidUntil();
101
        }
102 1
        if ($shortCodeMeta->hasMaxVisits()) {
103 1
            $this->maxVisits = $shortCodeMeta->getMaxVisits();
104
        }
105
    }
106
107
    /**
108
     * @throws ShortCodeCannotBeRegeneratedException
109
     */
110 4
    public function regenerateShortCode(): self
111
    {
112
        // In ShortUrls where a custom slug was provided, do nothing
113 4
        if ($this->customSlugWasProvided) {
114 1
            throw ShortCodeCannotBeRegeneratedException::forShortUrlWithCustomSlug();
115
        }
116
117
        // The short code can be regenerated only on ShortUrl which have not been persisted yet
118 3
        if ($this->id !== null) {
119 1
            throw ShortCodeCannotBeRegeneratedException::forShortUrlAlreadyPersisted();
120
        }
121
122 2
        $this->shortCode = generateRandomShortCode();
123 2
        return $this;
124
    }
125
126 13
    public function getValidSince(): ?Chronos
127
    {
128 13
        return $this->validSince;
129
    }
130
131 13
    public function getValidUntil(): ?Chronos
132
    {
133 13
        return $this->validUntil;
134
    }
135
136 16
    public function getVisitsCount(): int
137
    {
138 16
        return count($this->visits);
139
    }
140
141
    /**
142
     * @param Collection|Visit[] $visits
143
     * @internal
144
     */
145 4
    public function setVisits(Collection $visits): self
146
    {
147 4
        $this->visits = $visits;
148 4
        return $this;
149
    }
150
151 13
    public function getMaxVisits(): ?int
152
    {
153 13
        return $this->maxVisits;
154
    }
155
156 5
    public function isEnabled(): bool
157
    {
158 5
        $maxVisitsReached = $this->maxVisits !== null && $this->getVisitsCount() >= $this->maxVisits;
159 5
        if ($maxVisitsReached) {
160 2
            return false;
161
        }
162
163 3
        $now = Chronos::now();
164 3
        $beforeValidSince = $this->validSince !== null && $this->validSince->gt($now);
165 3
        if ($beforeValidSince) {
166 1
            return false;
167
        }
168
169 2
        $afterValidUntil = $this->validUntil !== null && $this->validUntil->lt($now);
170 2
        if ($afterValidUntil) {
171 1
            return false;
172
        }
173
174 1
        return true;
175
    }
176
177 14
    public function toString(array $domainConfig): string
178
    {
179 14
        return (string) (new Uri())->withPath($this->shortCode)
180 14
                                   ->withScheme($domainConfig['schema'] ?? 'http')
181 14
                                   ->withHost($this->resolveDomain($domainConfig['hostname'] ?? ''));
182
    }
183
184 15
    private function resolveDomain(string $fallback = ''): string
185
    {
186 15
        if ($this->domain === null) {
187 14
            return $fallback;
188
        }
189
190 1
        return $this->domain->getAuthority();
191
    }
192
193 9
    public function matchesCriteria(ShortUrlMeta $meta, array $tags): bool
194
    {
195 9
        if ($meta->hasMaxVisits() && $meta->getMaxVisits() !== $this->maxVisits) {
196 1
            return false;
197
        }
198 9
        if ($meta->hasDomain() && $meta->getDomain() !== $this->resolveDomain()) {
199
            return false;
200
        }
201 9
        if ($meta->hasValidSince() && ! $meta->getValidSince()->eq($this->validSince)) {
0 ignored issues
show
Bug introduced by
It seems like $this->validSince can also be of type null; however, parameter $dt of Cake\Chronos\Chronos::eq() does only seem to accept Cake\Chronos\ChronosInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

201
        if ($meta->hasValidSince() && ! $meta->getValidSince()->eq(/** @scrutinizer ignore-type */ $this->validSince)) {
Loading history...
202
            return false;
203
        }
204 9
        if ($meta->hasValidUntil() && ! $meta->getValidUntil()->eq($this->validUntil)) {
205
            return false;
206
        }
207
208 9
        $shortUrlTags = invoke($this->getTags(), '__toString');
209 9
        return count($shortUrlTags) === count($tags) && array_reduce(
210 9
            $tags,
211 9
            fn (bool $hasAllTags, string $tag) => $hasAllTags && contains($shortUrlTags, $tag),
212 9
            true,
213
        );
214
    }
215
}
216