ShortUrl::isEnabled()   B
last analyzed

Complexity

Conditions 7
Paths 22

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 19
ccs 10
cts 10
cp 1
rs 8.8333
c 0
b 0
f 0
cc 7
nc 22
nop 0
crap 7
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\Exception\ShortCodeCannotBeRegeneratedException;
13
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
14
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
15
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
16
use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
17
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
18
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
19
use Shlinkio\Shlink\Rest\Entity\ApiKey;
20
21
use function count;
22
use function Shlinkio\Shlink\Core\generateRandomShortCode;
23
24
class ShortUrl extends AbstractEntity
25
{
26
    private string $longUrl;
27
    private string $shortCode;
28
    private Chronos $dateCreated;
29
    /** @var Collection|Visit[] */
30
    private Collection $visits;
31
    /** @var Collection|Tag[] */
32
    private Collection $tags;
33
    private ?Chronos $validSince = null;
34
    private ?Chronos $validUntil = null;
35
    private ?int $maxVisits = null;
36
    private ?Domain $domain = null;
37
    private bool $customSlugWasProvided;
38
    private int $shortCodeLength;
39
    private ?string $importSource = null;
40
    private ?string $importOriginalShortCode = null;
41 100
    private ?ApiKey $authorApiKey = null;
42
43
    public function __construct(
44
        string $longUrl,
45
        ?ShortUrlMeta $meta = null,
46 100
        ?ShortUrlRelationResolverInterface $relationResolver = null
47
    ) {
48 100
        $meta = $meta ?? ShortUrlMeta::createEmpty();
49 100
        $relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
50 100
51 100
        $this->longUrl = $longUrl;
52 100
        $this->dateCreated = Chronos::now();
53 100
        $this->visits = new ArrayCollection();
54 100
        $this->tags = new ArrayCollection();
55 100
        $this->validSince = $meta->getValidSince();
56 100
        $this->validUntil = $meta->getValidUntil();
57 100
        $this->maxVisits = $meta->getMaxVisits();
58 100
        $this->customSlugWasProvided = $meta->hasCustomSlug();
59 100
        $this->shortCodeLength = $meta->getShortCodeLength();
60
        $this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($this->shortCodeLength);
61 3
        $this->domain = $relationResolver->resolveDomain($meta->getDomain());
62
        $this->authorApiKey = $relationResolver->resolveApiKey($meta->getApiKey());
63
    }
64
65
    public static function fromImport(
66
        ImportedShlinkUrl $url,
67 3
        bool $importShortCode,
68 3
        ?ShortUrlRelationResolverInterface $relationResolver = null
69
    ): self {
70 3
        $meta = [
71 3
            ShortUrlMetaInputFilter::DOMAIN => $url->domain(),
72
            ShortUrlMetaInputFilter::VALIDATE_URL => false,
73
        ];
74 3
        if ($importShortCode) {
75 3
            $meta[ShortUrlMetaInputFilter::CUSTOM_SLUG] = $url->shortCode();
76 3
        }
77 3
78
        $instance = new self($url->longUrl(), ShortUrlMeta::fromRawData($meta), $relationResolver);
79 3
        $instance->importSource = $url->source();
80
        $instance->importOriginalShortCode = $url->shortCode();
81
        $instance->dateCreated = Chronos::instance($url->createdAt());
82 34
83
        return $instance;
84 34
    }
85
86
    public function getLongUrl(): string
87 42
    {
88
        return $this->longUrl;
89 42
    }
90
91
    public function getShortCode(): string
92 15
    {
93
        return $this->shortCode;
94 15
    }
95
96
    public function getDateCreated(): Chronos
97 16
    {
98
        return $this->dateCreated;
99 16
    }
100
101
    public function getDomain(): ?Domain
102
    {
103
        return $this->domain;
104
    }
105 16
106
    /**
107 16
     * @return Collection|Tag[]
108
     */
109
    public function getTags(): Collection
110
    {
111
        return $this->tags;
112
    }
113 7
114
    /**
115 7
     * @param Collection|Tag[] $tags
116 7
     */
117
    public function setTags(Collection $tags): self
118
    {
119 4
        $this->tags = $tags;
120
        return $this;
121 4
    }
122 3
123
    public function update(ShortUrlEdit $shortUrlEdit): void
124 4
    {
125 2
        if ($shortUrlEdit->hasValidSince()) {
126
            $this->validSince = $shortUrlEdit->validSince();
127 4
        }
128 3
        if ($shortUrlEdit->hasValidUntil()) {
129
            $this->validUntil = $shortUrlEdit->validUntil();
130 4
        }
131 3
        if ($shortUrlEdit->hasMaxVisits()) {
132
            $this->maxVisits = $shortUrlEdit->maxVisits();
133 4
        }
134
        if ($shortUrlEdit->hasLongUrl()) {
135
            $this->longUrl = $shortUrlEdit->longUrl();
136
        }
137
    }
138 4
139
    /**
140
     * @throws ShortCodeCannotBeRegeneratedException
141 4
     */
142 1
    public function regenerateShortCode(): void
143
    {
144
        // In ShortUrls where a custom slug was provided, throw error, unless it is an imported one
145
        if ($this->customSlugWasProvided && $this->importSource === null) {
146 3
            throw ShortCodeCannotBeRegeneratedException::forShortUrlWithCustomSlug();
147 1
        }
148
149
        // The short code can be regenerated only on ShortUrl which have not been persisted yet
150 2
        if ($this->id !== null) {
151 2
            throw ShortCodeCannotBeRegeneratedException::forShortUrlAlreadyPersisted();
152
        }
153 18
154
        $this->shortCode = generateRandomShortCode($this->shortCodeLength);
155 18
    }
156
157
    public function getValidSince(): ?Chronos
158 18
    {
159
        return $this->validSince;
160 18
    }
161
162
    public function getValidUntil(): ?Chronos
163 19
    {
164
        return $this->validUntil;
165 19
    }
166
167
    public function getVisitsCount(): int
168
    {
169
        return count($this->visits);
170
    }
171
172 4
    /**
173
     * @param Collection|Visit[] $visits
174 4
     * @internal
175 4
     */
176
    public function setVisits(Collection $visits): self
177
    {
178 18
        $this->visits = $visits;
179
        return $this;
180 18
    }
181
182
    public function getMaxVisits(): ?int
183 6
    {
184
        return $this->maxVisits;
185 6
    }
186 6
187 3
    public function isEnabled(): bool
188
    {
189
        $maxVisitsReached = $this->maxVisits !== null && $this->getVisitsCount() >= $this->maxVisits;
190 4
        if ($maxVisitsReached) {
191 4
            return false;
192 4
        }
193 2
194
        $now = Chronos::now();
195
        $beforeValidSince = $this->validSince !== null && $this->validSince->gt($now);
196 3
        if ($beforeValidSince) {
197 3
            return false;
198 2
        }
199
200
        $afterValidUntil = $this->validUntil !== null && $this->validUntil->lt($now);
201 2
        if ($afterValidUntil) {
202
            return false;
203
        }
204 26
205
        return true;
206 26
    }
207 26
208 26
    public function toString(array $domainConfig): string
209
    {
210
        return (string) (new Uri())->withPath($this->shortCode)
211 26
                                   ->withScheme($domainConfig['schema'] ?? 'http')
212
                                   ->withHost($this->resolveDomain($domainConfig['hostname'] ?? ''));
213 26
    }
214 26
215
    private function resolveDomain(string $fallback = ''): string
216
    {
217 1
        if ($this->domain === null) {
218
            return $fallback;
219
        }
220
221
        return $this->domain->getAuthority();
222
    }
223
}
224