mirror of
https://github.com/shlinkio/shlink.git
synced 2024-12-22 15:13:59 -06:00
Refactor ShortUrlInputFilter for creation and edition
This commit is contained in:
parent
50cc7ae632
commit
0e78deb8f2
@ -18,39 +18,39 @@ use function Shlinkio\Shlink\Core\normalizeOptionalDate;
|
||||
|
||||
use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
|
||||
|
||||
final class ShortUrlCreation implements TitleResolutionModelInterface
|
||||
final readonly class ShortUrlCreation implements TitleResolutionModelInterface
|
||||
{
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @param DeviceLongUrlPair[] $deviceLongUrls
|
||||
*/
|
||||
private function __construct(
|
||||
public readonly string $longUrl,
|
||||
public readonly ShortUrlMode $shortUrlMode,
|
||||
public readonly array $deviceLongUrls = [],
|
||||
public readonly ?Chronos $validSince = null,
|
||||
public readonly ?Chronos $validUntil = null,
|
||||
public readonly ?string $customSlug = null,
|
||||
public readonly ?int $maxVisits = null,
|
||||
public readonly bool $findIfExists = false,
|
||||
public readonly ?string $domain = null,
|
||||
public readonly int $shortCodeLength = 5,
|
||||
public readonly ?ApiKey $apiKey = null,
|
||||
public readonly array $tags = [],
|
||||
public readonly ?string $title = null,
|
||||
public readonly bool $titleWasAutoResolved = false,
|
||||
public readonly bool $crawlable = false,
|
||||
public readonly bool $forwardQuery = true,
|
||||
public string $longUrl,
|
||||
public ShortUrlMode $shortUrlMode,
|
||||
public array $deviceLongUrls = [],
|
||||
public ?Chronos $validSince = null,
|
||||
public ?Chronos $validUntil = null,
|
||||
public ?string $customSlug = null,
|
||||
public ?string $pathPrefix = null,
|
||||
public ?int $maxVisits = null,
|
||||
public bool $findIfExists = false,
|
||||
public ?string $domain = null,
|
||||
public int $shortCodeLength = 5,
|
||||
public ?ApiKey $apiKey = null,
|
||||
public array $tags = [],
|
||||
public ?string $title = null,
|
||||
public bool $titleWasAutoResolved = false,
|
||||
public bool $crawlable = false,
|
||||
public bool $forwardQuery = true,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public static function fromRawData(array $data, ?UrlShortenerOptions $options = null): self
|
||||
public static function fromRawData(array $data, UrlShortenerOptions $options = new UrlShortenerOptions()): self
|
||||
{
|
||||
$options = $options ?? new UrlShortenerOptions();
|
||||
$inputFilter = ShortUrlInputFilter::withRequiredLongUrl($data, $options);
|
||||
$inputFilter = ShortUrlInputFilter::forCreation($data, $options);
|
||||
if (! $inputFilter->isValid()) {
|
||||
throw ValidationException::fromInputFilter($inputFilter);
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter;
|
||||
use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter;
|
||||
use function Shlinkio\Shlink\Core\normalizeOptionalDate;
|
||||
|
||||
final class ShortUrlEdition implements TitleResolutionModelInterface
|
||||
final readonly class ShortUrlEdition implements TitleResolutionModelInterface
|
||||
{
|
||||
/**
|
||||
* @param string[] $tags
|
||||
@ -23,25 +23,25 @@ final class ShortUrlEdition implements TitleResolutionModelInterface
|
||||
* @param DeviceType[] $devicesToRemove
|
||||
*/
|
||||
private function __construct(
|
||||
private readonly bool $longUrlPropWasProvided = false,
|
||||
public readonly ?string $longUrl = null,
|
||||
public readonly array $deviceLongUrls = [],
|
||||
public readonly array $devicesToRemove = [],
|
||||
private readonly bool $validSincePropWasProvided = false,
|
||||
public readonly ?Chronos $validSince = null,
|
||||
private readonly bool $validUntilPropWasProvided = false,
|
||||
public readonly ?Chronos $validUntil = null,
|
||||
private readonly bool $maxVisitsPropWasProvided = false,
|
||||
public readonly ?int $maxVisits = null,
|
||||
private readonly bool $tagsPropWasProvided = false,
|
||||
public readonly array $tags = [],
|
||||
private readonly bool $titlePropWasProvided = false,
|
||||
public readonly ?string $title = null,
|
||||
public readonly bool $titleWasAutoResolved = false,
|
||||
private readonly bool $crawlablePropWasProvided = false,
|
||||
public readonly bool $crawlable = false,
|
||||
private readonly bool $forwardQueryPropWasProvided = false,
|
||||
public readonly bool $forwardQuery = true,
|
||||
private bool $longUrlPropWasProvided = false,
|
||||
public ?string $longUrl = null,
|
||||
public array $deviceLongUrls = [],
|
||||
public array $devicesToRemove = [],
|
||||
private bool $validSincePropWasProvided = false,
|
||||
public ?Chronos $validSince = null,
|
||||
private bool $validUntilPropWasProvided = false,
|
||||
public ?Chronos $validUntil = null,
|
||||
private bool $maxVisitsPropWasProvided = false,
|
||||
public ?int $maxVisits = null,
|
||||
private bool $tagsPropWasProvided = false,
|
||||
public array $tags = [],
|
||||
private bool $titlePropWasProvided = false,
|
||||
public ?string $title = null,
|
||||
public bool $titleWasAutoResolved = false,
|
||||
private bool $crawlablePropWasProvided = false,
|
||||
public bool $crawlable = false,
|
||||
private bool $forwardQueryPropWasProvided = false,
|
||||
public bool $forwardQuery = true,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ final class ShortUrlEdition implements TitleResolutionModelInterface
|
||||
*/
|
||||
public static function fromRawData(array $data): self
|
||||
{
|
||||
$inputFilter = ShortUrlInputFilter::withNonRequiredLongUrl($data);
|
||||
$inputFilter = ShortUrlInputFilter::forEdition($data);
|
||||
if (! $inputFilter->isValid()) {
|
||||
throw ValidationException::fromInputFilter($inputFilter);
|
||||
}
|
||||
|
@ -12,9 +12,9 @@ use function str_replace;
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
|
||||
class CustomSlugFilter implements FilterInterface
|
||||
readonly class CustomSlugFilter implements FilterInterface
|
||||
{
|
||||
public function __construct(private readonly UrlShortenerOptions $options)
|
||||
public function __construct(private UrlShortenerOptions $options)
|
||||
{
|
||||
}
|
||||
|
||||
@ -25,9 +25,8 @@ class CustomSlugFilter implements FilterInterface
|
||||
}
|
||||
|
||||
$value = $this->options->isLooseMode() ? strtolower($value) : $value;
|
||||
return (match ($this->options->multiSegmentSlugsEnabled) {
|
||||
true => trim(str_replace(' ', '-', $value), '/'),
|
||||
false => str_replace([' ', '/'], '-', $value),
|
||||
});
|
||||
return $this->options->multiSegmentSlugsEnabled
|
||||
? trim(str_replace(' ', '-', $value), '/')
|
||||
: str_replace([' ', '/'], '-', $value);
|
||||
}
|
||||
}
|
||||
|
@ -19,68 +19,52 @@ use function substr;
|
||||
use const Shlinkio\Shlink\LOOSE_URI_MATCHER;
|
||||
use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
|
||||
|
||||
/**
|
||||
* @todo Pass forCreation/forEdition, instead of withRequiredLongUrl/withNonRequiredLongUrl.
|
||||
* Make it also dynamically add the relevant fields
|
||||
*/
|
||||
class ShortUrlInputFilter extends InputFilter
|
||||
{
|
||||
use Validation\InputFactoryTrait;
|
||||
|
||||
public const VALID_SINCE = 'validSince';
|
||||
public const VALID_UNTIL = 'validUntil';
|
||||
// Fields for creation only
|
||||
public const SHORT_CODE_LENGTH = 'shortCodeLength';
|
||||
public const CUSTOM_SLUG = 'customSlug';
|
||||
public const MAX_VISITS = 'maxVisits';
|
||||
public const PATH_PREFIX = 'pathPrefix';
|
||||
public const FIND_IF_EXISTS = 'findIfExists';
|
||||
public const DOMAIN = 'domain';
|
||||
public const SHORT_CODE_LENGTH = 'shortCodeLength';
|
||||
|
||||
// Fields for creation and edition
|
||||
public const LONG_URL = 'longUrl';
|
||||
public const DEVICE_LONG_URLS = 'deviceLongUrls';
|
||||
public const API_KEY = 'apiKey';
|
||||
public const TAGS = 'tags';
|
||||
public const VALID_SINCE = 'validSince';
|
||||
public const VALID_UNTIL = 'validUntil';
|
||||
public const MAX_VISITS = 'maxVisits';
|
||||
public const TITLE = 'title';
|
||||
public const TAGS = 'tags';
|
||||
public const CRAWLABLE = 'crawlable';
|
||||
public const FORWARD_QUERY = 'forwardQuery';
|
||||
public const API_KEY = 'apiKey';
|
||||
|
||||
private function __construct(array $data, bool $requireLongUrl, UrlShortenerOptions $options)
|
||||
public static function forCreation(array $data, UrlShortenerOptions $options): self
|
||||
{
|
||||
$this->initialize($requireLongUrl, $options);
|
||||
$this->setData($data);
|
||||
$instance = new self();
|
||||
$instance->initializeForCreation($options);
|
||||
$instance->setData($data);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public static function withRequiredLongUrl(array $data, UrlShortenerOptions $options): self
|
||||
public static function forEdition(array $data): self
|
||||
{
|
||||
return new self($data, true, $options);
|
||||
$instance = new self();
|
||||
$instance->initializeForEdition();
|
||||
$instance->setData($data);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public static function withNonRequiredLongUrl(array $data): self
|
||||
private function initializeForCreation(UrlShortenerOptions $options): void
|
||||
{
|
||||
return new self($data, false, new UrlShortenerOptions());
|
||||
}
|
||||
|
||||
private function initialize(bool $requireLongUrl, UrlShortenerOptions $options): void
|
||||
{
|
||||
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
|
||||
$longUrlInput->getValidatorChain()->merge($this->longUrlValidators());
|
||||
$this->add($longUrlInput);
|
||||
|
||||
$deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, false);
|
||||
$deviceLongUrlsInput->getValidatorChain()->attach(
|
||||
new DeviceLongUrlsValidator($this->longUrlValidators(allowNull: ! $requireLongUrl)),
|
||||
);
|
||||
$this->add($deviceLongUrlsInput);
|
||||
|
||||
$validSince = $this->createInput(self::VALID_SINCE, false);
|
||||
$validSince->getValidatorChain()->attach(new Validator\Date(['format' => DateTimeInterface::ATOM]));
|
||||
$this->add($validSince);
|
||||
|
||||
$validUntil = $this->createInput(self::VALID_UNTIL, false);
|
||||
$validUntil->getValidatorChain()->attach(new Validator\Date(['format' => DateTimeInterface::ATOM]));
|
||||
$this->add($validUntil);
|
||||
|
||||
// The only way to enforce the NotEmpty validator to be evaluated when the key is present with an empty value
|
||||
// is with setContinueIfEmpty
|
||||
$customSlug = $this->createInput(self::CUSTOM_SLUG, false)->setContinueIfEmpty(true);
|
||||
// is with setContinueIfEmpty(true)
|
||||
$customSlug = $this->createInput(self::CUSTOM_SLUG, required: false)->setContinueIfEmpty(true);
|
||||
$customSlug->getFilterChain()->attach(new CustomSlugFilter($options));
|
||||
$customSlug->getValidatorChain()
|
||||
->attach(new Validator\NotEmpty([
|
||||
@ -90,32 +74,62 @@ class ShortUrlInputFilter extends InputFilter
|
||||
->attach(CustomSlugValidator::forUrlShortenerOptions($options));
|
||||
$this->add($customSlug);
|
||||
|
||||
$this->add($this->createNumericInput(self::MAX_VISITS, false));
|
||||
$this->add($this->createNumericInput(self::SHORT_CODE_LENGTH, false, MIN_SHORT_CODES_LENGTH));
|
||||
// The path prefix is subject to the same filtering and validation logic as the custom slug, which takes into
|
||||
// consideration if multi-segment slugs are enabled or not.
|
||||
// The only difference is that empty values are allowed here.
|
||||
$pathPrefix = $this->createInput(self::PATH_PREFIX, required: false);
|
||||
$pathPrefix->getFilterChain()->attach(new CustomSlugFilter($options));
|
||||
$pathPrefix->getValidatorChain()->attach(CustomSlugValidator::forUrlShortenerOptions($options));
|
||||
$this->add($pathPrefix);
|
||||
|
||||
$this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false));
|
||||
$this->add($this->createNumericInput(self::SHORT_CODE_LENGTH, required: false, min: MIN_SHORT_CODES_LENGTH));
|
||||
$this->add($this->createBooleanInput(self::FIND_IF_EXISTS, required: false));
|
||||
|
||||
// This cannot be defined as a boolean inputs, because they can actually have 3 values: true, false and null.
|
||||
// Defining them as boolean will make null fall back to false, which is not the desired behavior.
|
||||
$this->add($this->createInput(self::FORWARD_QUERY, false));
|
||||
|
||||
$domain = $this->createInput(self::DOMAIN, false);
|
||||
$domain = $this->createInput(self::DOMAIN, required: false);
|
||||
$domain->getValidatorChain()->attach(new Validation\HostAndPortValidator());
|
||||
$this->add($domain);
|
||||
|
||||
$apiKeyInput = $this->createInput(self::API_KEY, false);
|
||||
$apiKeyInput->getValidatorChain()->attach(new Validator\IsInstanceOf(['className' => ApiKey::class]));
|
||||
$this->add($apiKeyInput);
|
||||
$this->initializeForEdition(requireLongUrl: true);
|
||||
}
|
||||
|
||||
$this->add($this->createTagsInput(self::TAGS, false));
|
||||
private function initializeForEdition(bool $requireLongUrl = false): void
|
||||
{
|
||||
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
|
||||
$longUrlInput->getValidatorChain()->merge($this->longUrlValidators());
|
||||
$this->add($longUrlInput);
|
||||
|
||||
$title = $this->createInput(self::TITLE, false);
|
||||
$deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, required: false);
|
||||
$deviceLongUrlsInput->getValidatorChain()->attach(
|
||||
new DeviceLongUrlsValidator($this->longUrlValidators(allowNull: ! $requireLongUrl)),
|
||||
);
|
||||
$this->add($deviceLongUrlsInput);
|
||||
|
||||
$validSince = $this->createInput(self::VALID_SINCE, required: false);
|
||||
$validSince->getValidatorChain()->attach(new Validator\Date(['format' => DateTimeInterface::ATOM]));
|
||||
$this->add($validSince);
|
||||
|
||||
$validUntil = $this->createInput(self::VALID_UNTIL, required: false);
|
||||
$validUntil->getValidatorChain()->attach(new Validator\Date(['format' => DateTimeInterface::ATOM]));
|
||||
$this->add($validUntil);
|
||||
|
||||
$this->add($this->createNumericInput(self::MAX_VISITS, required: false));
|
||||
|
||||
$title = $this->createInput(self::TITLE, required: false);
|
||||
$title->getFilterChain()->attach(new Filter\Callback(
|
||||
static fn (?string $value) => $value === null ? $value : substr($value, 0, 512),
|
||||
));
|
||||
$this->add($title);
|
||||
|
||||
$this->add($this->createBooleanInput(self::CRAWLABLE, false));
|
||||
$this->add($this->createTagsInput(self::TAGS, required: false));
|
||||
$this->add($this->createBooleanInput(self::CRAWLABLE, required: false));
|
||||
|
||||
// This cannot be defined as a boolean inputs, because it can actually have 3 values: true, false and null.
|
||||
// Defining them as boolean will make null fall back to false, which is not the desired behavior.
|
||||
$this->add($this->createInput(self::FORWARD_QUERY, required: false));
|
||||
|
||||
$apiKeyInput = $this->createInput(self::API_KEY, required: false);
|
||||
$apiKeyInput->getValidatorChain()->attach(new Validator\IsInstanceOf(['className' => ApiKey::class]));
|
||||
$this->add($apiKeyInput);
|
||||
}
|
||||
|
||||
private function longUrlValidators(bool $allowNull = false): Validator\ValidatorChain
|
||||
|
Loading…
Reference in New Issue
Block a user