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);
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,
],
];

View file

@ -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) {

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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]);
}
}