Passed
Pull Request — master (#510)
by Alejandro
06:36
created

UrlShortener::checkUrlExists()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Shlinkio\Shlink\Core\Service;
6
7
use Doctrine\ORM\EntityManagerInterface;
8
use Fig\Http\Message\RequestMethodInterface;
9
use GuzzleHttp\ClientInterface;
10
use GuzzleHttp\Exception\GuzzleException;
11
use GuzzleHttp\RequestOptions;
12
use Psr\Http\Message\UriInterface;
13
use Shlinkio\Shlink\Core\Domain\Resolver\PersistenceDomainResolver;
14
use Shlinkio\Shlink\Core\Entity\ShortUrl;
15
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
16
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
17
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
18
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
19
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
20
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
21
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
22
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
23
use Throwable;
24
25
use function array_reduce;
26
27
class UrlShortener implements UrlShortenerInterface
28
{
29
    use TagManagerTrait;
30
31
    /** @var ClientInterface */
32
    private $httpClient;
33
    /** @var EntityManagerInterface */
34
    private $em;
35
    /** @var UrlShortenerOptions */
36
    private $options;
37
38 15
    public function __construct(ClientInterface $httpClient, EntityManagerInterface $em, UrlShortenerOptions $options)
39
    {
40 15
        $this->httpClient = $httpClient;
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->checkUrlExists($url);
64
        }
65
66 4
        $this->em->beginTransaction();
67 4
        $shortUrl = new ShortUrl($url, $meta, new PersistenceDomainResolver($this->em));
68 4
        $shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
69
70
        try {
71 4
            $this->verifyShortCodeUniqueness($meta, $shortUrl);
72 3
            $this->em->persist($shortUrl);
73 3
            $this->em->flush();
74 2
            $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 2
        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 1
    private function checkUrlExists(string $url): void
114
    {
115
        try {
116 1
            $this->httpClient->request(RequestMethodInterface::METHOD_GET, $url, [
117 1
                RequestOptions::ALLOW_REDIRECTS => ['max' => 15],
118
            ]);
119 1
        } catch (GuzzleException $e) {
120 1
            throw InvalidUrlException::fromUrl($url, $e);
121
        }
122
    }
123
124 4
    private function verifyShortCodeUniqueness(ShortUrlMeta $meta, ShortUrl $shortUrlToBeCreated): void
125
    {
126 4
        $shortCode = $shortUrlToBeCreated->getShortCode();
127 4
        $domain = $meta->getDomain();
128
129
        /** @var ShortUrlRepository $repo */
130 4
        $repo = $this->em->getRepository(ShortUrl::class);
131 4
        $otherShortUrlsExist = $repo->shortCodeIsInUse($shortCode, $domain);
132
133 4
        if ($otherShortUrlsExist && $meta->hasCustomSlug()) {
134 1
            throw NonUniqueSlugException::fromSlug($shortCode, $domain);
135
        }
136
137 3
        if ($otherShortUrlsExist) {
138 1
            $shortUrlToBeCreated->regenerateShortCode();
139 1
            $this->verifyShortCodeUniqueness($meta, $shortUrlToBeCreated);
140
        }
141
    }
142
143
    /**
144
     * @throws InvalidShortCodeException
145
     * @throws EntityDoesNotExistException
146
     */
147 1
    public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl
148
    {
149
        /** @var ShortUrlRepository $shortUrlRepo */
150 1
        $shortUrlRepo = $this->em->getRepository(ShortUrl::class);
151 1
        $shortUrl = $shortUrlRepo->findOneByShortCode($shortCode, $domain);
152 1
        if ($shortUrl === null) {
153
            throw EntityDoesNotExistException::createFromEntityAndConditions(ShortUrl::class, [
154
                'shortCode' => $shortCode,
155
            ]);
156
        }
157
158 1
        return $shortUrl;
159
    }
160
}
161