Completed
Pull Request — develop (#688)
by Alejandro
05:47
created

UrlShortener   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 98
Duplicated Lines 0 %

Test Coverage

Coverage 97.83%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 47
c 1
b 0
f 0
dl 0
loc 98
ccs 45
cts 46
cp 0.9783
rs 10
wmc 15

4 Methods

Rating   Name   Duplication   Size   Complexity  
A findExistingShortUrlIfExists() 0 23 6
A verifyShortCodeUniqueness() 0 16 4
A urlToShortCode() 0 30 4
A __construct() 0 8 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\DomainResolverInterface;
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\Model\ShortUrlMeta;
14
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
15
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
16
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
17
use Throwable;
18
19
use function array_reduce;
20
21
class UrlShortener implements UrlShortenerInterface
22
{
23
    use TagManagerTrait;
24
25
    private EntityManagerInterface $em;
26
    private UrlValidatorInterface $urlValidator;
27
    private DomainResolverInterface $domainResolver;
28
29 13
    public function __construct(
30
        UrlValidatorInterface $urlValidator,
31
        EntityManagerInterface $em,
32
        DomainResolverInterface $domainResolver
33
    ) {
34 13
        $this->urlValidator = $urlValidator;
35 13
        $this->em = $em;
36 13
        $this->domainResolver = $domainResolver;
37
    }
38
39
    /**
40
     * @param string[] $tags
41
     * @throws NonUniqueSlugException
42
     * @throws InvalidUrlException
43
     * @throws Throwable
44
     */
45 13
    public function urlToShortCode(UriInterface $url, array $tags, ShortUrlMeta $meta): ShortUrl
46
    {
47 13
        $url = (string) $url;
48
49
        // First, check if a short URL exists for all provided params
50 13
        $existingShortUrl = $this->findExistingShortUrlIfExists($url, $tags, $meta);
51 13
        if ($existingShortUrl !== null) {
52 9
            return $existingShortUrl;
53
        }
54
55 4
        $this->urlValidator->validateUrl($url);
56 4
        $this->em->beginTransaction();
57 4
        $shortUrl = new ShortUrl($url, $meta, $this->domainResolver);
58 4
        $shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
59
60
        try {
61 4
            $this->verifyShortCodeUniqueness($meta, $shortUrl);
62 3
            $this->em->persist($shortUrl);
63 3
            $this->em->flush();
64 2
            $this->em->commit();
65 2
        } catch (Throwable $e) {
66 2
            if ($this->em->getConnection()->isTransactionActive()) {
67 1
                $this->em->rollback();
68 1
                $this->em->close();
69
            }
70
71 2
            throw $e;
72
        }
73
74 2
        return $shortUrl;
75
    }
76
77 13
    private function findExistingShortUrlIfExists(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl
78
    {
79 13
        if (! $meta->findIfExists()) {
80 4
            return null;
81
        }
82
83 9
        $criteria = ['longUrl' => $url];
84 9
        if ($meta->hasCustomSlug()) {
85 1
            $criteria['shortCode'] = $meta->getCustomSlug();
86
        }
87
        /** @var ShortUrl[] $shortUrls */
88 9
        $shortUrls = $this->em->getRepository(ShortUrl::class)->findBy($criteria);
89 9
        if (empty($shortUrls)) {
90
            return null;
91
        }
92
93
        // Iterate short URLs until one that matches is found, or return null otherwise
94
        return array_reduce($shortUrls, function (?ShortUrl $found, ShortUrl $shortUrl) use ($tags, $meta) {
95 9
            if ($found !== null) {
96 1
                return $found;
97
            }
98
99 9
            return $shortUrl->matchesCriteria($meta, $tags) ? $shortUrl : null;
100 9
        });
101
    }
102
103 4
    private function verifyShortCodeUniqueness(ShortUrlMeta $meta, ShortUrl $shortUrlToBeCreated): void
104
    {
105 4
        $shortCode = $shortUrlToBeCreated->getShortCode();
106 4
        $domain = $meta->getDomain();
107
108
        /** @var ShortUrlRepository $repo */
109 4
        $repo = $this->em->getRepository(ShortUrl::class);
110 4
        $otherShortUrlsExist = $repo->shortCodeIsInUse($shortCode, $domain);
111
112 4
        if ($otherShortUrlsExist && $meta->hasCustomSlug()) {
113 1
            throw NonUniqueSlugException::fromSlug($shortCode, $domain);
114
        }
115
116 3
        if ($otherShortUrlsExist) {
117 1
            $shortUrlToBeCreated->regenerateShortCode();
118 1
            $this->verifyShortCodeUniqueness($meta, $shortUrlToBeCreated);
119
        }
120
    }
121
}
122