Completed
Pull Request — master (#506)
by Alejandro
13:13
created

UrlShortener::urlToShortCode()   A

Complexity

Conditions 5
Paths 19

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 20
nc 19
nop 3
dl 0
loc 34
ccs 13
cts 13
cp 1
crap 5
rs 9.2888
c 0
b 0
f 0
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
    public function __construct(ClientInterface $httpClient, EntityManagerInterface $em, UrlShortenerOptions $options)
39
    {
40
        $this->httpClient = $httpClient;
41
        $this->em = $em;
42
        $this->options = $options;
43
    }
44
45 15
    /**
46
     * @param string[] $tags
47 15
     * @throws NonUniqueSlugException
48 15
     * @throws InvalidUrlException
49 15
     * @throws Throwable
50
     */
51
    public function urlToShortCode(UriInterface $url, array $tags, ShortUrlMeta $meta): ShortUrl
52
    {
53
        $url = (string) $url;
54
55
        // First, check if a short URL exists for all provided params
56
        $existingShortUrl = $this->findExistingShortUrlIfExists($url, $tags, $meta);
57
        if ($existingShortUrl !== null) {
58 13
            return $existingShortUrl;
59
        }
60 13
61
        // If the URL validation is enabled, check that the URL actually exists
62
        if ($this->options->isUrlValidationEnabled()) {
63 13
            $this->checkUrlExists($url);
64 13
        }
65 8
66
        $this->em->beginTransaction();
67
        $shortUrl = new ShortUrl($url, $meta, new PersistenceDomainResolver($this->em));
68
        $shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
69 5
70 1
        try {
71
            $this->verifyShortCodeUniqueness($meta, $shortUrl);
72 4
            $this->em->persist($shortUrl);
73
            $this->em->flush();
74
            $this->em->commit();
75
        } catch (Throwable $e) {
76 3
            if ($this->em->getConnection()->isTransactionActive()) {
77
                $this->em->rollback();
78
                $this->em->close();
79 3
            }
80 3
81 3
            throw $e;
82
        }
83
84 2
        return $shortUrl;
85
    }
86 2
87 2
    private function findExistingShortUrlIfExists(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl
88
    {
89 2
        if (! $meta->findIfExists()) {
90 2
            return null;
91
        }
92 2
93 2
        $criteria = ['longUrl' => $url];
94 1
        if ($meta->hasCustomSlug()) {
95 1
            $criteria['shortCode'] = $meta->getCustomSlug();
96 1
        }
97 1
        /** @var ShortUrl[] $shortUrls */
98
        $shortUrls = $this->em->getRepository(ShortUrl::class)->findBy($criteria);
99
        if (empty($shortUrls)) {
100 1
            return null;
101
        }
102
103
        // Iterate short URLs until one that matches is found, or return null otherwise
104 13
        return array_reduce($shortUrls, function (?ShortUrl $found, ShortUrl $shortUrl) use ($tags, $meta) {
105
            if ($found !== null) {
106 13
                return $found;
107 4
            }
108
109
            return $shortUrl->matchesCriteria($meta, $tags) ? $shortUrl : null;
110 9
        });
111 9
    }
112 1
113
    private function checkUrlExists(string $url): void
114
    {
115 9
        try {
116 9
            $this->httpClient->request(RequestMethodInterface::METHOD_GET, $url, [
117 1
                RequestOptions::ALLOW_REDIRECTS => ['max' => 15],
118
            ]);
119
        } catch (GuzzleException $e) {
120
            throw InvalidUrlException::fromUrl($url, $e);
121
        }
122 8
    }
123 1
124
    private function verifyShortCodeUniqueness(ShortUrlMeta $meta, ShortUrl $shortUrlToBeCreated): void
125
    {
126 8
        $shortCode = $shortUrlToBeCreated->getShortCode();
127 8
        $domain = $meta->getDomain();
128
129
        /** @var ShortUrlRepository $repo */
130 1
        $repo = $this->em->getRepository(ShortUrl::class);
131
        $otherShortUrlsExist = $repo->shortCodeIsInUse($shortCode, $domain);
132
133 1
        if ($otherShortUrlsExist && $meta->hasCustomSlug()) {
134 1
            throw NonUniqueSlugException::fromSlug($shortCode, $domain);
135
        }
136 1
137 1
        if ($otherShortUrlsExist) {
138
            $shortUrlToBeCreated->regenerateShortCode();
139
            $this->verifyShortCodeUniqueness($meta, $shortUrlToBeCreated);
140
        }
141 4
    }
142
143 4
    /**
144 3
     * @throws InvalidShortCodeException
145
     * @throws EntityDoesNotExistException
146
     */
147 1
    public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl
148 1
    {
149
        /** @var ShortUrlRepository $shortUrlRepo */
150
        $shortUrlRepo = $this->em->getRepository(ShortUrl::class);
151 1
        $shortUrl = $shortUrlRepo->findOneByShortCode($shortCode, $domain);
152 1
        if ($shortUrl === null) {
153 1
            throw EntityDoesNotExistException::createFromEntityAndConditions(ShortUrl::class, [
154 1
                'shortCode' => $shortCode,
155
            ]);
156
        }
157
158 2
        return $shortUrl;
159
    }
160
}
161