From 13555366e3f16a06afee16921b4c4edaf9322598 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Feb 2020 18:54:40 +0100 Subject: [PATCH] Short code lengths can now be customized --- config/autoload/url-shortener.global.php | 3 +++ module/Core/functions/functions.php | 4 ++- module/Core/src/Entity/ShortUrl.php | 6 +++-- module/Core/src/Model/ShortUrlMeta.php | 21 +++++++++++++-- .../Validation/ShortUrlMetaInputFilter.php | 17 +++++++++--- module/Core/test/Entity/ShortUrlTest.php | 26 +++++++++++++++++++ 6 files changed, 68 insertions(+), 9 deletions(-) diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 5cf4f86f..165e0258 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; + return [ 'url_shortener' => [ @@ -11,6 +13,7 @@ return [ ], 'validate_url' => false, 'visits_webhooks' => [], + 'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH, ], ]; diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index 7ab5ebbb..61d0be1e 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -10,7 +10,9 @@ use PUGX\Shortid\Factory as ShortIdFactory; use function sprintf; -function generateRandomShortCode(int $length = 5): string +const DEFAULT_SHORT_CODES_LENGTH = 5; + +function generateRandomShortCode(int $length): string { static $shortIdFactory; if ($shortIdFactory === null) { diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index 98d6a146..4af8844b 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -34,6 +34,7 @@ class ShortUrl extends AbstractEntity private ?int $maxVisits = null; private ?Domain $domain; private bool $customSlugWasProvided; + private int $shortCodeLength; public function __construct( string $longUrl, @@ -50,7 +51,8 @@ class ShortUrl extends AbstractEntity $this->validUntil = $meta->getValidUntil(); $this->maxVisits = $meta->getMaxVisits(); $this->customSlugWasProvided = $meta->hasCustomSlug(); - $this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode(); + $this->shortCodeLength = $meta->getShortCodeLength(); + $this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($this->shortCodeLength); $this->domain = ($domainResolver ?? new SimpleDomainResolver())->resolveDomain($meta->getDomain()); } @@ -119,7 +121,7 @@ class ShortUrl extends AbstractEntity throw ShortCodeCannotBeRegeneratedException::forShortUrlAlreadyPersisted(); } - $this->shortCode = generateRandomShortCode(); + $this->shortCode = generateRandomShortCode($this->shortCodeLength); return $this; } diff --git a/module/Core/src/Model/ShortUrlMeta.php b/module/Core/src/Model/ShortUrlMeta.php index 27c8e624..3bba5c98 100644 --- a/module/Core/src/Model/ShortUrlMeta.php +++ b/module/Core/src/Model/ShortUrlMeta.php @@ -11,6 +11,8 @@ use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter; use function array_key_exists; use function Shlinkio\Shlink\Core\parseDateField; +use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; + final class ShortUrlMeta { private bool $validSincePropWasProvided = false; @@ -22,6 +24,7 @@ final class ShortUrlMeta private ?int $maxVisits = null; private ?bool $findIfExists = null; private ?string $domain = null; + private int $shortCodeLength = 5; // Force named constructors private function __construct() @@ -58,11 +61,20 @@ final class ShortUrlMeta $this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL)); $this->validUntilPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::VALID_UNTIL, $data); $this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG); - $maxVisits = $inputFilter->getValue(ShortUrlMetaInputFilter::MAX_VISITS); - $this->maxVisits = $maxVisits !== null ? (int) $maxVisits : null; + $this->maxVisits = $this->getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS); $this->maxVisitsPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::MAX_VISITS, $data); $this->findIfExists = $inputFilter->getValue(ShortUrlMetaInputFilter::FIND_IF_EXISTS); $this->domain = $inputFilter->getValue(ShortUrlMetaInputFilter::DOMAIN); + $this->shortCodeLength = $this->getOptionalIntFromInputFilter( + $inputFilter, + ShortUrlMetaInputFilter::SHORT_CODE_LENGTH, + ) ?? DEFAULT_SHORT_CODES_LENGTH; + } + + private function getOptionalIntFromInputFilter(ShortUrlMetaInputFilter $inputFilter, string $fieldName): ?int + { + $value = $inputFilter->getValue($fieldName); + return $value !== null ? (int) $value : null; } public function getValidSince(): ?Chronos @@ -119,4 +131,9 @@ final class ShortUrlMeta { return $this->domain; } + + public function getShortCodeLength(): int + { + return $this->shortCodeLength; + } } diff --git a/module/Core/src/Validation/ShortUrlMetaInputFilter.php b/module/Core/src/Validation/ShortUrlMetaInputFilter.php index 187ec66f..0663a760 100644 --- a/module/Core/src/Validation/ShortUrlMetaInputFilter.php +++ b/module/Core/src/Validation/ShortUrlMetaInputFilter.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Validation; use DateTime; +use Laminas\InputFilter\Input; use Laminas\InputFilter\InputFilter; use Laminas\Validator; use Shlinkio\Shlink\Common\Validation; @@ -19,6 +20,7 @@ class ShortUrlMetaInputFilter extends InputFilter public const MAX_VISITS = 'maxVisits'; public const FIND_IF_EXISTS = 'findIfExists'; public const DOMAIN = 'domain'; + public const SHORT_CODE_LENGTH = 'shortCodeLength'; public function __construct(array $data) { @@ -40,10 +42,8 @@ class ShortUrlMetaInputFilter extends InputFilter $customSlug->getFilterChain()->attach(new Validation\SluggerFilter()); $this->add($customSlug); - $maxVisits = $this->createInput(self::MAX_VISITS, false); - $maxVisits->getValidatorChain()->attach(new Validator\Digits()) - ->attach(new Validator\GreaterThan(['min' => 1, 'inclusive' => true])); - $this->add($maxVisits); + $this->add($this->createPositiveNumberInput(self::MAX_VISITS)); + $this->add($this->createPositiveNumberInput(self::SHORT_CODE_LENGTH)); $this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false)); @@ -51,4 +51,13 @@ class ShortUrlMetaInputFilter extends InputFilter $domain->getValidatorChain()->attach(new Validation\HostAndPortValidator()); $this->add($domain); } + + private function createPositiveNumberInput(string $name): Input + { + $input = $this->createInput($name, false); + $input->getValidatorChain()->attach(new Validator\Digits()) + ->attach(new Validator\GreaterThan(['min' => 1, 'inclusive' => true])); + + return $input; + } } diff --git a/module/Core/test/Entity/ShortUrlTest.php b/module/Core/test/Entity/ShortUrlTest.php index 9aba83fa..21c869aa 100644 --- a/module/Core/test/Entity/ShortUrlTest.php +++ b/module/Core/test/Entity/ShortUrlTest.php @@ -8,6 +8,13 @@ use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; +use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter; + +use function Functional\map; +use function range; +use function strlen; + +use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; class ShortUrlTest extends TestCase { @@ -48,4 +55,23 @@ class ShortUrlTest extends TestCase $this->assertNotEquals($firstShortCode, $secondShortCode); } + + /** + * @test + * @dataProvider provideLengths + */ + public function shortCodesHaveExpectedLength(?int $length, int $expectedLength): void + { + $shortUrl = new ShortUrl('', ShortUrlMeta::fromRawData( + [ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $length], + )); + + $this->assertEquals($expectedLength, strlen($shortUrl->getShortCode())); + } + + public function provideLengths(): iterable + { + yield [null, DEFAULT_SHORT_CODES_LENGTH]; + yield from map(range(1, 10), fn (int $value) => [$value, $value]); + } }