diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bff8681..e4839205 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # CHANGELOG +## 1.11.0 - 2018-08-13 + +#### Added + +* [#170](https://github.com/shlinkio/shlink/issues/170) and [#171](https://github.com/shlinkio/shlink/issues/171) Updated `[GET /short-codes]` and `[GET /short-codes/{shortCode}]` endpoints to return more meaningful information and make their response consistent. + + The short URLs are now represented by this object in both cases: + + ```json + { + "shortCode": "12Kb3", + "shortUrl": "https://doma.in/12Kb3", + "longUrl": "https://shlink.io", + "dateCreated": "2016-05-01T20:34:16+02:00", + "visitsCount": 1029, + "tags": [ + "shlink" + ], + "originalUrl": "https://shlink.io" + } + ``` + + The `originalUrl` property is considered deprecated and has been kept for backward compatibility purposes. It holds the same value as the `longUrl` property. + +#### Changed + +* *Nothing* + +#### Deprecated + +* The `originalUrl` property in `[GET /short-codes]` and `[GET /short-codes/{shortCode}]` endpoints is now deprecated and replaced by the `longUrl` property. + +#### Removed + +* *Nothing* + +#### Fixed + +* *Nothing* + + ## 1.10.2 - 2018-08-04 #### Added diff --git a/composer.json b/composer.json index d12596d7..9d46a882 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ ], "require": { "php": "^7.1", + "ext-json": "*", "acelaya/ze-content-based-error-handler": "^2.2", "cocur/slugify": "^3.0", "doctrine/cache": "^1.6", diff --git a/docs/swagger/definitions/ShortUrl.json b/docs/swagger/definitions/ShortUrl.json index 6b1750b8..14cd0fe5 100644 --- a/docs/swagger/definitions/ShortUrl.json +++ b/docs/swagger/definitions/ShortUrl.json @@ -5,7 +5,11 @@ "type": "string", "description": "The short code for this short URL." }, - "originalUrl": { + "shortUrl": { + "type": "string", + "description": "The short URL." + }, + "longUrl": { "type": "string", "description": "The original long URL." }, @@ -24,6 +28,11 @@ "type": "string" }, "description": "A list of tags applied to this short URL" + }, + "originalUrl": { + "deprecated": true, + "type": "string", + "description": "The original long URL. [DEPRECATED. Use longUrl instead]" } } } diff --git a/docs/swagger/paths/v1_short-codes.json b/docs/swagger/paths/v1_short-codes.json index f377e67c..48d16a16 100644 --- a/docs/swagger/paths/v1_short-codes.json +++ b/docs/swagger/paths/v1_short-codes.json @@ -44,7 +44,7 @@ "schema": { "type": "string", "enum": [ - "originalUrl", + "longUrl", "shortCode", "dateCreated", "visits" @@ -89,7 +89,8 @@ "data": [ { "shortCode": "12C18", - "originalUrl": "https://store.steampowered.com", + "shortUrl": "https://doma.in/12C18", + "longUrl": "https://store.steampowered.com", "dateCreated": "2016-08-21T20:34:16+02:00", "visitsCount": 328, "tags": [ @@ -99,7 +100,8 @@ }, { "shortCode": "12Kb3", - "originalUrl": "https://shlink.io", + "shortUrl": "https://doma.in/12Kb3", + "longUrl": "https://shlink.io", "dateCreated": "2016-05-01T20:34:16+02:00", "visitsCount": 1029, "tags": [ @@ -108,7 +110,8 @@ }, { "shortCode": "123bA", - "originalUrl": "https://www.google.com", + "shortUrl": "https://doma.in/123bA", + "longUrl": "https://www.google.com", "dateCreated": "2015-10-01T20:34:16+02:00", "visitsCount": 25, "tags": [] diff --git a/docs/swagger/paths/v1_short-codes_{shortCode}.json b/docs/swagger/paths/v1_short-codes_{shortCode}.json index d0c26622..dba85af4 100644 --- a/docs/swagger/paths/v1_short-codes_{shortCode}.json +++ b/docs/swagger/paths/v1_short-codes_{shortCode}.json @@ -23,23 +23,24 @@ ], "responses": { "200": { - "description": "The long URL behind a short code.", + "description": "The URL info behind a short code.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "longUrl": { - "type": "string", - "description": "The original long URL behind the short code." - } - } + "$ref": "../definitions/ShortUrl.json" } } }, "examples": { "application/json": { - "longUrl": "https://shlink.io" + "shortCode": "12Kb3", + "shortUrl": "https://doma.in/12Kb3", + "longUrl": "https://shlink.io", + "dateCreated": "2016-05-01T20:34:16+02:00", + "visitsCount": 1029, + "tags": [ + "shlink" + ] } } }, diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 4dd461be..236b8d87 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -42,7 +42,11 @@ return [ 'config.url_shortener.domain', ], Command\Shortcode\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'], - Command\Shortcode\ListShortcodesCommand::class => [Service\ShortUrlService::class, 'translator'], + Command\Shortcode\ListShortcodesCommand::class => [ + Service\ShortUrlService::class, + 'translator', + 'config.url_shortener.domain', + ], Command\Shortcode\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'], Command\Shortcode\GeneratePreviewCommand::class => [ Service\ShortUrlService::class, diff --git a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php index 1e8ce12c..f0e9efa3 100644 --- a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php +++ b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php @@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Shortcode; use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter; use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; +use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -27,15 +28,23 @@ class ListShortcodesCommand extends Command * @var TranslatorInterface */ private $translator; + /** + * @var array + */ + private $domainConfig; - public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator) - { + public function __construct( + ShortUrlServiceInterface $shortUrlService, + TranslatorInterface $translator, + array $domainConfig + ) { $this->shortUrlService = $shortUrlService; $this->translator = $translator; parent::__construct(); + $this->domainConfig = $domainConfig; } - public function configure() + protected function configure(): void { $this->setName(self::NAME) ->setDescription($this->translator->translate('List all short URLs')) @@ -79,7 +88,7 @@ class ListShortcodesCommand extends Command ); } - public function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); $page = (int) $input->getOption('page'); @@ -87,6 +96,7 @@ class ListShortcodesCommand extends Command $tags = $input->getOption('tags'); $tags = ! empty($tags) ? \explode(',', $tags) : []; $showTags = $input->getOption('showTags'); + $transformer = new ShortUrlDataTransformer($this->domainConfig); do { $result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input)); @@ -94,7 +104,8 @@ class ListShortcodesCommand extends Command $headers = [ $this->translator->translate('Short code'), - $this->translator->translate('Original URL'), + $this->translator->translate('Short URL'), + $this->translator->translate('Long URL'), $this->translator->translate('Date created'), $this->translator->translate('Visits count'), ]; @@ -104,17 +115,14 @@ class ListShortcodesCommand extends Command $rows = []; foreach ($result as $row) { - $shortUrl = $row->jsonSerialize(); + $shortUrl = $transformer->transform($row); if ($showTags) { - $shortUrl['tags'] = []; - foreach ($row->getTags() as $tag) { - $shortUrl['tags'][] = $tag->getName(); - } $shortUrl['tags'] = implode(', ', $shortUrl['tags']); } else { unset($shortUrl['tags']); } + unset($shortUrl['originalUrl']); $rows[] = \array_values($shortUrl); } $io->table($headers, $rows); diff --git a/module/CLI/src/Command/Shortcode/ResolveUrlCommand.php b/module/CLI/src/Command/Shortcode/ResolveUrlCommand.php index fcf62fbe..2bd87a89 100644 --- a/module/CLI/src/Command/Shortcode/ResolveUrlCommand.php +++ b/module/CLI/src/Command/Shortcode/ResolveUrlCommand.php @@ -66,8 +66,10 @@ class ResolveUrlCommand extends Command $shortCode = $input->getArgument('shortCode'); try { - $longUrl = $this->urlShortener->shortCodeToUrl($shortCode); - $output->writeln(\sprintf('%s %s', $this->translator->translate('Long URL:'), $longUrl)); + $url = $this->urlShortener->shortCodeToUrl($shortCode); + $output->writeln( + \sprintf('%s %s', $this->translator->translate('Long URL:'), $url->getLongUrl()) + ); } catch (InvalidShortCodeException $e) { $io->error( \sprintf($this->translator->translate('Provided short code "%s" has an invalid format.'), $shortCode) diff --git a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php index 8aede93a..c79a2bb1 100644 --- a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php +++ b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php @@ -30,7 +30,7 @@ class ListShortcodesCommandTest extends TestCase { $this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class); $app = new Application(); - $command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([])); + $command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([]), []); $app->add($command); $this->commandTester = new CommandTester($command); } @@ -55,7 +55,7 @@ class ListShortcodesCommandTest extends TestCase // The paginator will return more than one page for the first 3 times $data = []; for ($i = 0; $i < 50; $i++) { - $data[] = new ShortUrl(); + $data[] = (new ShortUrl())->setLongUrl('url_' . $i); } $this->shortUrlService->listShortUrls(Argument::cetera())->will(function () use (&$data) { @@ -74,7 +74,7 @@ class ListShortcodesCommandTest extends TestCase // The paginator will return more than one page $data = []; for ($i = 0; $i < 30; $i++) { - $data[] = new ShortUrl(); + $data[] = (new ShortUrl())->setLongUrl('url_' . $i); } $this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data))) diff --git a/module/CLI/test/Command/Shortcode/ResolveUrlCommandTest.php b/module/CLI/test/Command/Shortcode/ResolveUrlCommandTest.php index d1bc50a3..dfa2e483 100644 --- a/module/CLI/test/Command/Shortcode/ResolveUrlCommandTest.php +++ b/module/CLI/test/Command/Shortcode/ResolveUrlCommandTest.php @@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Shortcode; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\UrlShortener; @@ -41,7 +42,8 @@ class ResolveUrlCommandTest extends TestCase { $shortCode = 'abc123'; $expectedUrl = 'http://domain.com/foo/bar'; - $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl) + $shortUrl = (new ShortUrl())->setLongUrl($expectedUrl); + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl) ->shouldBeCalledTimes(1); $this->commandTester->execute([ diff --git a/module/Common/src/Paginator/Util/PaginatorUtilsTrait.php b/module/Common/src/Paginator/Util/PaginatorUtilsTrait.php index 40f51d90..1b01de42 100644 --- a/module/Common/src/Paginator/Util/PaginatorUtilsTrait.php +++ b/module/Common/src/Paginator/Util/PaginatorUtilsTrait.php @@ -3,15 +3,16 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Common\Paginator\Util; +use Shlinkio\Shlink\Common\Rest\DataTransformerInterface; use Zend\Paginator\Paginator; use Zend\Stdlib\ArrayUtils; trait PaginatorUtilsTrait { - protected function serializePaginator(Paginator $paginator): array + private function serializePaginator(Paginator $paginator, ?DataTransformerInterface $transformer = null): array { return [ - 'data' => ArrayUtils::iteratorToArray($paginator->getCurrentItems()), + 'data' => $this->serializeItems(ArrayUtils::iteratorToArray($paginator->getCurrentItems()), $transformer), 'pagination' => [ 'currentPage' => $paginator->getCurrentPageNumber(), 'pagesCount' => $paginator->count(), @@ -22,13 +23,18 @@ trait PaginatorUtilsTrait ]; } + private function serializeItems(array $items, ?DataTransformerInterface $transformer = null): array + { + return $transformer === null ? $items : \array_map([$transformer, 'transform'], $items); + } + /** * Checks if provided paginator is in last page * * @param Paginator $paginator * @return bool */ - protected function isLastPage(Paginator $paginator): bool + private function isLastPage(Paginator $paginator): bool { return $paginator->getCurrentPageNumber() >= $paginator->count(); } diff --git a/module/Common/src/Rest/DataTransformerInterface.php b/module/Common/src/Rest/DataTransformerInterface.php new file mode 100644 index 00000000..933f6cce --- /dev/null +++ b/module/Common/src/Rest/DataTransformerInterface.php @@ -0,0 +1,9 @@ + [ 'httpClient', 'em', - Cache::class, 'config.url_shortener.validate_url', 'config.url_shortener.shortcode_chars', ], diff --git a/module/Core/src/Action/AbstractTrackingAction.php b/module/Core/src/Action/AbstractTrackingAction.php index 147e31b6..cfb4223c 100644 --- a/module/Core/src/Action/AbstractTrackingAction.php +++ b/module/Core/src/Action/AbstractTrackingAction.php @@ -65,14 +65,14 @@ abstract class AbstractTrackingAction implements MiddlewareInterface $disableTrackParam = $this->appOptions->getDisableTrackParam(); try { - $longUrl = $this->urlShortener->shortCodeToUrl($shortCode); + $url = $this->urlShortener->shortCodeToUrl($shortCode); // Track visit to this short code if ($disableTrackParam === null || ! \array_key_exists($disableTrackParam, $query)) { $this->visitTracker->track($shortCode, $request); } - return $this->createResp($longUrl); + return $this->createResp($url->getLongUrl()); } catch (InvalidShortCodeException | EntityDoesNotExistException $e) { $this->logger->warning('An error occurred while tracking short code.' . PHP_EOL . $e); return $this->buildErrorResponse($request, $handler); diff --git a/module/Core/src/Action/PreviewAction.php b/module/Core/src/Action/PreviewAction.php index 83748a3b..9f95e884 100644 --- a/module/Core/src/Action/PreviewAction.php +++ b/module/Core/src/Action/PreviewAction.php @@ -60,7 +60,7 @@ class PreviewAction implements MiddlewareInterface try { $url = $this->urlShortener->shortCodeToUrl($shortCode); - $imagePath = $this->previewGenerator->generatePreview($url); + $imagePath = $this->previewGenerator->generatePreview($url->getLongUrl()); return $this->generateImageResponse($imagePath); } catch (InvalidShortCodeException | EntityDoesNotExistException | PreviewGenerationException $e) { $this->logger->warning('An error occurred while generating preview image.' . PHP_EOL . $e); diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index ff21c47b..def81f01 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -16,7 +16,7 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity; * @ORM\Entity(repositoryClass="Shlinkio\Shlink\Core\Repository\ShortUrlRepository") * @ORM\Table(name="short_urls") */ -class ShortUrl extends AbstractEntity implements \JsonSerializable +class ShortUrl extends AbstractEntity { /** * @var string @@ -84,21 +84,40 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable /** * @return string */ - public function getOriginalUrl(): string + public function getLongUrl(): string { return $this->originalUrl; } /** - * @param string $originalUrl + * @param string $longUrl * @return $this */ - public function setOriginalUrl(string $originalUrl) + public function setLongUrl(string $longUrl): self { - $this->originalUrl = $originalUrl; + $this->originalUrl = $longUrl; return $this; } + /** + * @return string + * @deprecated Use getLongUrl() instead + */ + public function getOriginalUrl(): string + { + return $this->getLongUrl(); + } + + /** + * @param string $originalUrl + * @return $this + * @deprecated Use setLongUrl() instead + */ + public function setOriginalUrl(string $originalUrl): self + { + return $this->setLongUrl($originalUrl); + } + /** * @return string */ @@ -237,22 +256,4 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable { return $this->maxVisits !== null && $this->getVisitsCount() >= $this->maxVisits; } - - /** - * Specify data which should be serialized to JSON - * @link http://php.net/manual/en/jsonserializable.jsonserialize.php - * @return mixed data which can be serialized by json_encode, - * which is a value of any type other than a resource. - * @since 5.4.0 - */ - public function jsonSerialize() - { - return [ - 'shortCode' => $this->shortCode, - 'originalUrl' => $this->originalUrl, - 'dateCreated' => $this->dateCreated !== null ? $this->dateCreated->format(\DateTime::ATOM) : null, - 'visitsCount' => $this->getVisitsCount(), - 'tags' => $this->tags->toArray(), - ]; - } } diff --git a/module/Core/src/Repository/ShortUrlRepository.php b/module/Core/src/Repository/ShortUrlRepository.php index 42b2c73e..6a96f45f 100644 --- a/module/Core/src/Repository/ShortUrlRepository.php +++ b/module/Core/src/Repository/ShortUrlRepository.php @@ -47,6 +47,13 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI protected function processOrderByForList(QueryBuilder $qb, $orderBy) { + // Map public field names to column names + $fieldNameMap = [ + 'originalUrl' => 'originalUrl', + 'longUrl' => 'originalUrl', + 'shortCode' => 'shortCode', + 'dateCreated' => 'dateCreated', + ]; $fieldName = \is_array($orderBy) ? \key($orderBy) : $orderBy; $order = \is_array($orderBy) ? $orderBy[$fieldName] : 'ASC'; @@ -59,8 +66,8 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI return \array_column($qb->getQuery()->getResult(), 0); } - if (\in_array($fieldName, ['originalUrl', 'shortCode', 'dateCreated'], true)) { - $qb->orderBy('s.' . $fieldName, $order); + if (\array_key_exists($fieldName, $fieldNameMap)) { + $qb->orderBy('s.' . $fieldNameMap[$fieldName], $order); } return $qb->getQuery()->getResult(); } diff --git a/module/Core/src/Service/UrlShortener.php b/module/Core/src/Service/UrlShortener.php index 87621060..d5bf8cc2 100644 --- a/module/Core/src/Service/UrlShortener.php +++ b/module/Core/src/Service/UrlShortener.php @@ -5,7 +5,6 @@ namespace Shlinkio\Shlink\Core\Service; use Cocur\Slugify\Slugify; use Cocur\Slugify\SlugifyInterface; -use Doctrine\Common\Cache\Cache; use Doctrine\ORM\EntityManagerInterface; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\GuzzleException; @@ -23,7 +22,7 @@ class UrlShortener implements UrlShortenerInterface { use TagManagerTrait; - const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ'; + public const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ'; /** * @var ClientInterface @@ -37,10 +36,6 @@ class UrlShortener implements UrlShortenerInterface * @var string */ private $chars; - /** - * @var Cache - */ - private $cache; /** * @var SlugifyInterface */ @@ -53,14 +48,12 @@ class UrlShortener implements UrlShortenerInterface public function __construct( ClientInterface $httpClient, EntityManagerInterface $em, - Cache $cache, $urlValidationEnabled, $chars = self::DEFAULT_CHARS, SlugifyInterface $slugger = null ) { $this->httpClient = $httpClient; $this->em = $em; - $this->cache = $cache; $this->urlValidationEnabled = $urlValidationEnabled; $this->chars = empty($chars) ? self::DEFAULT_CHARS : $chars; $this->slugger = $slugger ?: new Slugify(); @@ -192,19 +185,11 @@ class UrlShortener implements UrlShortenerInterface /** * Tries to find the mapped URL for provided short code. Returns null if not found * - * @param string $shortCode - * @return string * @throws InvalidShortCodeException * @throws EntityDoesNotExistException */ - public function shortCodeToUrl(string $shortCode): string + public function shortCodeToUrl(string $shortCode): ShortUrl { - $cacheKey = sprintf('%s_longUrl', $shortCode); - // Check if the short code => URL map is already cached - if ($this->cache->contains($cacheKey)) { - return $this->cache->fetch($cacheKey); - } - // Validate short code format if (! preg_match('|[' . $this->chars . ']+|', $shortCode)) { throw InvalidShortCodeException::fromCharset($shortCode, $this->chars); @@ -219,9 +204,6 @@ class UrlShortener implements UrlShortenerInterface ]); } - // Cache the shortcode - $url = $shortUrl->getOriginalUrl(); - $this->cache->save($cacheKey, $url); - return $url; + return $shortUrl; } } diff --git a/module/Core/src/Service/UrlShortenerInterface.php b/module/Core/src/Service/UrlShortenerInterface.php index 9dd354d5..7b97af91 100644 --- a/module/Core/src/Service/UrlShortenerInterface.php +++ b/module/Core/src/Service/UrlShortenerInterface.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Service; use Psr\Http\Message\UriInterface; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; @@ -21,7 +22,6 @@ interface UrlShortenerInterface * @param \DateTime|null $validUntil * @param string|null $customSlug * @param int|null $maxVisits - * @return string * @throws NonUniqueSlugException * @throws InvalidUrlException * @throws RuntimeException @@ -38,10 +38,8 @@ interface UrlShortenerInterface /** * Tries to find the mapped URL for provided short code. Returns null if not found * - * @param string $shortCode - * @return string * @throws InvalidShortCodeException * @throws EntityDoesNotExistException */ - public function shortCodeToUrl(string $shortCode): string; + public function shortCodeToUrl(string $shortCode): ShortUrl; } diff --git a/module/Core/src/Transformer/ShortUrlDataTransformer.php b/module/Core/src/Transformer/ShortUrlDataTransformer.php new file mode 100644 index 00000000..c1113a81 --- /dev/null +++ b/module/Core/src/Transformer/ShortUrlDataTransformer.php @@ -0,0 +1,54 @@ +domainConfig = $domainConfig; + } + + /** + * @param ShortUrl $value + * @return array + */ + public function transform($value): array + { + $dateCreated = $value->getDateCreated(); + $longUrl = $value->getLongUrl(); + $shortCode = $value->getShortCode(); + + return [ + 'shortCode' => $shortCode, + 'shortUrl' => \sprintf( + '%s://%s/%s', + $this->domainConfig['schema'] ?? 'http', + $this->domainConfig['hostname'] ?? '', + $shortCode + ), + 'longUrl' => $longUrl, + 'dateCreated' => $dateCreated !== null ? $dateCreated->format(\DateTime::ATOM) : null, + 'visitsCount' => $value->getVisitsCount(), + 'tags' => \array_map([$this, 'serializeTag'], $value->getTags()->toArray()), + + // Deprecated + 'originalUrl' => $longUrl, + ]; + } + + private function serializeTag(Tag $tag): string + { + return $tag->getName(); + } +} diff --git a/module/Core/test-func/Repository/ShortUrlRepositoryTest.php b/module/Core/test-func/Repository/ShortUrlRepositoryTest.php index 1a0cf1e6..aa544fa6 100644 --- a/module/Core/test-func/Repository/ShortUrlRepositoryTest.php +++ b/module/Core/test-func/Repository/ShortUrlRepositoryTest.php @@ -112,4 +112,28 @@ class ShortUrlRepositoryTest extends DatabaseTestCase $this->assertCount(1, $result); $this->assertSame($foo, $result[0]); } + + /** + * @test + */ + public function findListProperlyMapsFieldNamesToColumnNamesWhenOrdering() + { + $urls = ['a', 'z', 'c', 'b']; + foreach ($urls as $url) { + $this->getEntityManager()->persist( + (new ShortUrl())->setShortCode($url) + ->setLongUrl($url) + ); + } + + $this->getEntityManager()->flush(); + + $result = $this->repo->findList(null, null, null, [], ['longUrl' => 'ASC']); + + $this->assertCount(\count($urls), $result); + $this->assertEquals('a', $result[0]->getLongUrl()); + $this->assertEquals('b', $result[1]->getLongUrl()); + $this->assertEquals('c', $result[2]->getLongUrl()); + $this->assertEquals('z', $result[3]->getLongUrl()); + } } diff --git a/module/Core/test/Action/PixelActionTest.php b/module/Core/test/Action/PixelActionTest.php index 1a7f7296..f827b0d1 100644 --- a/module/Core/test/Action/PixelActionTest.php +++ b/module/Core/test/Action/PixelActionTest.php @@ -9,6 +9,7 @@ use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Common\Response\PixelResponse; use Shlinkio\Shlink\Core\Action\PixelAction; use Shlinkio\Shlink\Core\Action\RedirectAction; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\VisitsTracker; @@ -48,8 +49,9 @@ class PixelActionTest extends TestCase public function imageIsReturned() { $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode)->willReturn('http://domain.com/foo/bar') - ->shouldBeCalledTimes(1); + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn( + (new ShortUrl())->setLongUrl('http://domain.com/foo/bar') + )->shouldBeCalledTimes(1); $this->visitTracker->track(Argument::cetera())->willReturn(null) ->shouldBeCalledTimes(1); diff --git a/module/Core/test/Action/PreviewActionTest.php b/module/Core/test/Action/PreviewActionTest.php index abd4dc35..d1c0a23d 100644 --- a/module/Core/test/Action/PreviewActionTest.php +++ b/module/Core/test/Action/PreviewActionTest.php @@ -10,6 +10,7 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Common\Service\PreviewGenerator; use Shlinkio\Shlink\Core\Action\PreviewAction; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\UrlShortener; @@ -64,8 +65,9 @@ class PreviewActionTest extends TestCase { $shortCode = 'abc123'; $url = 'foobar.com'; + $shortUrl = (new ShortUrl())->setLongUrl($url); $path = __FILE__; - $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($url)->shouldBeCalledTimes(1); + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)->shouldBeCalledTimes(1); $this->previewGenerator->generatePreview($url)->willReturn($path)->shouldBeCalledTimes(1); $resp = $this->action->process( diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php index 2d58c8c9..4e24303b 100644 --- a/module/Core/test/Action/QrCodeActionTest.php +++ b/module/Core/test/Action/QrCodeActionTest.php @@ -10,6 +10,7 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Common\Response\QrCodeResponse; use Shlinkio\Shlink\Core\Action\QrCodeAction; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\UrlShortener; @@ -83,7 +84,8 @@ class QrCodeActionTest extends TestCase public function aCorrectRequestReturnsTheQrCodeResponse() { $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode)->willReturn('')->shouldBeCalledTimes(1); + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn((new ShortUrl())->setLongUrl('')) + ->shouldBeCalledTimes(1); $delegate = $this->prophesize(RequestHandlerInterface::class); $resp = $this->action->process( diff --git a/module/Core/test/Action/RedirectActionTest.php b/module/Core/test/Action/RedirectActionTest.php index 88b44d07..0c83f8fe 100644 --- a/module/Core/test/Action/RedirectActionTest.php +++ b/module/Core/test/Action/RedirectActionTest.php @@ -9,6 +9,7 @@ use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Action\RedirectAction; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Service\UrlShortener; @@ -51,7 +52,8 @@ class RedirectActionTest extends TestCase { $shortCode = 'abc123'; $expectedUrl = 'http://domain.com/foo/bar'; - $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl) + $shortUrl = (new ShortUrl())->setLongUrl($expectedUrl); + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl) ->shouldBeCalledTimes(1); $this->visitTracker->track(Argument::cetera())->willReturn(null) ->shouldBeCalledTimes(1); @@ -93,7 +95,8 @@ class RedirectActionTest extends TestCase { $shortCode = 'abc123'; $expectedUrl = 'http://domain.com/foo/bar'; - $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl) + $shortUrl = (new ShortUrl())->setLongUrl($expectedUrl); + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl) ->shouldBeCalledTimes(1); $this->visitTracker->track(Argument::cetera())->willReturn(null) ->shouldNotBeCalled(); diff --git a/module/Core/test/Service/UrlShortenerTest.php b/module/Core/test/Service/UrlShortenerTest.php index 6f78c70c..5ba33612 100644 --- a/module/Core/test/Service/UrlShortenerTest.php +++ b/module/Core/test/Service/UrlShortenerTest.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\Service; use Cocur\Slugify\SlugifyInterface; -use Doctrine\Common\Cache\ArrayCache; -use Doctrine\Common\Cache\Cache; use Doctrine\Common\Persistence\ObjectRepository; use Doctrine\DBAL\Connection; use Doctrine\ORM\EntityManagerInterface; @@ -37,10 +35,6 @@ class UrlShortenerTest extends TestCase * @var ObjectProphecy */ protected $httpClient; - /** - * @var Cache - */ - protected $cache; /** * @var ObjectProphecy */ @@ -66,7 +60,6 @@ class UrlShortenerTest extends TestCase $repo->findOneBy(Argument::any())->willReturn(null); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); - $this->cache = new ArrayCache(); $this->slugger = $this->prophesize(SlugifyInterface::class); $this->setUrlShortener(false); @@ -80,7 +73,6 @@ class UrlShortenerTest extends TestCase $this->urlShortener = new UrlShortener( $this->httpClient->reveal(), $this->em->reveal(), - $this->cache, $urlValidationEnabled, UrlShortener::DEFAULT_CHARS, $this->slugger->reveal() @@ -205,10 +197,8 @@ class UrlShortenerTest extends TestCase $repo->findOneByShortCode($shortCode)->willReturn($shortUrl); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); - $this->assertFalse($this->cache->contains($shortCode . '_longUrl')); $url = $this->urlShortener->shortCodeToUrl($shortCode); - $this->assertEquals($shortUrl->getOriginalUrl(), $url); - $this->assertTrue($this->cache->contains($shortCode . '_longUrl')); + $this->assertSame($shortUrl, $url); } /** @@ -219,18 +209,4 @@ class UrlShortenerTest extends TestCase { $this->urlShortener->shortCodeToUrl('&/('); } - - /** - * @test - */ - public function cachedShortCodeDoesNotHitDatabase() - { - $shortCode = '12C1c'; - $expectedUrl = 'expected_url'; - $this->cache->save($shortCode . '_longUrl', $expectedUrl); - $this->em->getRepository(ShortUrl::class)->willReturn(null)->shouldBeCalledTimes(0); - - $url = $this->urlShortener->shortCodeToUrl($shortCode); - $this->assertEquals($expectedUrl, $url); - } } diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 304e667d..92c991e1 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -59,9 +59,18 @@ return [ 'Logger_Shlink', ], Action\ShortCode\EditShortCodeAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink',], - Action\ShortCode\ResolveUrlAction::class => [Service\UrlShortener::class, 'translator'], + Action\ShortCode\ResolveUrlAction::class => [ + Service\UrlShortener::class, + 'translator', + 'config.url_shortener.domain', + ], Action\Visit\GetVisitsAction::class => [Service\VisitsTracker::class, 'translator', 'Logger_Shlink'], - Action\ShortCode\ListShortCodesAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink'], + Action\ShortCode\ListShortCodesAction::class => [ + Service\ShortUrlService::class, + 'translator', + 'config.url_shortener.domain', + 'Logger_Shlink', + ], Action\ShortCode\EditShortCodeTagsAction::class => [ Service\ShortUrlService::class, 'translator', diff --git a/module/Rest/src/Action/ShortCode/ListShortCodesAction.php b/module/Rest/src/Action/ShortCode/ListShortCodesAction.php index 58f26d77..e69840a5 100644 --- a/module/Rest/src/Action/ShortCode/ListShortCodesAction.php +++ b/module/Rest/src/Action/ShortCode/ListShortCodesAction.php @@ -8,6 +8,7 @@ use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; +use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; @@ -28,15 +29,21 @@ class ListShortCodesAction extends AbstractRestAction * @var TranslatorInterface */ private $translator; + /** + * @var array + */ + private $domainConfig; public function __construct( ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator, + array $domainConfig, LoggerInterface $logger = null ) { parent::__construct($logger); $this->shortUrlService = $shortUrlService; $this->translator = $translator; + $this->domainConfig = $domainConfig; } /** @@ -49,7 +56,9 @@ class ListShortCodesAction extends AbstractRestAction try { $params = $this->queryToListParams($request->getQueryParams()); $shortUrls = $this->shortUrlService->listShortUrls(...$params); - return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls)]); + return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls, new ShortUrlDataTransformer( + $this->domainConfig + ))]); } catch (\Exception $e) { $this->logger->error('Unexpected error while listing short URLs.' . PHP_EOL . $e); return new JsonResponse([ diff --git a/module/Rest/src/Action/ShortCode/ResolveUrlAction.php b/module/Rest/src/Action/ShortCode/ResolveUrlAction.php index 8a7f2ba7..d847c4d9 100644 --- a/module/Rest/src/Action/ShortCode/ResolveUrlAction.php +++ b/module/Rest/src/Action/ShortCode/ResolveUrlAction.php @@ -9,6 +9,7 @@ use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; +use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; @@ -27,15 +28,21 @@ class ResolveUrlAction extends AbstractRestAction * @var TranslatorInterface */ private $translator; + /** + * @var array + */ + private $domainConfig; public function __construct( UrlShortenerInterface $urlShortener, TranslatorInterface $translator, + array $domainConfig, LoggerInterface $logger = null ) { parent::__construct($logger); $this->urlShortener = $urlShortener; $this->translator = $translator; + $this->domainConfig = $domainConfig; } /** @@ -46,17 +53,16 @@ class ResolveUrlAction extends AbstractRestAction public function handle(Request $request): Response { $shortCode = $request->getAttribute('shortCode'); + $transformer = new ShortUrlDataTransformer($this->domainConfig); try { - $longUrl = $this->urlShortener->shortCodeToUrl($shortCode); - return new JsonResponse([ - 'longUrl' => $longUrl, - ]); + $url = $this->urlShortener->shortCodeToUrl($shortCode); + return new JsonResponse($transformer->transform($url)); } catch (InvalidShortCodeException $e) { $this->logger->warning('Provided short code with invalid format.' . PHP_EOL . $e); return new JsonResponse([ 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf( + 'message' => \sprintf( $this->translator->translate('Provided short code "%s" has an invalid format'), $shortCode ), @@ -65,7 +71,7 @@ class ResolveUrlAction extends AbstractRestAction $this->logger->warning('Provided short code couldn\'t be found.' . PHP_EOL . $e); return new JsonResponse([ 'error' => RestUtils::INVALID_ARGUMENT_ERROR, - 'message' => sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode), + 'message' => \sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode), ], self::STATUS_NOT_FOUND); } catch (\Exception $e) { $this->logger->error('Unexpected error while resolving the URL behind a short code.' . PHP_EOL . $e); diff --git a/module/Rest/test/Action/ShortCode/ListShortCodesActionTest.php b/module/Rest/test/Action/ShortCode/ListShortCodesActionTest.php index 59912f5a..7c414e44 100644 --- a/module/Rest/test/Action/ShortCode/ListShortCodesActionTest.php +++ b/module/Rest/test/Action/ShortCode/ListShortCodesActionTest.php @@ -26,7 +26,10 @@ class ListShortCodesActionTest extends TestCase public function setUp() { $this->service = $this->prophesize(ShortUrlService::class); - $this->action = new ListShortCodesAction($this->service->reveal(), Translator::factory([])); + $this->action = new ListShortCodesAction($this->service->reveal(), Translator::factory([]), [ + 'hostname' => 'doma.in', + 'schema' => 'https', + ]); } /** diff --git a/module/Rest/test/Action/ShortCode/ResolveUrlActionTest.php b/module/Rest/test/Action/ShortCode/ResolveUrlActionTest.php index 3761604e..bde3e89a 100644 --- a/module/Rest/test/Action/ShortCode/ResolveUrlActionTest.php +++ b/module/Rest/test/Action/ShortCode/ResolveUrlActionTest.php @@ -5,6 +5,7 @@ namespace ShlinkioTest\Shlink\Rest\Action\ShortCode; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\UrlShortener; @@ -27,7 +28,7 @@ class ResolveUrlActionTest extends TestCase public function setUp() { $this->urlShortener = $this->prophesize(UrlShortener::class); - $this->action = new ResolveUrlAction($this->urlShortener->reveal(), Translator::factory([])); + $this->action = new ResolveUrlAction($this->urlShortener->reveal(), Translator::factory([]), []); } /** @@ -51,8 +52,9 @@ class ResolveUrlActionTest extends TestCase public function correctShortCodeReturnsSuccess() { $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode)->willReturn('http://domain.com/foo/bar') - ->shouldBeCalledTimes(1); + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn( + (new ShortUrl())->setLongUrl('http://domain.com/foo/bar') + )->shouldBeCalledTimes(1); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $response = $this->action->handle($request);