Defined abstract action which handles short codes generations

This commit is contained in:
Alejandro Celaya 2018-05-01 19:35:12 +02:00
parent 2f5290b9d3
commit e5e1aa2ff4
6 changed files with 198 additions and 88 deletions

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Psr\Http\Message\UriInterface;
final class CreateShortCodeData
{
/**
* @var UriInterface
*/
private $longUrl;
/**
* @var array
*/
private $tags;
/**
* @var ShortUrlMeta
*/
private $meta;
public function __construct(
UriInterface $longUrl,
array $tags = [],
ShortUrlMeta $meta = null
) {
$this->longUrl = $longUrl;
$this->tags = $tags;
$this->meta = $meta ?? ShortUrlMeta::createFromParams();
}
/**
* @return UriInterface
*/
public function getLongUrl(): UriInterface
{
return $this->longUrl;
}
/**
* @return array
*/
public function getTags(): array
{
return $this->tags;
}
/**
* @return ShortUrlMeta
*/
public function getMeta(): ShortUrlMeta
{
return $this->meta;
}
}

View File

@ -71,7 +71,7 @@ final class ShortUrlMeta
* @param array $data
* @throws ValidationException
*/
private function validate(array $data)
private function validate(array $data): void
{
$inputFilter = new ShortUrlMetaInputFilter($data);
if (! $inputFilter->isValid()) {

View File

@ -10,12 +10,7 @@ return [
Action\AuthenticateAction::getRouteDef(),
// Short codes
[
'name' => Action\CreateShortcodeAction::class,
'path' => '/short-codes',
'middleware' => Action\CreateShortcodeAction::class,
'allowed_methods' => [RequestMethod::METHOD_POST],
],
Action\CreateShortcodeAction::getRouteDef(),
// [
// 'name' => Action\CreateShortcodeAction::class,
// 'path' => '/short-codes',

View File

@ -3,104 +3,43 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\CreateShortCodeData;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Rest\Action\ShortCode\AbstractCreateShortCodeAction;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface;
class CreateShortcodeAction extends AbstractRestAction
class CreateShortcodeAction extends AbstractCreateShortCodeAction
{
/**
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var array
*/
private $domainConfig;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
array $domainConfig,
LoggerInterface $logger = null
) {
parent::__construct($logger);
$this->urlShortener = $urlShortener;
$this->translator = $translator;
$this->domainConfig = $domainConfig;
}
protected const ROUTE_PATH = '/short-codes';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/**
* @param Request $request
* @return Response
* @return CreateShortCodeData
* @throws ValidationException
* @throws InvalidArgumentException
* @throws \InvalidArgumentException
*/
public function handle(Request $request): Response
protected function buildUrlToShortCodeData(Request $request): CreateShortCodeData
{
$postData = (array) $request->getParsedBody();
if (! isset($postData['longUrl'])) {
return new JsonResponse([
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
'message' => $this->translator->translate('A URL was not provided'),
], self::STATUS_BAD_REQUEST);
throw new InvalidArgumentException('A URL was not provided');
}
$longUrl = $postData['longUrl'];
$customSlug = $postData['customSlug'] ?? null;
try {
$shortCode = $this->urlShortener->urlToShortCode(
new Uri($longUrl),
(array) ($postData['tags'] ?? []),
return new CreateShortCodeData(
new Uri($postData['longUrl']),
(array) ($postData['tags'] ?? []),
ShortUrlMeta::createFromParams(
$this->getOptionalDate($postData, 'validSince'),
$this->getOptionalDate($postData, 'validUntil'),
$customSlug,
$postData['customSlug'] ?? null,
isset($postData['maxVisits']) ? (int) $postData['maxVisits'] : null
);
$shortUrl = (new Uri())->withPath($shortCode)
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
return new JsonResponse([
'longUrl' => $longUrl,
'shortUrl' => (string) $shortUrl,
'shortCode' => $shortCode,
]);
} catch (InvalidUrlException $e) {
$this->logger->warning('Provided Invalid URL.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => \sprintf(
$this->translator->translate('Provided URL %s is invalid. Try with a different one.'),
$longUrl
),
], self::STATUS_BAD_REQUEST);
} catch (NonUniqueSlugException $e) {
$this->logger->warning('Provided non-unique slug.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => \sprintf(
$this->translator->translate('Provided slug %s is already in use. Try with a different one.'),
$customSlug
),
], self::STATUS_BAD_REQUEST);
} catch (\Throwable $e) {
$this->logger->error('Unexpected error creating shortcode.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR,
'message' => $this->translator->translate('Unexpected error occurred'),
], self::STATUS_INTERNAL_SERVER_ERROR);
}
)
);
}
private function getOptionalDate(array $postData, string $fieldName)

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\CreateShortCodeData;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface;
abstract class AbstractCreateShortCodeAction extends AbstractRestAction
{
/**
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var array
*/
private $domainConfig;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
array $domainConfig,
LoggerInterface $logger = null
) {
parent::__construct($logger);
$this->urlShortener = $urlShortener;
$this->translator = $translator;
$this->domainConfig = $domainConfig;
}
/**
* @param Request $request
* @return Response
* @throws \InvalidArgumentException
*/
public function handle(Request $request): Response
{
try {
$shortCodeData = $this->buildUrlToShortCodeData($request);
$shortCodeMeta = $shortCodeData->getMeta();
$longUrl = $shortCodeData->getLongUrl();
$customSlug = $shortCodeMeta->getCustomSlug();
$shortCode = $this->urlShortener->urlToShortCode(
$longUrl,
$shortCodeData->getTags(),
$shortCodeMeta->getValidSince(),
$shortCodeMeta->getValidUntil(),
$customSlug,
$shortCodeMeta->getMaxVisits()
);
$shortUrl = (new Uri())->withPath($shortCode)
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
// TODO Make response to be generated based on Accept header
return new JsonResponse([
'longUrl' => (string) $longUrl,
'shortUrl' => (string) $shortUrl,
'shortCode' => $shortCode,
]);
} catch (InvalidUrlException $e) {
$this->logger->warning('Provided Invalid URL.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => \sprintf(
$this->translator->translate('Provided URL %s is invalid. Try with a different one.'),
$longUrl
),
], self::STATUS_BAD_REQUEST);
} catch (NonUniqueSlugException $e) {
$this->logger->warning('Provided non-unique slug.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => \sprintf(
$this->translator->translate('Provided slug %s is already in use. Try with a different one.'),
$customSlug
),
], self::STATUS_BAD_REQUEST);
} catch (ValidationException | InvalidArgumentException $e) {
$this->logger->warning('Provided data is invalid.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
'message' => $this->translator->translate('Provided data is invalid'),
], self::STATUS_BAD_REQUEST);
} catch (\Throwable $e) {
$this->logger->error('Unexpected error creating shortcode.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR,
'message' => $this->translator->translate('Unexpected error occurred'),
], self::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* @param Request $request
* @return CreateShortCodeData
* @throws ValidationException
* @throws InvalidArgumentException
*/
abstract protected function buildUrlToShortCodeData(Request $request): CreateShortCodeData;
}

View File

@ -30,6 +30,7 @@ class RestUtils
case $e instanceof Core\NonUniqueSlugException:
return self::INVALID_SLUG_ERROR;
case $e instanceof Common\InvalidArgumentException:
case $e instanceof Core\InvalidArgumentException:
case $e instanceof Core\ValidationException:
return self::INVALID_ARGUMENT_ERROR;
case $e instanceof Rest\AuthenticationException: