Created DTO used to transfer props needed to uniquely identify a short URL

This commit is contained in:
Alejandro Celaya 2020-02-01 11:46:54 +01:00
parent e18187f04e
commit 327d35fe57
24 changed files with 134 additions and 67 deletions

View File

@ -13,7 +13,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
#### Changed #### Changed
* [#577](https://github.com/shlinkio/shlink/issues/577) Wrapped params used to customize short URL lists into a DTO with implicit validation. * [#577](https://github.com/shlinkio/shlink/issues/577) Wrapped params used to customize short URL lists into a DTO with implicit validation.
* [#620](https://github.com/shlinkio/shlink/issues/620) "Controlled" errors (like validation errors and such) will no longer be logged with error level, preventing logs to be polluted.
#### Deprecated #### Deprecated
@ -25,7 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
#### Fixed #### Fixed
* *Nothing* * [#620](https://github.com/shlinkio/shlink/issues/620) Ensured "controlled" errors (like validation errors and such) won't be logged with error level, preventing logs to be polluted.
## 2.0.3 - 2020-01-27 ## 2.0.3 - 2020-01-27

View File

@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -54,11 +55,9 @@ class ResolveUrlCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): ?int protected function execute(InputInterface $input, OutputInterface $output): ?int
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$shortCode = $input->getArgument('shortCode');
$domain = $input->getOption('domain');
try { try {
$url = $this->urlResolver->shortCodeToShortUrl($shortCode, $domain); $url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromCli($input));
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl())); $output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
return ExitCodes::EXIT_SUCCESS; return ExitCodes::EXIT_SUCCESS;
} catch (ShortUrlNotFoundException $e) { } catch (ShortUrlNotFoundException $e) {

View File

@ -9,6 +9,7 @@ use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand; use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
use Shlinkio\Shlink\Core\Exception; use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
@ -56,7 +57,7 @@ class DeleteShortUrlCommandTest extends TestCase
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow( $deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
Exception\ShortUrlNotFoundException::fromNotFoundShortCode($shortCode), Exception\ShortUrlNotFoundException::fromNotFound(new ShortUrlIdentifier($shortCode)),
); );
$this->commandTester->execute(['shortCode' => $shortCode]); $this->commandTester->execute(['shortCode' => $shortCode]);

View File

