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\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Model\DeviceLongUrlPair;
class DeviceLongUrl extends AbstractEntity
{
public function __construct(
public readonly ShortUrl $shortUrl,
private ShortUrl $shortUrl; // @phpstan-ignore-line
private function __construct(
public readonly DeviceType $deviceType,
private string $longUrl,
) {
}
public static function fromPair(DeviceLongUrlPair $pair): self
{
return new self($pair->deviceType, $pair->longUrl);
}
public function longUrl(): string
{
return $this->longUrl;

View File

@ -4,14 +4,21 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Helper;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
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
{
if ($data->hasTitle()) {

View File

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

View File

@ -12,5 +12,5 @@ interface TitleResolutionModelInterface
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(
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(
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 Shlinkio\Shlink\Common\Validation;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Model\DeviceType;
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 Shlinkio\Shlink\Core\enumValues;
use function str_replace;
use function substr;
use function trim;
@ -64,37 +57,20 @@ class ShortUrlInputFilter extends InputFilter
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::SPACE,
Validator\NotEmpty::NULL,
Validator\NotEmpty::EMPTY_ARRAY,
Validator\NotEmpty::BOOLEAN,
Validator\NotEmpty::STRING,
]);
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
$longUrlInput->getValidatorChain()->attach($notEmptyValidator);
]));
$this->add($longUrlInput);
$deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, false);
$deviceLongUrlsInput->getValidatorChain()->attach( // TODO Extract callback to own validator
new Validator\Callback(function (mixed $value) use ($notEmptyValidator): bool {
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(...));
}),
$deviceLongUrlsInput->getValidatorChain()->attach(
new DeviceLongUrlsValidator($longUrlInput->getValidatorChain()),
);
$this->add($deviceLongUrlsInput);

View File

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

View File

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

View File

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

View File

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

View File

@ -31,22 +31,21 @@ class UrlShortener implements UrlShortenerInterface
* @throws NonUniqueSlugException
* @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
$existingShortUrl = $this->findExistingShortUrlIfExists($meta);
$existingShortUrl = $this->findExistingShortUrlIfExists($creation);
if ($existingShortUrl !== null) {
return $existingShortUrl;
}
/** @var \Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation $meta */
$meta = $this->titleResolutionHelper->processTitleAndValidateUrl($meta);
$creation = $this->titleResolutionHelper->processTitleAndValidateUrl($creation);
/** @var ShortUrl $newShortUrl */
$newShortUrl = $this->em->wrapInTransaction(function () use ($meta) {
$shortUrl = ShortUrl::create($meta, $this->relationResolver);
$newShortUrl = $this->em->wrapInTransaction(function () use ($creation): ShortUrl {
$shortUrl = ShortUrl::create($creation, $this->relationResolver);
$this->verifyShortCodeUniqueness($meta, $shortUrl);
$this->verifyShortCodeUniqueness($creation, $shortUrl);
$this->em->persist($shortUrl);
return $shortUrl;

View File

@ -15,5 +15,5 @@ interface UrlShortenerInterface
* @throws NonUniqueSlugException
* @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 ?Chronos $expirationDate = null;
private bool $enabled;
/** @var Collection|ApiKeyRole[] */
/** @var Collection<string, ApiKeyRole> */
private Collection $roles;
private ?string $name = null;