Refactored UrlShortener public method to receibe DTOs instead of primitive params

This commit is contained in:
Alejandro Celaya 2019-01-29 13:55:47 +01:00
parent 5756609531
commit d61f5faf59
9 changed files with 74 additions and 78 deletions

View file

@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Util\ShortUrlBuilderTrait; use Shlinkio\Shlink\Core\Util\ShortUrlBuilderTrait;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -116,10 +117,12 @@ class GenerateShortUrlCommand extends Command
$shortCode = $this->urlShortener->urlToShortCode( $shortCode = $this->urlShortener->urlToShortCode(
new Uri($longUrl), new Uri($longUrl),
$tags, $tags,
$this->getOptionalDate($input, 'validSince'), ShortUrlMeta::createFromParams(
$this->getOptionalDate($input, 'validUntil'), $this->getOptionalDate($input, 'validSince'),
$customSlug, $this->getOptionalDate($input, 'validUntil'),
$maxVisits !== null ? (int) $maxVisits : null $customSlug,
$maxVisits !== null ? (int) $maxVisits : null
)
)->getShortCode(); )->getShortCode();
$shortUrl = $this->buildShortUrl($this->domainConfig, $shortCode); $shortUrl = $this->buildShortUrl($this->domainConfig, $shortCode);

View file

