Merge pull request #179 from acelaya/feature/1.11.0

v1.11.0
This commit is contained in:
Alejandro Celaya 2018-08-13 16:40:37 +02:00 committed by GitHub
commit 987919e87a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 297 additions and 132 deletions

View file

@ -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

View file

@ -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",

View file

@ -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]"
}
}
}

View file

@ -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": []

View file

@ -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"
]
}
}
},

View file

@ -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,

View file

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

View file

@ -66,8 +66,10 @@ class ResolveUrlCommand extends Command
$shortCode = $input->getArgument('shortCode');
try {
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
$output->writeln(\sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $longUrl));
$url = $this->urlShortener->shortCodeToUrl($shortCode);
$output->writeln(
\sprintf('%s <info>%s</info>', $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)

View file

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

View file

@ -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([

View file

@ -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();
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Rest;
interface DataTransformerInterface
{
public function transform($value): array;
}

View file

@ -42,7 +42,6 @@ return [
Service\UrlShortener::class => [
'httpClient',
'em',
Cache::class,
'config.url_shortener.validate_url',
'config.url_shortener.shortcode_chars',
],

View file

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

View file

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

View file

@ -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 <b>json_encode</b>,
* 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(),
];
}
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Transformer;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Tag;
class ShortUrlDataTransformer implements DataTransformerInterface
{
/**
* @var array
*/
private $domainConfig;
public function __construct(array $domainConfig)
{
$this->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();
}
}

View file

@ -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());
}
}

View file

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

View file

@ -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(

View file

@ -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(

View file

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

View file

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

View file

@ -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',

View file

@ -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([

View file

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

View file

@ -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',
]);
}
/**

View file

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