Extract device long URL validation to its own validation class

This commit is contained in:
Alejandro Celaya 2023-01-14 16:50:42 +01:00
parent 822652cac3
commit 3e26f1113d
15 changed files with 96 additions and 48 deletions

View File

@ -6,16 +6,23 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Entity;
use Shlinkio\Shlink\Common\Entity\AbstractEntity; use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Core\Model\DeviceType; use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Model\DeviceLongUrlPair;
class DeviceLongUrl extends AbstractEntity class DeviceLongUrl extends AbstractEntity
{ {
public function __construct( private ShortUrl $shortUrl; // @phpstan-ignore-line
public readonly ShortUrl $shortUrl,
private function __construct(
public readonly DeviceType $deviceType, public readonly DeviceType $deviceType,
private string $longUrl, private string $longUrl,
) { ) {
} }
public static function fromPair(DeviceLongUrlPair $pair): self
{
return new self($pair->deviceType, $pair->longUrl);
}
public function longUrl(): string public function longUrl(): string
{ {
return $this->longUrl; return $this->longUrl;

View File

@ -4,14 +4,21 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Helper; namespace Shlinkio\Shlink\Core\ShortUrl\Helper;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface; use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
class ShortUrlTitleResolutionHelper implements ShortUrlTitleResolutionHelperInterface class ShortUrlTitleResolutionHelper implements ShortUrlTitleResolutionHelperInterface
{ {
public function __construct(private UrlValidatorInterface $urlValidator) public function __construct(private readonly UrlValidatorInterface $urlValidator)
{ {
} }
/**
* @template T of TitleResolutionModelInterface
* @param T $data
* @return T
* @throws InvalidUrlException
*/
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface
{ {
if ($data->hasTitle()) { if ($data->hasTitle()) {

View File

@ -9,6 +9,9 @@ use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
interface ShortUrlTitleResolutionHelperInterface interface ShortUrlTitleResolutionHelperInterface
{ {
/** /**
* @template T of TitleResolutionModelInterface
* @param T $data
* @return T
* @throws InvalidUrlException * @throws InvalidUrlException
*/ */
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface; public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface;

View File

@ -12,5 +12,5 @@ interface TitleResolutionModelInterface
public function doValidateUrl(): bool; public function doValidateUrl(): bool;
public function withResolvedTitle(string $title): self; public function withResolvedTitle(string $title): static;
} }

View File

@ -85,7 +85,7 @@ final class ShortUrlCreation implements TitleResolutionModelInterface
); );
} }
public function withResolvedTitle(string $title): self public function withResolvedTitle(string $title): static
{ {
return new self( return new self(
longUrl: $this->longUrl, longUrl: $this->longUrl,

View File

@ -76,7 +76,7 @@ final class ShortUrlEdition implements TitleResolutionModelInterface
); );
} }
public function withResolvedTitle(string $title): self public function withResolvedTitle(string $title): static
{ {
return new self( return new self(
longUrlPropWasProvided: $this->longUrlPropWasProvided, longUrlPropWasProvided: $this->longUrlPropWasProvided,

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Model\Validation;
use Laminas\Validator\AbstractValidator;
use Laminas\Validator\ValidatorChain;
use Shlinkio\Shlink\Core\Model\DeviceType;
use function array_keys;
use function array_values;
use function Functional\contains;
use function Functional\every;
use function is_array;
use function Shlinkio\Shlink\Core\enumValues;
class DeviceLongUrlsValidator extends AbstractValidator
{
private const NOT_ARRAY = 'NOT_ARRAY';
private const INVALID_DEVICE = 'INVALID_DEVICE';
private const INVALID_LONG_URL = 'INVALID_LONG_URL';
protected array $messageTemplates = [
self::NOT_ARRAY => 'Provided value is not an array.',
self::INVALID_DEVICE => 'You have provided at least one invalid device identifier.',
self::INVALID_LONG_URL => 'At least one of the long URLs are invalid.',
];
public function __construct(private readonly ValidatorChain $longUrlValidators)
{
parent::__construct();
}
public function isValid(mixed $value): bool
{
if (! is_array($value)) {
$this->error(self::NOT_ARRAY);
return false;
}
$validValues = enumValues(DeviceType::class);
$keys = array_keys($value);
if (! every($keys, static fn ($key) => contains($validValues, $key))) {
$this->error(self::INVALID_DEVICE);
return false;
}
$longUrls = array_values($value);
$result = every($longUrls, $this->longUrlValidators->isValid(...));
if (! $result) {
$this->error(self::INVALID_LONG_URL);
}
return $result;
}
}

View File

@ -10,16 +10,9 @@ use Laminas\InputFilter\InputFilter;
use Laminas\Validator; use Laminas\Validator;
use Shlinkio\Shlink\Common\Validation; use Shlinkio\Shlink\Common\Validation;
use Shlinkio\Shlink\Core\Config\EnvVars; use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function array_keys;
use function array_values;
use function Functional\contains;
use function Functional\every;
use function is_array;
use function is_string; use function is_string;
use function Shlinkio\Shlink\Core\enumValues;
use function str_replace; use function str_replace;
use function substr; use function substr;
use function trim; use function trim;
@ -64,37 +57,20 @@ class ShortUrlInputFilter extends InputFilter
private function initialize(bool $requireLongUrl, bool $multiSegmentEnabled): void private function initialize(bool $requireLongUrl, bool $multiSegmentEnabled): void
{ {
$notEmptyValidator = new Validator\NotEmpty([ $longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
$longUrlInput->getValidatorChain()->attach(new Validator\NotEmpty([
Validator\NotEmpty::OBJECT, Validator\NotEmpty::OBJECT,
Validator\NotEmpty::SPACE, Validator\NotEmpty::SPACE,
Validator\NotEmpty::NULL, Validator\NotEmpty::NULL,
Validator\NotEmpty::EMPTY_ARRAY, Validator\NotEmpty::EMPTY_ARRAY,
Validator\NotEmpty::BOOLEAN, Validator\NotEmpty::BOOLEAN,
Validator\NotEmpty::STRING, Validator\NotEmpty::STRING,
]); ]));
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
$longUrlInput->getValidatorChain()->attach($notEmptyValidator);
$this->add($longUrlInput); $this->add($longUrlInput);
$deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, false); $deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, false);
$deviceLongUrlsInput->getValidatorChain()->attach( // TODO Extract callback to own validator $deviceLongUrlsInput->getValidatorChain()->attach(
new Validator\Callback(function (mixed $value) use ($notEmptyValidator): bool { new DeviceLongUrlsValidator($longUrlInput->getValidatorChain()),
if (! is_array($value)) {
// TODO Set proper error: Not array
return false;
}
$validValues = enumValues(DeviceType::class);
$keys = array_keys($value);
if (! every($keys, static fn ($key) => contains($validValues, $key))) {
// TODO Set proper error: Provided invalid device type
return false;
}
$longUrls = array_values($value);
return every($longUrls, $notEmptyValidator->isValid(...));
}),
); );
$this->add($deviceLongUrlsInput); $this->add($deviceLongUrlsInput);

View File

@ -49,7 +49,7 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
/** /**
* @param string[] $tags * @param string[] $tags
* @return Collection|Tag[] * @return Collection<int, Tag>
*/ */
public function resolveTags(array $tags): Collections\Collection public function resolveTags(array $tags): Collections\Collection
{ {

View File

@ -14,7 +14,7 @@ interface ShortUrlRelationResolverInterface
/** /**
* @param string[] $tags * @param string[] $tags
* @return Collection|Tag[] * @return Collection<int, Tag>
*/ */
public function resolveTags(array $tags): Collection; public function resolveTags(array $tags): Collection;
} }

View File

@ -20,7 +20,7 @@ class SimpleShortUrlRelationResolver implements ShortUrlRelationResolverInterfac
/** /**
* @param string[] $tags * @param string[] $tags
* @return Collection|Tag[] * @return Collection<int, Tag>
*/ */
public function resolveTags(array $tags): Collections\Collection public function resolveTags(array $tags): Collections\Collection
{ {

View File

@ -34,7 +34,6 @@ class ShortUrlService implements ShortUrlServiceInterface
?ApiKey $apiKey = null, ?ApiKey $apiKey = null,
): ShortUrl { ): ShortUrl {
if ($shortUrlEdit->longUrlWasProvided()) { if ($shortUrlEdit->longUrlWasProvided()) {
/** @var ShortUrlEdition $shortUrlEdit */
$shortUrlEdit = $this->titleResolutionHelper->processTitleAndValidateUrl($shortUrlEdit); $shortUrlEdit = $this->titleResolutionHelper->processTitleAndValidateUrl($shortUrlEdit);
} }

View File

@ -31,22 +31,21 @@ class UrlShortener implements UrlShortenerInterface
* @throws NonUniqueSlugException * @throws NonUniqueSlugException
* @throws InvalidUrlException * @throws InvalidUrlException
*/ */
public function shorten(ShortUrlCreation $meta): ShortUrl public function shorten(ShortUrlCreation $creation): ShortUrl
{ {
// First, check if a short URL exists for all provided params // First, check if a short URL exists for all provided params
$existingShortUrl = $this->findExistingShortUrlIfExists($meta); $existingShortUrl = $this->findExistingShortUrlIfExists($creation);
if ($existingShortUrl !== null) { if ($existingShortUrl !== null) {
return $existingShortUrl; return $existingShortUrl;
} }
/** @var \Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation $meta */ $creation = $this->titleResolutionHelper->processTitleAndValidateUrl($creation);
$meta = $this->titleResolutionHelper->processTitleAndValidateUrl($meta);
/** @var ShortUrl $newShortUrl */ /** @var ShortUrl $newShortUrl */
$newShortUrl = $this->em->wrapInTransaction(function () use ($meta) { $newShortUrl = $this->em->wrapInTransaction(function () use ($creation): ShortUrl {
$shortUrl = ShortUrl::create($meta, $this->relationResolver); $shortUrl = ShortUrl::create($creation, $this->relationResolver);
$this->verifyShortCodeUniqueness($meta, $shortUrl); $this->verifyShortCodeUniqueness($creation, $shortUrl);
$this->em->persist($shortUrl); $this->em->persist($shortUrl);
return $shortUrl; return $shortUrl;

View File

@ -15,5 +15,5 @@ interface UrlShortenerInterface
* @throws NonUniqueSlugException * @throws NonUniqueSlugException
* @throws InvalidUrlException * @throws InvalidUrlException
*/ */
public function shorten(ShortUrlCreation $meta): ShortUrl; public function shorten(ShortUrlCreation $creation): ShortUrl;
} }

View File

@ -21,7 +21,7 @@ class ApiKey extends AbstractEntity
private string $key; private string $key;
private ?Chronos $expirationDate = null; private ?Chronos $expirationDate = null;
private bool $enabled; private bool $enabled;
/** @var Collection|ApiKeyRole[] */ /** @var Collection<string, ApiKeyRole> */
private Collection $roles; private Collection $roles;
private ?string $name = null; private ?string $name = null;