@ -17,11 +17,11 @@ final class CreateShortUrlData
public function __construct( public function __construct(
UriInterface $longUrl, UriInterface $longUrl,
array $tags = [], array $tags = [],
ShortUrlMeta $meta = null ?ShortUrlMeta $meta = null
) { ) {
$this->longUrl = $longUrl; $this->longUrl = $longUrl;
$this->tags = $tags; $this->tags = $tags;
$this->meta = $meta ?? ShortUrlMeta::createFromParams(); $this->meta = $meta ?? ShortUrlMeta::createEmpty();
} }
/** /**

View file

@ -138,4 +138,12 @@ final class ShortUrlMeta
{ {
return $this->maxVisits !== null; return $this->maxVisits !== null;
} }
public function withCustomSlug(string $customSlug): self
{
$clone = clone $this;
$clone->customSlug = $customSlug;
return $clone;
}
} }

View file

@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Service; namespace Shlinkio\Shlink\Core\Service;
use Cake\Chronos\Chronos;
use Cocur\Slugify\SlugifyInterface; use Cocur\Slugify\SlugifyInterface;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use GuzzleHttp\ClientInterface; use GuzzleHttp\ClientInterface;
@ -53,41 +52,35 @@ class UrlShortener implements UrlShortenerInterface
} }
/** /**
* @param string[] $tags
* @throws NonUniqueSlugException * @throws NonUniqueSlugException
* @throws InvalidUrlException * @throws InvalidUrlException
* @throws RuntimeException * @throws RuntimeException
*/ */
public function urlToShortCode( public function urlToShortCode(UriInterface $url, array $tags, ShortUrlMeta $meta): ShortUrl
UriInterface $url, {
array $tags = [],
?Chronos $validSince = null,
?Chronos $validUntil = null,
?string $customSlug = null,
?int $maxVisits = null
): ShortUrl {
// If the URL validation is enabled, check that the URL actually exists // If the URL validation is enabled, check that the URL actually exists
if ($this->options->isUrlValidationEnabled()) { if ($this->options->isUrlValidationEnabled()) {
$this->checkUrlExists($url); $this->checkUrlExists($url);
} }
$customSlug = $this->processCustomSlug($customSlug); $meta = $this->processCustomSlug($meta);
// Transactionally insert the short url, then generate the short code and finally update the short code // Transactionally insert the short url, then generate the short code and finally update the short code
try { try {
$this->em->beginTransaction(); $this->em->beginTransaction();
// First, create the short URL with an empty short code // First, create the short URL with an empty short code
$shortUrl = new ShortUrl( $shortUrl = new ShortUrl((string) $url, $meta);
(string) $url,
ShortUrlMeta::createFromParams($validSince, $validUntil, null, $maxVisits)
);
$this->em->persist($shortUrl); $this->em->persist($shortUrl);
$this->em->flush(); $this->em->flush();
// Generate the short code and persist it // Generate the short code and persist it if no custom slug was provided
// TODO Somehow provide the logic to calculate the shortCode to avoid the need of a setter if (! $meta->hasCustomSlug()) {
$shortCode = $customSlug ?? $this->convertAutoincrementIdToShortCode((float) $shortUrl->getId()); // TODO Somehow provide the logic to calculate the shortCode to avoid the need of a setter
$shortUrl->setShortCode($shortCode) $shortCode = $this->convertAutoincrementIdToShortCode((float) $shortUrl->getId());
->setTags($this->tagNamesToEntities($this->em, $tags)); $shortUrl->setShortCode($shortCode);
}
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
$this->em->flush(); $this->em->flush();
$this->em->commit(); $this->em->commit();
@ -130,25 +123,27 @@ class UrlShortener implements UrlShortenerInterface
return $chars[(int) $id] . $code; return $chars[(int) $id] . $code;
} }
private function processCustomSlug(?string $customSlug): ?string private function processCustomSlug(ShortUrlMeta $meta): ?ShortUrlMeta
{ {
if ($customSlug === null) { if (! $meta->hasCustomSlug()) {
return null; return $meta;
} }
// If a custom slug was provided, make sure it's unique // FIXME If the slug was generated while filtering the value originally, we would not need an immutable setter
$customSlug = $this->slugger->slugify($customSlug); // in ShortUrlMeta
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy(['shortCode' => $customSlug]); $customSlug = $this->slugger->slugify($meta->getCustomSlug());
if ($shortUrl !== null) {
/** @var ShortUrlRepository $repo */
$repo = $this->em->getRepository(ShortUrl::class);
$shortUrlsCount = $repo->count(['shortCode' => $customSlug]);
if ($shortUrlsCount > 0) {
throw NonUniqueSlugException::fromSlug($customSlug); throw NonUniqueSlugException::fromSlug($customSlug);
} }
return $customSlug; return $meta->withCustomSlug($customSlug);
} }
/** /**
* Tries to find the mapped URL for provided short code. Returns null if not found
*
* @throws InvalidShortCodeException * @throws InvalidShortCodeException
* @throws EntityDoesNotExistException * @throws EntityDoesNotExistException
*/ */

View file

@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Service; namespace Shlinkio\Shlink\Core\Service;
use Cake\Chronos\Chronos;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
@ -11,26 +10,19 @@ use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Exception\RuntimeException; use Shlinkio\Shlink\Core\Exception\RuntimeException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
interface UrlShortenerInterface interface UrlShortenerInterface
{ {
/** /**
* @param string[] $tags
* @throws NonUniqueSlugException * @throws NonUniqueSlugException
* @throws InvalidUrlException * @throws InvalidUrlException
* @throws RuntimeException * @throws RuntimeException
*/ */
public function urlToShortCode( public function urlToShortCode(UriInterface $url, array $tags, ShortUrlMeta $meta): ShortUrl;
UriInterface $url,
array $tags = [],
?Chronos $validSince = null,
?Chronos $validUntil = null,
?string $customSlug = null,
?int $maxVisits = null
): ShortUrl;
/** /**
* Tries to find the mapped URL for provided short code. Returns null if not found
*
* @throws InvalidShortCodeException * @throws InvalidShortCodeException
* @throws EntityDoesNotExistException * @throws EntityDoesNotExistException
*/ */

View file

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Service; namespace ShlinkioTest\Shlink\Core\Service;
use Cocur\Slugify\SlugifyInterface; use Cocur\Slugify\SlugifyInterface;
use Doctrine\Common\Persistence\ObjectRepository;
use Doctrine\DBAL\Connection; use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\ORMException; use Doctrine\ORM\ORMException;
@ -17,7 +16,9 @@ use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Zend\Diactoros\Uri; use Zend\Diactoros\Uri;
@ -49,8 +50,8 @@ class UrlShortenerTest extends TestCase
$shortUrl = $arguments[0]; $shortUrl = $arguments[0];
$shortUrl->setId('10'); $shortUrl->setId('10');
}); });
$repo = $this->prophesize(ObjectRepository::class); $repo = $this->prophesize(ShortUrlRepository::class);
$repo->findOneBy(Argument::any())->willReturn(null); $repo->count(Argument::any())->willReturn(0);
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
$this->slugger = $this->prophesize(SlugifyInterface::class); $this->slugger = $this->prophesize(SlugifyInterface::class);
@ -74,7 +75,11 @@ class UrlShortenerTest extends TestCase
public function urlIsProperlyShortened() public function urlIsProperlyShortened()
{ {
// 10 -> 12C1c // 10 -> 12C1c
$shortUrl = $this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar')); $shortUrl = $this->urlShortener->urlToShortCode(
new Uri('http://foobar.com/12345/hello?foo=bar'),
[],
ShortUrlMeta::createEmpty()
);
$this->assertEquals('12C1c', $shortUrl->getShortCode()); $this->assertEquals('12C1c', $shortUrl->getShortCode());
} }
@ -91,7 +96,11 @@ class UrlShortenerTest extends TestCase
$this->em->close()->shouldBeCalledOnce(); $this->em->close()->shouldBeCalledOnce();
$this->em->flush()->willThrow(new ORMException()); $this->em->flush()->willThrow(new ORMException());
$this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar')); $this->urlShortener->urlToShortCode(
new Uri('http://foobar.com/12345/hello?foo=bar'),
[],
ShortUrlMeta::createEmpty()
);
} }
/** /**
@ -105,7 +114,11 @@ class UrlShortenerTest extends TestCase
$this->httpClient->request(Argument::cetera())->willThrow( $this->httpClient->request(Argument::cetera())->willThrow(
new ClientException('', $this->prophesize(Request::class)->reveal()) new ClientException('', $this->prophesize(Request::class)->reveal())
); );
$this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar')); $this->urlShortener->urlToShortCode(
new Uri('http://foobar.com/12345/hello?foo=bar'),
[],
ShortUrlMeta::createEmpty()
);
} }
/** /**
@ -119,9 +132,7 @@ class UrlShortenerTest extends TestCase
$this->urlShortener->urlToShortCode( $this->urlShortener->urlToShortCode(
new Uri('http://foobar.com/12345/hello?foo=bar'), new Uri('http://foobar.com/12345/hello?foo=bar'),
[], [],
null, ShortUrlMeta::createFromRawData(['customSlug' => 'custom-slug'])
null,
'custom-slug'
); );
$slugify->shouldHaveBeenCalledOnce(); $slugify->shouldHaveBeenCalledOnce();
@ -135,24 +146,21 @@ class UrlShortenerTest extends TestCase
/** @var MethodProphecy $slugify */ /** @var MethodProphecy $slugify */
$slugify = $this->slugger->slugify('custom-slug')->willReturnArgument(); $slugify = $this->slugger->slugify('custom-slug')->willReturnArgument();
$repo = $this->prophesize(ShortUrlRepositoryInterface::class); $repo = $this->prophesize(ShortUrlRepository::class);
/** @var MethodProphecy $findBySlug */ $countBySlug = $repo->count(['shortCode' => 'custom-slug'])->willReturn(1);
$findBySlug = $repo->findOneBy(['shortCode' => 'custom-slug'])->willReturn(new ShortUrl(''));
$repo->findOneBy(Argument::cetera())->willReturn(null); $repo->findOneBy(Argument::cetera())->willReturn(null);
/** @var MethodProphecy $getRepo */ /** @var MethodProphecy $getRepo */
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
$slugify->shouldBeCalledOnce(); $slugify->shouldBeCalledOnce();
$findBySlug->shouldBeCalledOnce(); $countBySlug->shouldBeCalledOnce();
$getRepo->shouldBeCalled(); $getRepo->shouldBeCalled();
$this->expectException(NonUniqueSlugException::class); $this->expectException(NonUniqueSlugException::class);
$this->urlShortener->urlToShortCode( $this->urlShortener->urlToShortCode(
new Uri('http://foobar.com/12345/hello?foo=bar'), new Uri('http://foobar.com/12345/hello?foo=bar'),
[], [],
null, ShortUrlMeta::createFromRawData(['customSlug' => 'custom-slug'])
null,
'custom-slug'
); );
} }

