Allow providing device long URLs during short URL edition

This commit is contained in:
Alejandro Celaya 2023-01-14 15:44:12 +01:00
parent 1447687ebe
commit 822652cac3
6 changed files with 140 additions and 134 deletions

View file

@ -55,6 +55,9 @@ class ShortUrl extends AbstractEntity
{
}
/**
* @deprecated This should not be allowed
*/
public static function createEmpty(): self
{
return self::create(ShortUrlCreation::createEmpty());
@ -226,34 +229,34 @@ class ShortUrl extends AbstractEntity
?ShortUrlRelationResolverInterface $relationResolver = null,
): void {
if ($shortUrlEdit->validSinceWasProvided()) {
$this->validSince = $shortUrlEdit->validSince();
$this->validSince = $shortUrlEdit->validSince;
}
if ($shortUrlEdit->validUntilWasProvided()) {
$this->validUntil = $shortUrlEdit->validUntil();
$this->validUntil = $shortUrlEdit->validUntil;
}
if ($shortUrlEdit->maxVisitsWasProvided()) {
$this->maxVisits = $shortUrlEdit->maxVisits();
$this->maxVisits = $shortUrlEdit->maxVisits;
}
if ($shortUrlEdit->longUrlWasProvided()) {
$this->longUrl = $shortUrlEdit->longUrl() ?? $this->longUrl;
$this->longUrl = $shortUrlEdit->longUrl ?? $this->longUrl;
}
if ($shortUrlEdit->tagsWereProvided()) {
$relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
$this->tags = $relationResolver->resolveTags($shortUrlEdit->tags());
$this->tags = $relationResolver->resolveTags($shortUrlEdit->tags);
}
if ($shortUrlEdit->crawlableWasProvided()) {
$this->crawlable = $shortUrlEdit->crawlable();
$this->crawlable = $shortUrlEdit->crawlable;
}
if (
$this->title === null
|| $shortUrlEdit->titleWasProvided()
|| ($this->titleWasAutoResolved && $shortUrlEdit->titleWasAutoResolved())
) {
$this->title = $shortUrlEdit->title();
$this->title = $shortUrlEdit->title;
$this->titleWasAutoResolved = $shortUrlEdit->titleWasAutoResolved();
}
if ($shortUrlEdit->forwardQueryWasProvided()) {
$this->forwardQuery = $shortUrlEdit->forwardQuery();
$this->forwardQuery = $shortUrlEdit->forwardQuery;
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Model;
use Shlinkio\Shlink\Core\Model\DeviceType;
use function array_values;
use function Functional\map;
use function trim;
final class DeviceLongUrlPair
{
private function __construct(public readonly DeviceType $deviceType, public readonly string $longUrl)
{
}
public static function fromRawTypeAndLongUrl(string $type, string $longUrl): self
{
return new self(DeviceType::from($type), trim($longUrl));
}
/**
* @param array<string, string> $map
* @return self[]
*/
public static function fromMapToList(array $map): array
{
return array_values(map(
$map,
fn (string $longUrl, string $deviceType) => self::fromRawTypeAndLongUrl($deviceType, $longUrl),
));
}
}

View file

@ -6,17 +6,14 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Helper\TitleResolutionModelInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function Functional\map;
use function Shlinkio\Shlink\Core\getNonEmptyOptionalValueFromInputFilter;
use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter;
use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter;
use function Shlinkio\Shlink\Core\normalizeOptionalDate;
use function trim;
use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
@ -24,7 +21,7 @@ final class ShortUrlCreation implements TitleResolutionModelInterface
{
/**
* @param string[] $tags
* @param array{DeviceType, string}[] $deviceLongUrls
* @param DeviceLongUrlPair[] $deviceLongUrls
*/
private function __construct(
public readonly string $longUrl,
@ -46,6 +43,9 @@ final class ShortUrlCreation implements TitleResolutionModelInterface
) {
}
/**
* @deprecated This should not be allowed
*/
public static function createEmpty(): self
{
return new self('');
@ -63,9 +63,8 @@ final class ShortUrlCreation implements TitleResolutionModelInterface
return new self(
longUrl: $inputFilter->getValue(ShortUrlInputFilter::LONG_URL),
deviceLongUrls: map(
deviceLongUrls: DeviceLongUrlPair::fromMapToList(
$inputFilter->getValue(ShortUrlInputFilter::DEVICE_LONG_URLS) ?? [],
static fn (string $longUrl, string $deviceType) => [DeviceType::from($deviceType), trim($longUrl)],
),
validSince: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE)),
validUntil: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL)),
@ -89,22 +88,22 @@ final class ShortUrlCreation implements TitleResolutionModelInterface
public function withResolvedTitle(string $title): self
{
return new self(
$this->longUrl,
$this->deviceLongUrls,
$this->validSince,
$this->validUntil,
$this->customSlug,
$this->maxVisits,
$this->findIfExists,
$this->domain,
$this->shortCodeLength,
$this->validateUrl,
$this->apiKey,
$this->tags,
$title,
true,
$this->crawlable,
$this->forwardQuery,
longUrl: $this->longUrl,
deviceLongUrls: $this->deviceLongUrls,
validSince: $this->validSince,
validUntil: $this->validUntil,
customSlug: $this->customSlug,
maxVisits: $this->maxVisits,
findIfExists: $this->findIfExists,
domain: $this->domain,
shortCodeLength: $this->shortCodeLength,
validateUrl: $this->validateUrl,
apiKey: $this->apiKey,
tags: $this->tags,
title: $title,
titleWasAutoResolved: true,
crawlable: $this->crawlable,
forwardQuery: $this->forwardQuery,
);
}

View file

@ -16,77 +16,93 @@ use function Shlinkio\Shlink\Core\normalizeOptionalDate;
final class ShortUrlEdition implements TitleResolutionModelInterface
{
private bool $longUrlPropWasProvided = false;
private ?string $longUrl = null;
private bool $validSincePropWasProvided = false;
private ?Chronos $validSince = null;
private bool $validUntilPropWasProvided = false;
private ?Chronos $validUntil = null;
private bool $maxVisitsPropWasProvided = false;
private ?int $maxVisits = null;
private bool $tagsPropWasProvided = false;
private array $tags = [];
private bool $titlePropWasProvided = false;
private ?string $title = null;
private bool $titleWasAutoResolved = false;
private bool $validateUrl = false;
private bool $crawlablePropWasProvided = false;
private bool $crawlable = false;
private bool $forwardQueryPropWasProvided = false;
private bool $forwardQuery = true;
private function __construct()
{
/**
* @param string[] $tags
*/
private function __construct(
private readonly bool $longUrlPropWasProvided = false,
public readonly ?string $longUrl = null,
public readonly array $deviceLongUrls = [],
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,
public readonly bool $validateUrl = false,
private readonly bool $crawlablePropWasProvided = false,
public readonly bool $crawlable = false,
private readonly bool $forwardQueryPropWasProvided = false,
public readonly bool $forwardQuery = true,
) {
}
/**
* @throws ValidationException
*/
public static function fromRawData(array $data): self
{
$instance = new self();
$instance->validateAndInit($data);
return $instance;
}
/**
* @throws ValidationException
*/
private function validateAndInit(array $data): void
{
$inputFilter = ShortUrlInputFilter::withNonRequiredLongUrl($data);
if (! $inputFilter->isValid()) {
throw ValidationException::fromInputFilter($inputFilter);
}
$this->longUrlPropWasProvided = array_key_exists(ShortUrlInputFilter::LONG_URL, $data);
$this->validSincePropWasProvided = array_key_exists(ShortUrlInputFilter::VALID_SINCE, $data);
$this->validUntilPropWasProvided = array_key_exists(ShortUrlInputFilter::VALID_UNTIL, $data);
$this->maxVisitsPropWasProvided = array_key_exists(ShortUrlInputFilter::MAX_VISITS, $data);
$this->tagsPropWasProvided = array_key_exists(ShortUrlInputFilter::TAGS, $data);
$this->titlePropWasProvided = array_key_exists(ShortUrlInputFilter::TITLE, $data);
$this->crawlablePropWasProvided = array_key_exists(ShortUrlInputFilter::CRAWLABLE, $data);
$this->forwardQueryPropWasProvided = array_key_exists(ShortUrlInputFilter::FORWARD_QUERY, $data);
$this->longUrl = $inputFilter->getValue(ShortUrlInputFilter::LONG_URL);
$this->validSince = normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE));
$this->validUntil = normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL));
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS);
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL) ?? false;
$this->tags = $inputFilter->getValue(ShortUrlInputFilter::TAGS);
$this->title = $inputFilter->getValue(ShortUrlInputFilter::TITLE);
$this->crawlable = $inputFilter->getValue(ShortUrlInputFilter::CRAWLABLE);
$this->forwardQuery = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::FORWARD_QUERY) ?? true;
return new self(
longUrlPropWasProvided: array_key_exists(ShortUrlInputFilter::LONG_URL, $data),
longUrl: $inputFilter->getValue(ShortUrlInputFilter::LONG_URL),
deviceLongUrls: DeviceLongUrlPair::fromMapToList(
$inputFilter->getValue(ShortUrlInputFilter::DEVICE_LONG_URLS) ?? [],
),
validSincePropWasProvided: array_key_exists(ShortUrlInputFilter::VALID_SINCE, $data),
validSince: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE)),
validUntilPropWasProvided: array_key_exists(ShortUrlInputFilter::VALID_UNTIL, $data),
validUntil: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL)),
maxVisitsPropWasProvided: array_key_exists(ShortUrlInputFilter::MAX_VISITS, $data),
maxVisits: getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS),
tagsPropWasProvided: array_key_exists(ShortUrlInputFilter::TAGS, $data),
tags: $inputFilter->getValue(ShortUrlInputFilter::TAGS),
titlePropWasProvided: array_key_exists(ShortUrlInputFilter::TITLE, $data),
title: $inputFilter->getValue(ShortUrlInputFilter::TITLE),
validateUrl: getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL) ?? false,
crawlablePropWasProvided: array_key_exists(ShortUrlInputFilter::CRAWLABLE, $data),
crawlable: $inputFilter->getValue(ShortUrlInputFilter::CRAWLABLE),
forwardQueryPropWasProvided: array_key_exists(ShortUrlInputFilter::FORWARD_QUERY, $data),
forwardQuery: getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::FORWARD_QUERY) ?? true,
);
}
public function longUrl(): ?string
public function withResolvedTitle(string $title): self
{
return $this->longUrl;
return new self(
longUrlPropWasProvided: $this->longUrlPropWasProvided,
longUrl: $this->longUrl,
validSincePropWasProvided: $this->validSincePropWasProvided,
validSince: $this->validSince,
validUntilPropWasProvided: $this->validUntilPropWasProvided,
validUntil: $this->validUntil,
maxVisitsPropWasProvided: $this->maxVisitsPropWasProvided,
maxVisits: $this->maxVisits,
tagsPropWasProvided: $this->tagsPropWasProvided,
tags: $this->tags,
titlePropWasProvided: $this->titlePropWasProvided,
title: $title,
titleWasAutoResolved: true,
validateUrl: $this->validateUrl,
crawlablePropWasProvided: $this->crawlablePropWasProvided,
crawlable: $this->crawlable,
forwardQueryPropWasProvided: $this->forwardQueryPropWasProvided,
forwardQuery: $this->forwardQuery,
);
}
public function getLongUrl(): string
{
return $this->longUrl() ?? '';
return $this->longUrl ?? '';
}
public function longUrlWasProvided(): bool
@ -94,54 +110,26 @@ final class ShortUrlEdition implements TitleResolutionModelInterface
return $this->longUrlPropWasProvided && $this->longUrl !== null;
}
public function validSince(): ?Chronos
{
return $this->validSince;
}
public function validSinceWasProvided(): bool
{
return $this->validSincePropWasProvided;
}
public function validUntil(): ?Chronos
{
return $this->validUntil;
}
public function validUntilWasProvided(): bool
{
return $this->validUntilPropWasProvided;
}
public function maxVisits(): ?int
{
return $this->maxVisits;
}
public function maxVisitsWasProvided(): bool
{
return $this->maxVisitsPropWasProvided;
}
/**
* @return string[]
*/
public function tags(): array
{
return $this->tags;
}
public function tagsWereProvided(): bool
{
return $this->tagsPropWasProvided;
}
public function title(): ?string
{
return $this->title;
}
public function titleWasProvided(): bool
{
return $this->titlePropWasProvided;
@ -157,35 +145,16 @@ final class ShortUrlEdition implements TitleResolutionModelInterface
return $this->titleWasAutoResolved;
}
public function withResolvedTitle(string $title): self
{
$copy = clone $this;
$copy->title = $title;
$copy->titleWasAutoResolved = true;
return $copy;
}
public function doValidateUrl(): bool
{
return $this->validateUrl;
}
public function crawlable(): bool
{
return $this->crawlable;
}
public function crawlableWasProvided(): bool
{
return $this->crawlablePropWasProvided;
}
public function forwardQuery(): bool
{
return $this->forwardQuery;
}
public function forwardQueryWasProvided(): bool
{
return $this->forwardQueryPropWasProvided;

View file

@ -78,7 +78,7 @@ class ShortUrlInputFilter extends InputFilter
$this->add($longUrlInput);
$deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, false);
$deviceLongUrlsInput->getValidatorChain()->attach(
$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

View file

@ -73,10 +73,10 @@ class ShortUrlServiceTest extends TestCase
);
self::assertSame($shortUrl, $result);
self::assertEquals($shortUrlEdit->validSince(), $shortUrl->getValidSince());
self::assertEquals($shortUrlEdit->validUntil(), $shortUrl->getValidUntil());
self::assertEquals($shortUrlEdit->maxVisits(), $shortUrl->getMaxVisits());
self::assertEquals($shortUrlEdit->longUrl() ?? $originalLongUrl, $shortUrl->getLongUrl());
self::assertEquals($shortUrlEdit->validSince, $shortUrl->getValidSince());
self::assertEquals($shortUrlEdit->validUntil, $shortUrl->getValidUntil());
self::assertEquals($shortUrlEdit->maxVisits, $shortUrl->getMaxVisits());
self::assertEquals($shortUrlEdit->longUrl ?? $originalLongUrl, $shortUrl->getLongUrl());
}
public function provideShortUrlEdits(): iterable