Passed
Push — master ( 4437d5...b3ea29 )
by Alejandro
04:00 queued 13s
created

UrlShortener::findExistingShortUrlIfExists()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.0208

Importance

Changes 0
Metric Value
cc 6
eloc 12
nc 5
nop 3
dl 0
loc 23
rs 9.2222
c 0
b 0
f 0
ccs 11
cts 12
cp 0.9167
crap 6.0208
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Shlinkio\Shlink\Core\Service;
6
7
use Doctrine\ORM\EntityManagerInterface;
8
use Psr\Http\Message\UriInterface;
9
use Shlinkio\Shlink\Core\Domain\Resolver\PersistenceDomainResolver;
10
use Shlinkio\Shlink\Core\Entity\ShortUrl;
11
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
12
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
13
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
14
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
15
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
16
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
17
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
18
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
19
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
20
use Throwable;
21
22
use function array_reduce;
23
24
class UrlShortener implements UrlShortenerInterface
25
{
26
    use TagManagerTrait;
27
28
    /** @var EntityManagerInterface */
29
    private $em;
30
    /** @var UrlShortenerOptions */
31
    private $options;
32
    /** @var UrlValidatorInterface */
33
    private $urlValidator;
34
35 15
    public function __construct(
36
        UrlValidatorInterface $urlValidator,
37
        EntityManagerInterface $em,
38
        UrlShortenerOptions $options
39
    ) {
40 15
        $this->urlValidator = $urlValidator;
41 15
        $this->em = $em;
42 15
        $this->options = $options;
43
    }
44
45
    /**
46
     * @param string[] $tags
47
     * @throws NonUniqueSlugException
48
     * @throws InvalidUrlException
49
     * @throws Throwable
50
     */
51 14
    public function urlToShortCode(UriInterface $url, array $tags, ShortUrlMeta $meta): ShortUrl
52
    {
53 14
        $url = (string) $url;
54
55
        // First, check if a short URL exists for all provided params
56 14
        $existingShortUrl = $this->findExistingShortUrlIfExists($url, $tags, $meta);
57 14
        if ($existingShortUrl !== null) {
58 9
            return $existingShortUrl;
59
        }
60
61
        // If the URL validation is enabled, check that the URL actually exists
62 5
        if ($this->options->isUrlValidationEnabled()) {
63 1
            $this->urlValidator->validateUrl($url);
64
        }
65
66 5
        $this->em->beginTransaction();
67 5
        $shortUrl = new ShortUrl($url, $meta, new PersistenceDomainResolver($this->em));
68 5
        $shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
69
70
        try {
71 5
            $this->verifyShortCodeUniqueness($meta, $shortUrl);
72 4
            $this->em->persist($shortUrl);
73 4
            $this->em->flush();
74 3
            $this->em->commit();
75 2
        } catch (Throwable $e) {
76 2
            if ($this->em->getConnection()->isTransactionActive()) {
77 1
                $this->em->rollback();
78 1
                $this->em->close();
79
            }
80
81 2
            throw $e;
82
        }
83
84 3
        return $shortUrl;
85
    }
86
87 14
    private function findExistingShortUrlIfExists(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl
88
    {
89 14
        if (! $meta->findIfExists()) {
90 5
            return null;
91
        }
92
93 9
        $criteria = ['longUrl' => $url];
94 9
        if ($meta->hasCustomSlug()) {
95 1
            $criteria['shortCode'] = $meta->getCustomSlug();
96
        }
97
        /** @var ShortUrl[] $shortUrls */
98 9
        $shortUrls = $this->em->getRepository(ShortUrl::class)->findBy($criteria);
99 9
        if (empty($shortUrls)) {
100
            return null;
101
        }
102
103
        // Iterate short URLs until one that matches is found, or return null otherwise
104
        return array_reduce($shortUrls, function (?ShortUrl $found, ShortUrl $shortUrl) use ($tags, $meta) {
105 9
            if ($found !== null) {
106 1
                return $found;
107
            }
108
109 9
            return $shortUrl->matchesCriteria($meta, $tags) ? $shortUrl : null;
110 9
        });
111
    }
112
113 5
    private function verifyShortCodeUniqueness(ShortUrlMeta $meta, ShortUrl $shortUrlToBeCreated): void
114
    {
115 5
        $shortCode = $shortUrlToBeCreated->getShortCode();
116 5
        $domain = $meta->getDomain();
117
118
        /** @var ShortUrlRepository $repo */
119 5
        $repo = $this->em->getRepository(ShortUrl::class);
120 5
        $otherShortUrlsExist = $repo->shortCodeIsInUse($shortCode, $domain);
121
122 5
        if ($otherShortUrlsExist && $meta->hasCustomSlug()) {
123 1
            throw NonUniqueSlugException::fromSlug($shortCode, $domain);
124
        }
125
126 4
        if ($otherShortUrlsExist) {
127 1
            $shortUrlToBeCreated->regenerateShortCode();
128 1
            $this->verifyShortCodeUniqueness($meta, $shortUrlToBeCreated);
129
        }
130
    }
131
132
    /**
133
     * @throws InvalidShortCodeException
134
     * @throws EntityDoesNotExistException
135
     */
136 1
    public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl
137
    {
138
        /** @var ShortUrlRepository $shortUrlRepo */
139 1
        $shortUrlRepo = $this->em->getRepository(ShortUrl::class);
140 1
        $shortUrl = $shortUrlRepo->findOneByShortCode($shortCode, $domain);
141 1
        if ($shortUrl === null) {
142
            throw EntityDoesNotExistException::createFromEntityAndConditions(ShortUrl::class, [
143
                'shortCode' => $shortCode,
144
                'domain' => $domain,
145
            ]);
146
        }
147
148 1
        return $shortUrl;
149
    }
150
}
151