View file

@ -53,17 +53,9 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction
$longUrl = $shortUrlData->getLongUrl(); $longUrl = $shortUrlData->getLongUrl();
$shortUrlMeta = $shortUrlData->getMeta(); $shortUrlMeta = $shortUrlData->getMeta();
$customSlug = $shortUrlMeta->getCustomSlug();
try { try {
$shortUrl = $this->urlShortener->urlToShortCode( $shortUrl = $this->urlShortener->urlToShortCode($longUrl, $shortUrlData->getTags(), $shortUrlMeta);
$longUrl,
$shortUrlData->getTags(),
$shortUrlMeta->getValidSince(),
$shortUrlMeta->getValidUntil(),
$customSlug,
$shortUrlMeta->getMaxVisits()
);
$transformer = new ShortUrlDataTransformer($this->domainConfig); $transformer = new ShortUrlDataTransformer($this->domainConfig);
return new JsonResponse($transformer->transform($shortUrl)); return new JsonResponse($transformer->transform($shortUrl));
@ -74,6 +66,7 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction
'message' => sprintf('Provided URL %s is invalid. Try with a different one.', $longUrl), 'message' => sprintf('Provided URL %s is invalid. Try with a different one.', $longUrl),
], self::STATUS_BAD_REQUEST); ], self::STATUS_BAD_REQUEST);
} catch (NonUniqueSlugException $e) { } catch (NonUniqueSlugException $e) {
$customSlug = $shortUrlMeta->getCustomSlug();
$this->logger->warning('Provided non-unique slug. {e}', ['e' => $e]); $this->logger->warning('Provided non-unique slug. {e}', ['e' => $e]);
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e), 'error' => RestUtils::getRestErrorCodeFromException($e),

