Completed
Push — master ( a7d308...df23f2 )
by Alejandro
25s queued 11s
created

UrlShortener::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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