Remove requirement of checker props in ShortUrlEdition

This commit is contained in:
Alejandro Celaya
2026-05-10 12:59:57 +02:00
parent 2482dabdc3
commit b7db56e43d
5 changed files with 66 additions and 32 deletions
@@ -57,9 +57,6 @@ final class ShortUrlDataInput
return [
...array_filter(get_object_vars($this), static fn (mixed $value) => $value !== null),
'longUrl' => $longUrl,
'validSinceWasProvided' => $this->validSince !== null,
'validUntilWasProvided' => $this->validUntil !== null,
'maxVisitsWasProvided' => $this->maxVisits !== null,
'titleWasProvided' => $this->title !== null,
];
}
@@ -10,37 +10,46 @@ use Shlinkio\Shlink\Common\ObjectMapper\LooseUriConverter;
use Shlinkio\Shlink\Common\ObjectMapper\SubstringConverter;
use Shlinkio\Shlink\Common\ObjectMapper\TagsConverter;
use Shlinkio\Shlink\Core\ShortUrl\Helper\TitleResolutionModelInterface;
use Shlinkio\Shlink\Core\Util\NoValue;
use function Shlinkio\Shlink\Common\normalizeOptionalDate;
final class ShortUrlEdition implements TitleResolutionModelInterface
final readonly class ShortUrlEdition implements TitleResolutionModelInterface
{
public Chronos|null $validSince;
public bool $validSinceWasProvided;
public Chronos|null $validUntil;
public bool $validUntilWasProvided;
public int|null $maxVisits;
public bool $maxVisitsWasProvided;
/**
* @param positive-int|NoValue|null $maxVisits
* @param string[]|null $tags
*/
public function __construct(
#[LooseUriConverter]
readonly public string|null $longUrl = null,
readonly public bool $validSinceWasProvided = false,
DateTimeInterface|string|null $validSince = null,
readonly public bool $validUntilWasProvided = false,
DateTimeInterface|string|null $validUntil = null,
readonly public bool $maxVisitsWasProvided = false,
readonly public int|null $maxVisits = null,
public string|null $longUrl = null,
DateTimeInterface|string|NoValue|null $validSince = NoValue::NO_VALUE,
DateTimeInterface|string|NoValue|null $validUntil = NoValue::NO_VALUE,
int|NoValue|null $maxVisits = null,
#[TagsConverter]
readonly public array|null $tags = null,
readonly public bool $titleWasProvided = false,
public array|null $tags = null,
public bool $titleWasProvided = false,
#[SubstringConverter(512)]
readonly public string|null $title = null,
readonly public bool $titleWasAutoResolved = false,
readonly public bool|null $crawlable = null,
readonly public bool|null $forwardQuery = null,
public string|null $title = null,
public bool $titleWasAutoResolved = false,
public bool|null $crawlable = null,
public bool|null $forwardQuery = null,
) {
$this->validSince = normalizeOptionalDate($validSince);
$this->validUntil = normalizeOptionalDate($validUntil);
$this->validSince = normalizeOptionalDate(NoValue::resolve($validSince));
$this->validSinceWasProvided = $validSince !== NoValue::NO_VALUE;
$this->validUntil = normalizeOptionalDate(NoValue::resolve($validUntil));
$this->validUntilWasProvided = $validUntil !== NoValue::NO_VALUE;
$this->maxVisits = NoValue::resolve($maxVisits);
$this->maxVisitsWasProvided = $maxVisits !== NoValue::NO_VALUE;
}
public function withResolvedTitle(string $title): static
@@ -53,11 +62,8 @@ final class ShortUrlEdition implements TitleResolutionModelInterface
return new self(
longUrl: $this->longUrl,
validSinceWasProvided: $this->validSinceWasProvided,
validSince: $this->validSince,
validUntilWasProvided: $this->validUntilWasProvided,
validUntil: $this->validUntil,
maxVisitsWasProvided: $this->maxVisitsWasProvided,
maxVisits: $this->maxVisits,
tags: $this->tags,
titleWasProvided: $this->titleWasProvided,
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace Shlinkio\Shlink\Core\Util;
/**
* To be used as the default value for arguments where we want to detect the param has not been explicitly provided,
* where other typically "no value" values (like `null`) have their own meaning.
*
* An Enum is used so that there's a single instance, making it easier to perform comparison checks to narrow-down the
* type.
*
* ```
* function foo(int|null|NoValue $something = NoValue::NO_VALUE) { ... }
*
* foo(); - The value has not been provided
* foo(null); - Null has been explicitly provided
* foo(123); - 123 has been explicitly provided
* ```
*/
enum NoValue
{
case NO_VALUE;
/**
* @template T
* @param T|null $value
* @return (T is NoValue::NO_VALUE ? null : T)
*/
public static function resolve(mixed $value): mixed
{
return $value === NoValue::NO_VALUE ? null : $value;
}
}
@@ -68,8 +68,14 @@ class ShortUrlServiceTest extends TestCase
self::assertSame($shortUrl, $result);
['validSince' => $since, 'validUntil' => $until, 'maxVisits' => $maxVisits] = $shortUrl->toArray()['meta'];
self::assertEquals($shortUrlEdit->validSince?->toAtomString(), $since);
self::assertEquals($shortUrlEdit->validUntil?->toAtomString(), $until);
self::assertEquals(
$shortUrlEdit->validSince instanceof Chronos ? $shortUrlEdit->validSince->toAtomString() : null,
$since,
);
self::assertEquals(
$shortUrlEdit->validUntil instanceof Chronos ? $shortUrlEdit->validUntil->toAtomString() : null,
$until,
);
self::assertEquals($shortUrlEdit->maxVisits, $maxVisits);
self::assertEquals($shortUrlEdit->longUrl ?? $originalLongUrl, $shortUrl->getLongUrl());
}
@@ -77,18 +83,13 @@ class ShortUrlServiceTest extends TestCase
public static function provideShortUrlEdits(): iterable
{
yield 'no long URL' => [new InvokedCount(0), new ShortUrlEdition(
validSinceWasProvided: true,
validSince: Chronos::parse('2017-01-01 00:00:00'),
validUntilWasProvided: true,
validUntil: Chronos::parse('2017-01-05 00:00:00'),
maxVisitsWasProvided: true,
maxVisits: 5,
), null];
yield 'long URL and API key' => [new InvokedCount(1), new ShortUrlEdition(
longUrl: 'https://modifiedLongUrl',
validSinceWasProvided: true,
validSince: Chronos::parse('2017-01-01 00:00:00'),
maxVisitsWasProvided: true,
maxVisits: 10,
), ApiKey::create()];
}
@@ -34,9 +34,6 @@ class EditShortUrlAction extends AbstractRestAction
$body = (array) $request->getParsedBody();
$shortUrlEdit = $this->treeMapper->map(ShortUrlEdition::class, [
...$body,
'validSinceWasProvided' => array_key_exists('validSince', $body),
'validUntilWasProvided' => array_key_exists('validUntil', $body),
'maxVisitsWasProvided' => array_key_exists('maxVisits', $body),
'titleWasProvided' => array_key_exists('title', $body),
]);