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);