@ -9,6 +9,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand; use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
@ -38,7 +39,7 @@ class ResolveUrlCommandTest extends TestCase
$shortCode = 'abc123'; $shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar'; $expectedUrl = 'http://domain.com/foo/bar';
$shortUrl = new ShortUrl($expectedUrl); $shortUrl = new ShortUrl($expectedUrl);
$this->urlResolver->shortCodeToShortUrl($shortCode, null)->willReturn($shortUrl) $this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode))->willReturn($shortUrl)
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$this->commandTester->execute(['shortCode' => $shortCode]); $this->commandTester->execute(['shortCode' => $shortCode]);
@ -49,9 +50,11 @@ class ResolveUrlCommandTest extends TestCase
/** @test */ /** @test */
public function incorrectShortCodeOutputsErrorMessage(): void public function incorrectShortCodeOutputsErrorMessage(): void
{ {
$shortCode = 'abc123'; $identifier = new ShortUrlIdentifier('abc123');
$this->urlResolver->shortCodeToShortUrl($shortCode, null) $shortCode = $identifier->shortCode();
->willThrow(ShortUrlNotFoundException::fromNotFoundShortCode($shortCode))
$this->urlResolver->resolveShortUrl($identifier)
->willThrow(ShortUrlNotFoundException::fromNotFound($identifier))
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$this->commandTester->execute(['shortCode' => $shortCode]); $this->commandTester->execute(['shortCode' => $shortCode]);

View File

@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
@ -45,12 +46,12 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$shortCode = $request->getAttribute('shortCode', ''); $shortCode = $request->getAttribute('shortCode', '');
$domain = $request->getUri()->getAuthority(); $identifier = ShortUrlIdentifier::fromRequest($request);
$query = $request->getQueryParams(); $query = $request->getQueryParams();
$disableTrackParam = $this->appOptions->getDisableTrackParam(); $disableTrackParam = $this->appOptions->getDisableTrackParam();
try { try {
$url = $this->urlResolver->shortCodeToEnabledShortUrl($shortCode, $domain); $url = $this->urlResolver->resolveEnabledShortUrl($identifier);
// Track visit to this short code // Track visit to this short code
if ($disableTrackParam === null || ! array_key_exists($disableTrackParam, $query)) { if ($disableTrackParam === null || ! array_key_exists($disableTrackParam, $query)) {

View File

@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Shlinkio\Shlink\Common\Response\QrCodeResponse; use Shlinkio\Shlink\Common\Response\QrCodeResponse;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
class QrCodeAction implements MiddlewareInterface class QrCodeAction implements MiddlewareInterface
@ -38,18 +39,16 @@ class QrCodeAction implements MiddlewareInterface
public function process(Request $request, RequestHandlerInterface $handler): Response public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
// Make sure the short URL exists for this short code $identifier = ShortUrlIdentifier::fromRequest($request);
$shortCode = $request->getAttribute('shortCode');
$domain = $request->getUri()->getAuthority();
try { try {
$this->urlResolver->shortCodeToEnabledShortUrl($shortCode, $domain); $this->urlResolver->resolveEnabledShortUrl($identifier);
} catch (ShortUrlNotFoundException $e) { } catch (ShortUrlNotFoundException $e) {
$this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]); $this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]);
return $handler->handle($request); return $handler->handle($request);
} }
$path = $this->router->generateUri(RedirectAction::class, ['shortCode' => $shortCode]); $path = $this->router->generateUri(RedirectAction::class, ['shortCode' => $identifier->shortCode()]);
$size = $this->getSizeParam($request); $size = $this->getSizeParam($request);
$qrCode = new QrCode((string) $request->getUri()->withPath($path)->withQuery('')); $qrCode = new QrCode((string) $request->getUri()->withPath($path)->withQuery(''));

View File

@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\Exception;
use Fig\Http\Message\StatusCodeInterface; use Fig\Http\Message\StatusCodeInterface;
use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use function sprintf; use function sprintf;
@ -17,8 +18,10 @@ class ShortUrlNotFoundException extends DomainException implements ProblemDetail
private const TITLE = 'Short URL not found'; private const TITLE = 'Short URL not found';
private const TYPE = 'INVALID_SHORTCODE'; private const TYPE = 'INVALID_SHORTCODE';
public static function fromNotFoundShortCode(string $shortCode, ?string $domain = null): self public static function fromNotFound(ShortUrlIdentifier $identifier): self
{ {
$shortCode = $identifier->shortCode();
$domain = $identifier->domain();
$suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain); $suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
$e = new self(sprintf('No URL found with short code "%s"%s', $shortCode, $suffix)); $e = new self(sprintf('No URL found with short code "%s"%s', $shortCode, $suffix));

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Console\Input\InputInterface;
final class ShortUrlIdentifier
{
private string $shortCode;
private ?string $domain;
public function __construct(string $shortCode, ?string $domain = null)
{
$this->shortCode = $shortCode;
$this->domain = $domain;
}
public static function fromRequest(ServerRequestInterface $request): self
{
$shortCode = $request->getAttribute('shortCode', '');
$domain = $request->getQueryParams()['domain'] ?? null;
return new self($shortCode, $domain);
}
public static function fromCli(InputInterface $input): self
{
$shortCode = $input->getArgument('shortCode');
$domain = $input->getOption('domain');
return new self($shortCode, $domain);
}
public function shortCode(): string
{
return $this->shortCode;
}
public function domain(): ?string
{
return $this->domain;
}
}

View File

@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\Service\ShortUrl;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception; use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Options\DeleteShortUrlsOptions; use Shlinkio\Shlink\Core\Options\DeleteShortUrlsOptions;
class DeleteShortUrlService implements DeleteShortUrlServiceInterface class DeleteShortUrlService implements DeleteShortUrlServiceInterface
@ -28,7 +29,7 @@ class DeleteShortUrlService implements DeleteShortUrlServiceInterface
*/ */
public function deleteByShortCode(string $shortCode, bool $ignoreThreshold = false): void public function deleteByShortCode(string $shortCode, bool $ignoreThreshold = false): void
{ {
$shortUrl = $this->findByShortCode($this->em, $shortCode); $shortUrl = $this->findByShortCode($this->em, new ShortUrlIdentifier($shortCode));
if (! $ignoreThreshold && $this->isThresholdReached($shortUrl)) { if (! $ignoreThreshold && $this->isThresholdReached($shortUrl)) {
throw Exception\DeleteShortUrlException::fromVisitsThreshold( throw Exception\DeleteShortUrlException::fromVisitsThreshold(
$this->deleteShortUrlsOptions->getVisitsThreshold(), $this->deleteShortUrlsOptions->getVisitsThreshold(),

View File

@ -7,20 +7,21 @@ namespace Shlinkio\Shlink\Core\Service\ShortUrl;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
trait FindShortCodeTrait trait FindShortCodeTrait
{ {
/** /**
* @throws ShortUrlNotFoundException * @throws ShortUrlNotFoundException
*/ */
private function findByShortCode(EntityManagerInterface $em, string $shortCode): ShortUrl private function findByShortCode(EntityManagerInterface $em, ShortUrlIdentifier $identifier): ShortUrl
{ {
/** @var ShortUrl|null $shortUrl */ /** @var ShortUrlRepositoryInterface $repo */
$shortUrl = $em->getRepository(ShortUrl::class)->findOneBy([ $repo = $em->getRepository(ShortUrl::class);
'shortCode' => $shortCode, $shortUrl = $repo->findOneByShortCode($identifier->shortCode(), $identifier->domain());
]);
if ($shortUrl === null) { if ($shortUrl === null) {
throw ShortUrlNotFoundException::fromNotFoundShortCode($shortCode); throw ShortUrlNotFoundException::fromNotFound($identifier);
} }
return $shortUrl; return $shortUrl;

View File

@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\Service\ShortUrl;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository; use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
class ShortUrlResolver implements ShortUrlResolverInterface class ShortUrlResolver implements ShortUrlResolverInterface
@ -21,13 +22,13 @@ class ShortUrlResolver implements ShortUrlResolverInterface
/** /**
* @throws ShortUrlNotFoundException * @throws ShortUrlNotFoundException
*/ */
public function shortCodeToShortUrl(string $shortCode, ?string $domain = null): ShortUrl public function resolveShortUrl(ShortUrlIdentifier $identifier): ShortUrl
{ {
/** @var ShortUrlRepository $shortUrlRepo */ /** @var ShortUrlRepository $shortUrlRepo */
$shortUrlRepo = $this->em->getRepository(ShortUrl::class); $shortUrlRepo = $this->em->getRepository(ShortUrl::class);
$shortUrl = $shortUrlRepo->findOneByShortCode($shortCode, $domain); $shortUrl = $shortUrlRepo->findOneByShortCode($identifier->shortCode(), $identifier->domain());
if ($shortUrl === null) { if ($shortUrl === null) {
throw ShortUrlNotFoundException::fromNotFoundShortCode($shortCode, $domain); throw ShortUrlNotFoundException::fromNotFound($identifier);
} }
return $shortUrl; return $shortUrl;
@ -36,11 +37,11 @@ class ShortUrlResolver implements ShortUrlResolverInterface
/** /**
* @throws ShortUrlNotFoundException * @throws ShortUrlNotFoundException
*/ */
public function shortCodeToEnabledShortUrl(string $shortCode, ?string $domain = null): ShortUrl public function resolveEnabledShortUrl(ShortUrlIdentifier $identifier): ShortUrl
{ {
$shortUrl = $this->shortCodeToShortUrl($shortCode, $domain); $shortUrl = $this->resolveShortUrl($identifier);
if (! $shortUrl->isEnabled()) { if (! $shortUrl->isEnabled()) {
throw ShortUrlNotFoundException::fromNotFoundShortCode($shortCode, $domain); throw ShortUrlNotFoundException::fromNotFound($identifier);
} }
return $shortUrl; return $shortUrl;

View File

@ -6,16 +6,17 @@ namespace Shlinkio\Shlink\Core\Service\ShortUrl;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
interface ShortUrlResolverInterface interface ShortUrlResolverInterface
{ {
/** /**
* @throws ShortUrlNotFoundException * @throws ShortUrlNotFoundException
*/ */
public function shortCodeToShortUrl(string $shortCode, ?string $domain = null): ShortUrl; public function resolveShortUrl(ShortUrlIdentifier $identifier): ShortUrl;
/** /**
* @throws ShortUrlNotFoundException * @throws ShortUrlNotFoundException
*/ */
public function shortCodeToEnabledShortUrl(string $shortCode, ?string $domain = null): ShortUrl; public function resolveEnabledShortUrl(ShortUrlIdentifier $identifier): ShortUrl;
} }

View File

@ -8,6 +8,7 @@ use Doctrine\ORM;
use Laminas\Paginator\Paginator; use Laminas\Paginator\Paginator;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter; use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
@ -47,7 +48,7 @@ class ShortUrlService implements ShortUrlServiceInterface
*/ */
public function setTagsByShortCode(string $shortCode, array $tags = []): ShortUrl public function setTagsByShortCode(string $shortCode, array $tags = []): ShortUrl
{ {
$shortUrl = $this->findByShortCode($this->em, $shortCode); $shortUrl = $this->findByShortCode($this->em, new ShortUrlIdentifier($shortCode));
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags)); $shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
$this->em->flush(); $this->em->flush();
@ -59,7 +60,7 @@ class ShortUrlService implements ShortUrlServiceInterface
*/ */
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortUrlMeta): ShortUrl public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortUrlMeta): ShortUrl
{ {
$shortUrl = $this->findByShortCode($this->em, $shortCode); $shortUrl = $this->findByShortCode($this->em, new ShortUrlIdentifier($shortCode));
$shortUrl->updateMeta($shortUrlMeta); $shortUrl->updateMeta($shortUrlMeta);
$this->em->flush(); $this->em->flush();

View File

@ -11,6 +11,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\EventDispatcher\ShortUrlVisited; use Shlinkio\Shlink\Core\EventDispatcher\ShortUrlVisited;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter; use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter;
@ -56,7 +57,7 @@ class VisitsTracker implements VisitsTrackerInterface
/** @var ORM\EntityRepository $repo */ /** @var ORM\EntityRepository $repo */
$repo = $this->em->getRepository(ShortUrl::class); $repo = $this->em->getRepository(ShortUrl::class);
if ($repo->count(['shortCode' => $shortCode]) < 1) { if ($repo->count(['shortCode' => $shortCode]) < 1) {
throw ShortUrlNotFoundException::fromNotFoundShortCode($shortCode); throw ShortUrlNotFoundException::fromNotFound(new ShortUrlIdentifier($shortCode)); // FIXME
} }
/** @var VisitRepository $repo */ /** @var VisitRepository $repo */

View File

@ -15,7 +15,7 @@ interface VisitsTrackerInterface
/** /**
* Tracks a new visit to provided short code from provided visitor * Tracks a new visit to provided short code from provided visitor
*/ */
public function track(string $shortCode, Visitor $visitor): void; public function track(string $shortCode, Visitor $visitor): void; // FIXME
/** /**
* Returns the visits on certain short code * Returns the visits on certain short code
@ -23,5 +23,5 @@ interface VisitsTrackerInterface
* @return Visit[]|Paginator * @return Visit[]|Paginator
* @throws ShortUrlNotFoundException * @throws ShortUrlNotFoundException
*/ */
public function info(string $shortCode, VisitsParams $params): Paginator; public function info(string $shortCode, VisitsParams $params): Paginator; // FIXME
} }

View File

@ -12,6 +12,7 @@ use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Common\Response\PixelResponse; use Shlinkio\Shlink\Common\Response\PixelResponse;
use Shlinkio\Shlink\Core\Action\PixelAction; use Shlinkio\Shlink\Core\Action\PixelAction;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTracker;
@ -38,7 +39,7 @@ class PixelActionTest extends TestCase
public function imageIsReturned(): void public function imageIsReturned(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlResolver->shortCodeToEnabledShortUrl($shortCode, '')->willReturn( $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn(
new ShortUrl('http://domain.com/foo/bar'), new ShortUrl('http://domain.com/foo/bar'),
)->shouldBeCalledOnce(); )->shouldBeCalledOnce();
$this->visitTracker->track(Argument::cetera())->shouldBeCalledOnce(); $this->visitTracker->track(Argument::cetera())->shouldBeCalledOnce();

View File

@ -15,6 +15,7 @@ use Shlinkio\Shlink\Common\Response\QrCodeResponse;
use Shlinkio\Shlink\Core\Action\QrCodeAction; use Shlinkio\Shlink\Core\Action\QrCodeAction;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
class QrCodeActionTest extends TestCase class QrCodeActionTest extends TestCase
@ -36,7 +37,8 @@ class QrCodeActionTest extends TestCase
public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlResolver->shortCodeToEnabledShortUrl($shortCode, '')->willThrow(ShortUrlNotFoundException::class) $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
->willThrow(ShortUrlNotFoundException::class)
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$delegate = $this->prophesize(RequestHandlerInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
$process = $delegate->handle(Argument::any())->willReturn(new Response()); $process = $delegate->handle(Argument::any())->willReturn(new Response());
@ -50,7 +52,8 @@ class QrCodeActionTest extends TestCase
public function anInvalidShortCodeWillReturnNotFoundResponse(): void public function anInvalidShortCodeWillReturnNotFoundResponse(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlResolver->shortCodeToEnabledShortUrl($shortCode, '')->willThrow(ShortUrlNotFoundException::class) $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
->willThrow(ShortUrlNotFoundException::class)
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$delegate = $this->prophesize(RequestHandlerInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
$process = $delegate->handle(Argument::any())->willReturn(new Response()); $process = $delegate->handle(Argument::any())->willReturn(new Response());
@ -64,7 +67,8 @@ class QrCodeActionTest extends TestCase
public function aCorrectRequestReturnsTheQrCodeResponse(): void public function aCorrectRequestReturnsTheQrCodeResponse(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlResolver->shortCodeToEnabledShortUrl($shortCode, '')->willReturn(new ShortUrl('')) $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
->willReturn(new ShortUrl(''))
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$delegate = $this->prophesize(RequestHandlerInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);

View File

@ -13,6 +13,7 @@ use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Action\RedirectAction; use Shlinkio\Shlink\Core\Action\RedirectAction;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Options; use Shlinkio\Shlink\Core\Options;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
@ -45,7 +46,8 @@ class RedirectActionTest extends TestCase
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$shortUrl = new ShortUrl('http://domain.com/foo/bar?some=thing'); $shortUrl = new ShortUrl('http://domain.com/foo/bar?some=thing');
$shortCodeToUrl = $this->urlResolver->shortCodeToEnabledShortUrl($shortCode, '')->willReturn($shortUrl); $shortCodeToUrl = $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
->willReturn($shortUrl);
$track = $this->visitTracker->track(Argument::cetera())->will(function (): void { $track = $this->visitTracker->track(Argument::cetera())->will(function (): void {
}); });
@ -74,7 +76,8 @@ class RedirectActionTest extends TestCase
public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlResolver->shortCodeToEnabledShortUrl($shortCode, '')->willThrow(ShortUrlNotFoundException::class) $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
->willThrow(ShortUrlNotFoundException::class)
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$this->visitTracker->track(Argument::cetera())->shouldNotBeCalled(); $this->visitTracker->track(Argument::cetera())->shouldNotBeCalled();

View File

@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\Exception;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
class ShortUrlNotFoundExceptionTest extends TestCase class ShortUrlNotFoundExceptionTest extends TestCase
{ {
@ -23,7 +24,7 @@ class ShortUrlNotFoundExceptionTest extends TestCase
$expectedAdditional['domain'] = $domain; $expectedAdditional['domain'] = $domain;
} }
$e = ShortUrlNotFoundException::fromNotFoundShortCode($shortCode, $domain); $e = ShortUrlNotFoundException::fromNotFound(new ShortUrlIdentifier($shortCode, $domain));
$this->assertEquals($expectedMessage, $e->getMessage()); $this->assertEquals($expectedMessage, $e->getMessage());
$this->assertEquals($expectedMessage, $e->getDetail()); $this->assertEquals($expectedMessage, $e->getDetail());

View File

@ -28,15 +28,15 @@ class DeleteShortUrlServiceTest extends TestCase
public function setUp(): void public function setUp(): void
{ {
$shortUrl = (new ShortUrl(''))->setVisits( $shortUrl = (new ShortUrl(''))->setVisits(new ArrayCollection(
new ArrayCollection(map(range(0, 10), fn () => new Visit(new ShortUrl(''), Visitor::emptyInstance()))), map(range(0, 10), fn () => new Visit(new ShortUrl(''), Visitor::emptyInstance())),
); ));
$this->shortCode = $shortUrl->getShortCode(); $this->shortCode = $shortUrl->getShortCode();
$this->em = $this->prophesize(EntityManagerInterface::class); $this->em = $this->prophesize(EntityManagerInterface::class);
$repo = $this->prophesize(ShortUrlRepositoryInterface::class); $repo = $this->prophesize(ShortUrlRepositoryInterface::class);
$repo->findOneBy(Argument::type('array'))->willReturn($shortUrl); $repo->findOneByShortCode(Argument::cetera())->willReturn($shortUrl);
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
} }

View File

@ -12,6 +12,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
@ -41,7 +42,7 @@ class ShortUrlResolverTest extends TestCase
$findOneByShortCode = $repo->findOneByShortCode($shortCode, null)->willReturn($shortUrl); $findOneByShortCode = $repo->findOneByShortCode($shortCode, null)->willReturn($shortUrl);
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
$result = $this->urlResolver->shortCodeToShortUrl($shortCode); $result = $this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode));
$this->assertSame($shortUrl, $result); $this->assertSame($shortUrl, $result);
$findOneByShortCode->shouldHaveBeenCalledOnce(); $findOneByShortCode->shouldHaveBeenCalledOnce();
@ -61,7 +62,7 @@ class ShortUrlResolverTest extends TestCase
$findOneByShortCode->shouldBeCalledOnce(); $findOneByShortCode->shouldBeCalledOnce();
$getRepo->shouldBeCalledOnce(); $getRepo->shouldBeCalledOnce();
$this->urlResolver->shortCodeToShortUrl($shortCode); $this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode));
} }
/** @test */ /** @test */
@ -74,7 +75,7 @@ class ShortUrlResolverTest extends TestCase
$findOneByShortCode = $repo->findOneByShortCode($shortCode, null)->willReturn($shortUrl); $findOneByShortCode = $repo->findOneByShortCode($shortCode, null)->willReturn($shortUrl);
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
$result = $this->urlResolver->shortCodeToEnabledShortUrl($shortCode); $result = $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode));
$this->assertSame($shortUrl, $result); $this->assertSame($shortUrl, $result);
$findOneByShortCode->shouldHaveBeenCalledOnce(); $findOneByShortCode->shouldHaveBeenCalledOnce();
@ -97,7 +98,7 @@ class ShortUrlResolverTest extends TestCase
$findOneByShortCode->shouldBeCalledOnce(); $findOneByShortCode->shouldBeCalledOnce();
$getRepo->shouldBeCalledOnce(); $getRepo->shouldBeCalledOnce();
$this->urlResolver->shortCodeToEnabledShortUrl($shortCode); $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode));
} }
public function provideDisabledShortUrls(): iterable public function provideDisabledShortUrls(): iterable

View File

@ -57,7 +57,7 @@ class ShortUrlServiceTest extends TestCase
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$repo = $this->prophesize(ShortUrlRepository::class); $repo = $this->prophesize(ShortUrlRepository::class);
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(null) $repo->findOneByShortCode($shortCode, null)->willReturn(null)
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
@ -72,7 +72,7 @@ class ShortUrlServiceTest extends TestCase
$shortUrl->setTags(Argument::any())->shouldBeCalledOnce(); $shortUrl->setTags(Argument::any())->shouldBeCalledOnce();
$shortCode = 'abc123'; $shortCode = 'abc123';
$repo = $this->prophesize(ShortUrlRepository::class); $repo = $this->prophesize(ShortUrlRepository::class);
$repo->findOneBy(['shortCode' => $shortCode])->willReturn($shortUrl->reveal()) $repo->findOneByShortCode($shortCode, null)->willReturn($shortUrl->reveal())
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
@ -90,7 +90,7 @@ class ShortUrlServiceTest extends TestCase
$shortUrl = new ShortUrl(''); $shortUrl = new ShortUrl('');
$repo = $this->prophesize(ShortUrlRepository::class); $repo = $this->prophesize(ShortUrlRepository::class);
$findShortUrl = $repo->findOneBy(['shortCode' => 'abc123'])->willReturn($shortUrl); $findShortUrl = $repo->findOneByShortCode('abc123', null)->willReturn($shortUrl);
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
$flush = $this->em->flush()->willReturn(null); $flush = $this->em->flush()->willReturn(null);

View File

@ -8,6 +8,7 @@ use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
@ -32,11 +33,9 @@ class ResolveShortUrlAction extends AbstractRestAction
public function handle(Request $request): Response public function handle(Request $request): Response
{ {
$shortCode = $request->getAttribute('shortCode');
$domain = $request->getQueryParams()['domain'] ?? null;
$transformer = new ShortUrlDataTransformer($this->domainConfig); $transformer = new ShortUrlDataTransformer($this->domainConfig);
$url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromRequest($request));
$url = $this->urlResolver->shortCodeToShortUrl($shortCode, $domain);
return new JsonResponse($transformer->transform($url)); return new JsonResponse($transformer->transform($url));
} }
} }

View File

@ -8,6 +8,7 @@ use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Rest\Action\ShortUrl\ResolveShortUrlAction; use Shlinkio\Shlink\Rest\Action\ShortUrl\ResolveShortUrlAction;
@ -28,7 +29,7 @@ class ResolveShortUrlActionTest extends TestCase
public function correctShortCodeReturnsSuccess(): void public function correctShortCodeReturnsSuccess(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlResolver->shortCodeToShortUrl($shortCode, null)->willReturn( $this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode))->willReturn(
new ShortUrl('http://domain.com/foo/bar'), new ShortUrl('http://domain.com/foo/bar'),
)->shouldBeCalledOnce(); )->shouldBeCalledOnce();