View file

@ -10,6 +10,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Rest\Action\ShortUrl\CreateShortUrlAction; use Shlinkio\Shlink\Rest\Action\ShortUrl\CreateShortUrlAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
@ -86,9 +87,7 @@ class CreateShortUrlActionTest extends TestCase
$this->urlShortener->urlToShortCode( $this->urlShortener->urlToShortCode(
Argument::type(Uri::class), Argument::type(Uri::class),
Argument::type('array'), Argument::type('array'),
null, ShortUrlMeta::createFromRawData(['customSlug' => 'foo']),
null,
'foo',
Argument::cetera() Argument::cetera()
)->willThrow(NonUniqueSlugException::class)->shouldBeCalledOnce(); )->willThrow(NonUniqueSlugException::class)->shouldBeCalledOnce();

View file

@ -9,6 +9,7 @@ use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\ShortUrl\SingleStepCreateShortUrlAction; use Shlinkio\Shlink\Rest\Action\ShortUrl\SingleStepCreateShortUrlAction;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
@ -91,10 +92,7 @@ class SingleStepCreateShortUrlActionTest extends TestCase
return $argument; return $argument;
}), }),
[], [],
null, ShortUrlMeta::createEmpty()
null,
null,
null
)->willReturn(new ShortUrl('')); )->willReturn(new ShortUrl(''));
$resp = $this->action->handle($request); $resp = $this->action->handle($request);