Completed
Push — develop ( 8162da...f53fa5 )
by Alejandro
31s queued 13s
created

ShortUrl::regenerateShortCode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 0
dl 0
loc 14
rs 10
c 0
b 0
f 0
ccs 7
cts 7
cp 1
crap 3
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
    private int $shortCodeLength;
38
39 143
    public function __construct(
40
        string $longUrl,
41
        ?ShortUrlMeta $meta = null,
42
        ?DomainResolverInterface $domainResolver = null
43
    ) {
44 143
        $meta = $meta ?? ShortUrlMeta::createEmpty();
45
46 143
        $this->longUrl = $longUrl;
47 143
        $this->dateCreated = Chronos::now();
48 143
        $this->visits = new ArrayCollection();
49 143
        $this->tags = new ArrayCollection();
50 143
        $this->validSince = $meta->getValidSince();
51 143
        $this->validUntil = $meta->getValidUntil();
52 143
        $this->maxVisits = $meta->getMaxVisits();
53 143
        $this->customSlugWasProvided = $meta->hasCustomSlug();
54 143
        $this->shortCodeLength = $meta->getShortCodeLength();
55 143
        $this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($this->shortCodeLength);
56 143
        $this->domain = ($domainResolver ?? new SimpleDomainResolver())->resolveDomain($meta->getDomain());
57
    }
58
59 22
    public function getLongUrl(): string
60
    {
61 22
        return $this->longUrl;
62
    }
63
64 36
    public function getShortCode(): string
65
    {
66 36
        return $this->shortCode;
67
    }
68
69 12
    public function getDateCreated(): Chronos
70
    {
71 12
        return $this->dateCreated;
72
    }
73
74 12
    public function getDomain(): ?Domain
75
    {
76 12
        return $this->domain;
77
    }
78
79
    /**
80
     * @return Collection|Tag[]
81
     */
82 22
    public function getTags(): Collection
83
    {
84 22
        return $this->tags;
85
    }
86
87
    /**
88
     * @param Collection|Tag[] $tags
89
     */
90 85
    public function setTags(Collection $tags): self
91
    {
92 85
        $this->tags = $tags;
93 85
        return $this;
94
    }
95
96 1
    public function updateMeta(ShortUrlMeta $shortCodeMeta): void
97
    {
98 1
        if ($shortCodeMeta->hasValidSince()) {
99 1
            $this->validSince = $shortCodeMeta->getValidSince();
100
        }
101 1
        if ($shortCodeMeta->hasValidUntil()) {
102 1
            $this->validUntil = $shortCodeMeta->getValidUntil();
103
        }
104 1
        if ($shortCodeMeta->hasMaxVisits()) {
105 1
            $this->maxVisits = $shortCodeMeta->getMaxVisits();
106
        }
107
    }
108
109
    /**
110
     * @throws ShortCodeCannotBeRegeneratedException
111
     */
112 4
    public function regenerateShortCode(): self
113
    {
114
        // In ShortUrls where a custom slug was provided, do nothing
115 4
        if ($this->customSlugWasProvided) {
116 1
            throw ShortCodeCannotBeRegeneratedException::forShortUrlWithCustomSlug();
117
        }
118
119
        // The short code can be regenerated only on ShortUrl which have not been persisted yet
120 3
        if ($this->id !== null) {
121 1
            throw ShortCodeCannotBeRegeneratedException::forShortUrlAlreadyPersisted();
122
        }
123
124 2
        $this->shortCode = generateRandomShortCode($this->shortCodeLength);
125 2
        return $this;
126
    }
127
128 13
    public function getValidSince(): ?Chronos
129
    {
130 13
        return $this->validSince;
131
    }
132
133 13
    public function getValidUntil(): ?Chronos
134
    {
135 13
        return $this->validUntil;
136
    }
137
138 16
    public function getVisitsCount(): int
139
    {
140 16
        return count($this->visits);
141
    }
142
143
    /**
144
     * @param Collection|Visit[] $visits
145
     * @internal
146
     */
147 4
    public function setVisits(Collection $visits): self
148
    {
149 4
        $this->visits = $visits;
150 4
        return $this;
151
    }
152
153 13
    public function getMaxVisits(): ?int
154
    {
155 13
        return $this->maxVisits;
156
    }
157
158 5
    public function isEnabled(): bool
159
    {
160 5
        $maxVisitsReached = $this->maxVisits !== null && $this->getVisitsCount() >= $this->maxVisits;
161 5
        if ($maxVisitsReached) {
162 2
            return false;
163
        }
164
165 3
        $now = Chronos::now();
166 3
        $beforeValidSince = $this->validSince !== null && $this->validSince->gt($now);
167 3
        if ($beforeValidSince) {
168 1
            return false;
169
        }
170
171 2
        $afterValidUntil = $this->validUntil !== null && $this->validUntil->lt($now);
172 2
        if ($afterValidUntil) {
173 1
            return false;
174
        }
175
176 1
        return true;
177
    }
178
179 14
    public function toString(array $domainConfig): string
180
    {
181 14
        return (string) (new Uri())->withPath($this->shortCode)
182 14
                                   ->withScheme($domainConfig['schema'] ?? 'http')
183 14
                                   ->withHost($this->resolveDomain($domainConfig['hostname'] ?? ''));
184
    }
185
186 15
    private function resolveDomain(string $fallback = ''): string
187
    {
188 15
        if ($this->domain === null) {
189 14
            return $fallback;
190
        }
191
192 1
        return $this->domain->getAuthority();
193
    }
194
195 9
    public function matchesCriteria(ShortUrlMeta $meta, array $tags): bool
196
    {
197 9
        if ($meta->hasMaxVisits() && $meta->getMaxVisits() !== $this->maxVisits) {
198 1
            return false;
199
        }
200 9
        if ($meta->hasDomain() && $meta->getDomain() !== $this->resolveDomain()) {
201
            return false;
202
        }
203 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

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