Short code lengths can now be customized

This commit is contained in:
Alejandro Celaya
2020-02-18 18:54:40 +01:00
parent 0b6602b275
commit 13555366e3
6 changed files with 68 additions and 9 deletions

View File

@@ -2,6 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
return [ return [
'url_shortener' => [ 'url_shortener' => [
@@ -11,6 +13,7 @@ return [
], ],
'validate_url' => false, 'validate_url' => false,
'visits_webhooks' => [], 'visits_webhooks' => [],
'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH,
], ],
]; ];

View File

@@ -10,7 +10,9 @@ use PUGX\Shortid\Factory as ShortIdFactory;
use function sprintf; use function sprintf;
function generateRandomShortCode(int $length = 5): string const DEFAULT_SHORT_CODES_LENGTH = 5;
function generateRandomShortCode(int $length): string
{ {
static $shortIdFactory; static $shortIdFactory;
if ($shortIdFactory === null) { if ($shortIdFactory === null) {

View File

@@ -34,6 +34,7 @@ class ShortUrl extends AbstractEntity
private ?int $maxVisits = null; private ?int $maxVisits = null;
private ?Domain $domain; private ?Domain $domain;
private bool $customSlugWasProvided; private bool $customSlugWasProvided;
private int $shortCodeLength;
public function __construct( public function __construct(
string $longUrl, string $longUrl,
@@ -50,7 +51,8 @@ class ShortUrl extends AbstractEntity
$this->validUntil = $meta->getValidUntil(); $this->validUntil = $meta->getValidUntil();
$this->maxVisits = $meta->getMaxVisits(); $this->maxVisits = $meta->getMaxVisits();
$this->customSlugWasProvided = $meta->hasCustomSlug(); $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()); $this->domain = ($domainResolver ?? new SimpleDomainResolver())->resolveDomain($meta->getDomain());
} }
@@ -119,7 +121,7 @@ class ShortUrl extends AbstractEntity
throw ShortCodeCannotBeRegeneratedException::forShortUrlAlreadyPersisted(); throw ShortCodeCannotBeRegeneratedException::forShortUrlAlreadyPersisted();
} }
$this->shortCode = generateRandomShortCode(); $this->shortCode = generateRandomShortCode($this->shortCodeLength);
return $this; return $this;
} }

View File

@@ -11,6 +11,8 @@ use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
use function array_key_exists; use function array_key_exists;
use function Shlinkio\Shlink\Core\parseDateField; use function Shlinkio\Shlink\Core\parseDateField;
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
final class ShortUrlMeta final class ShortUrlMeta
{ {
private bool $validSincePropWasProvided = false; private bool $validSincePropWasProvided = false;
@@ -22,6 +24,7 @@ final class ShortUrlMeta
private ?int $maxVisits = null; private ?int $maxVisits = null;
private ?bool $findIfExists = null; private ?bool $findIfExists = null;
private ?string $domain = null; private ?string $domain = null;
private int $shortCodeLength = 5;
// Force named constructors // Force named constructors
private function __construct() private function __construct()
@@ -58,11 +61,20 @@ final class ShortUrlMeta
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL)); $this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
$this->validUntilPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::VALID_UNTIL, $data); $this->validUntilPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::VALID_UNTIL, $data);
$this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG); $this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG);
$maxVisits = $inputFilter->getValue(ShortUrlMetaInputFilter::MAX_VISITS); $this->maxVisits = $this->getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS);
$this->maxVisits = $maxVisits !== null ? (int) $maxVisits : null;
$this->maxVisitsPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::MAX_VISITS, $data); $this->maxVisitsPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::MAX_VISITS, $data);
$this->findIfExists = $inputFilter->getValue(ShortUrlMetaInputFilter::FIND_IF_EXISTS); $this->findIfExists = $inputFilter->getValue(ShortUrlMetaInputFilter::FIND_IF_EXISTS);
$this->domain = $inputFilter->getValue(ShortUrlMetaInputFilter::DOMAIN); $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 public function getValidSince(): ?Chronos
@@ -119,4 +131,9 @@ final class ShortUrlMeta
{ {
return $this->domain; return $this->domain;
} }
public function getShortCodeLength(): int
{
return $this->shortCodeLength;
}
} }

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Validation; namespace Shlinkio\Shlink\Core\Validation;
use DateTime; use DateTime;
use Laminas\InputFilter\Input;
use Laminas\InputFilter\InputFilter; use Laminas\InputFilter\InputFilter;
use Laminas\Validator; use Laminas\Validator;
use Shlinkio\Shlink\Common\Validation; use Shlinkio\Shlink\Common\Validation;
@@ -19,6 +20,7 @@ class ShortUrlMetaInputFilter extends InputFilter
public const MAX_VISITS = 'maxVisits'; public const MAX_VISITS = 'maxVisits';
public const FIND_IF_EXISTS = 'findIfExists'; public const FIND_IF_EXISTS = 'findIfExists';
public const DOMAIN = 'domain'; public const DOMAIN = 'domain';
public const SHORT_CODE_LENGTH = 'shortCodeLength';
public function __construct(array $data) public function __construct(array $data)
{ {
@@ -40,10 +42,8 @@ class ShortUrlMetaInputFilter extends InputFilter
$customSlug->getFilterChain()->attach(new Validation\SluggerFilter()); $customSlug->getFilterChain()->attach(new Validation\SluggerFilter());
$this->add($customSlug); $this->add($customSlug);
$maxVisits = $this->createInput(self::MAX_VISITS, false); $this->add($this->createPositiveNumberInput(self::MAX_VISITS));
$maxVisits->getValidatorChain()->attach(new Validator\Digits()) $this->add($this->createPositiveNumberInput(self::SHORT_CODE_LENGTH));
->attach(new Validator\GreaterThan(['min' => 1, 'inclusive' => true]));
$this->add($maxVisits);
$this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false)); $this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false));
@@ -51,4 +51,13 @@ class ShortUrlMetaInputFilter extends InputFilter
$domain->getValidatorChain()->attach(new Validation\HostAndPortValidator()); $domain->getValidatorChain()->attach(new Validation\HostAndPortValidator());
$this->add($domain); $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;
}
} }

View File

@@ -8,6 +8,13 @@ use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException; use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; 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 class ShortUrlTest extends TestCase
{ {
@@ -48,4 +55,23 @@ class ShortUrlTest extends TestCase
$this->assertNotEquals($firstShortCode, $secondShortCode); $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]);
}
} }