mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
Short code lengths can now be customized
This commit is contained in:
parent
0b6602b275
commit
13555366e3
6 changed files with 68 additions and 9 deletions
|
@ -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,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue