diff --git a/data/migrations/Version20210202181026.php b/data/migrations/Version20210202181026.php new file mode 100644 index 00000000..00e82a40 --- /dev/null +++ b/data/migrations/Version20210202181026.php @@ -0,0 +1,32 @@ +getTable('short_urls'); + $this->skipIf($shortUrls->hasColumn(self::TITLE)); + + $shortUrls->addColumn(self::TITLE, Types::STRING, [ + 'notnull' => false, + 'length' => 512, + ]); + } + + public function down(Schema $schema): void + { + $shortUrls = $schema->getTable('short_urls'); + $this->skipIf(! $shortUrls->hasColumn(self::TITLE)); + $shortUrls->dropColumn(self::TITLE); + } +} diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Entity.ShortUrl.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Entity.ShortUrl.php index da4506af..df17ad95 100644 --- a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Entity.ShortUrl.php +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Entity.ShortUrl.php @@ -84,4 +84,10 @@ return static function (ClassMetadata $metadata, array $emConfig): void { ->build(); $builder->addUniqueConstraint(['short_code', 'domain_id'], 'unique_short_code_plus_domain'); + + $builder->createField('title', Types::STRING) + ->columnName('title') + ->length(512) + ->nullable() + ->build(); }; diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index c41d506e..fd061996 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -38,6 +38,7 @@ class ShortUrl extends AbstractEntity private ?string $importSource = null; private ?string $importOriginalShortCode = null; private ?ApiKey $authorApiKey = null; + private ?string $title = null; private function __construct() { @@ -72,6 +73,7 @@ class ShortUrl extends AbstractEntity $instance->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($instance->shortCodeLength); $instance->domain = $relationResolver->resolveDomain($meta->getDomain()); $instance->authorApiKey = $meta->getApiKey(); + $instance->title = $meta->getTitle(); return $instance; } @@ -157,6 +159,11 @@ class ShortUrl extends AbstractEntity return $this->maxVisits; } + public function getTitle(): ?string + { + return $this->title; + } + public function update( ShortUrlEdit $shortUrlEdit, ?ShortUrlRelationResolverInterface $relationResolver = null diff --git a/module/Core/src/Model/ShortUrlMeta.php b/module/Core/src/Model/ShortUrlMeta.php index 13f36362..65ff5e1e 100644 --- a/module/Core/src/Model/ShortUrlMeta.php +++ b/module/Core/src/Model/ShortUrlMeta.php @@ -28,6 +28,7 @@ final class ShortUrlMeta private ?bool $validateUrl = null; private ?ApiKey $apiKey = null; private array $tags = []; + private ?string $title = null; private function __construct() { @@ -76,6 +77,7 @@ final class ShortUrlMeta ) ?? DEFAULT_SHORT_CODES_LENGTH; $this->apiKey = $inputFilter->getValue(ShortUrlInputFilter::API_KEY); $this->tags = $inputFilter->getValue(ShortUrlInputFilter::TAGS); + $this->title = $inputFilter->getValue(ShortUrlInputFilter::TITLE); } public function getLongUrl(): string @@ -160,4 +162,9 @@ final class ShortUrlMeta { return $this->tags; } + + public function getTitle(): ?string + { + return $this->title; + } } diff --git a/module/Core/src/Model/ShortUrlsOrdering.php b/module/Core/src/Model/ShortUrlsOrdering.php index e1708a86..b59435ca 100644 --- a/module/Core/src/Model/ShortUrlsOrdering.php +++ b/module/Core/src/Model/ShortUrlsOrdering.php @@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Model; use Shlinkio\Shlink\Core\Exception\ValidationException; +use function array_pad; use function explode; use function is_array; use function is_string; @@ -50,9 +51,9 @@ final class ShortUrlsOrdering /** @var string|array $orderBy */ if (! $isArray) { - $parts = explode('-', $orderBy); - $this->orderField = $parts[0]; - $this->orderDirection = $parts[1] ?? self::DEFAULT_ORDER_DIRECTION; + [$field, $dir] = array_pad(explode('-', $orderBy), 2, null); + $this->orderField = $field; + $this->orderDirection = $dir ?? self::DEFAULT_ORDER_DIRECTION; } else { $this->orderField = key($orderBy); $this->orderDirection = $orderBy[$this->orderField]; diff --git a/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php b/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php index 462178a0..ce459714 100644 --- a/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php +++ b/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php @@ -34,6 +34,7 @@ class ShortUrlDataTransformer implements DataTransformerInterface 'tags' => invoke($shortUrl->getTags(), '__toString'), 'meta' => $this->buildMeta($shortUrl), 'domain' => $shortUrl->getDomain(), + 'title' => $shortUrl->getTitle(), ]; } diff --git a/module/Core/src/Validation/ShortUrlInputFilter.php b/module/Core/src/Validation/ShortUrlInputFilter.php index fa333b49..b5d4fa07 100644 --- a/module/Core/src/Validation/ShortUrlInputFilter.php +++ b/module/Core/src/Validation/ShortUrlInputFilter.php @@ -31,6 +31,7 @@ class ShortUrlInputFilter extends InputFilter public const VALIDATE_URL = 'validateUrl'; public const API_KEY = 'apiKey'; public const TAGS = 'tags'; + public const TITLE = 'title'; private function __construct(array $data, bool $requireLongUrl) { @@ -87,6 +88,8 @@ class ShortUrlInputFilter extends InputFilter $this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false)); + // This cannot be defined as a boolean input because it can actually have 3 values, true, false and null. + // Defining it as boolean will make null fall back to false, which is not the desired behavior. $this->add($this->createInput(self::VALIDATE_URL, false)); $domain = $this->createInput(self::DOMAIN, false); @@ -100,5 +103,7 @@ class ShortUrlInputFilter extends InputFilter $this->add($apiKeyInput); $this->add($this->createTagsInput(self::TAGS, false)); + + $this->add($this->createInput(self::TITLE, false)); } } diff --git a/module/Core/test/Mercure/MercureUpdatesGeneratorTest.php b/module/Core/test/Mercure/MercureUpdatesGeneratorTest.php index c3a8463f..435fb4d8 100644 --- a/module/Core/test/Mercure/MercureUpdatesGeneratorTest.php +++ b/module/Core/test/Mercure/MercureUpdatesGeneratorTest.php @@ -28,9 +28,13 @@ class MercureUpdatesGeneratorTest extends TestCase * @test * @dataProvider provideMethod */ - public function visitIsProperlySerializedIntoUpdate(string $method, string $expectedTopic): void + public function visitIsProperlySerializedIntoUpdate(string $method, string $expectedTopic, ?string $title): void { - $shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['customSlug' => 'foo', 'longUrl' => ''])); + $shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData([ + 'customSlug' => 'foo', + 'longUrl' => '', + 'title' => $title, + ])); $visit = new Visit($shortUrl, Visitor::emptyInstance()); $update = $this->generator->{$method}($visit); @@ -50,6 +54,7 @@ class MercureUpdatesGeneratorTest extends TestCase 'maxVisits' => null, ], 'domain' => null, + 'title' => $title, ], 'visit' => [ 'referer' => '', @@ -62,7 +67,7 @@ class MercureUpdatesGeneratorTest extends TestCase public function provideMethod(): iterable { - yield 'newVisitUpdate' => ['newVisitUpdate', 'https://shlink.io/new-visit']; - yield 'newShortUrlVisitUpdate' => ['newShortUrlVisitUpdate', 'https://shlink.io/new-visit/foo']; + yield 'newVisitUpdate' => ['newVisitUpdate', 'https://shlink.io/new-visit', 'the cool title']; + yield 'newShortUrlVisitUpdate' => ['newShortUrlVisitUpdate', 'https://shlink.io/new-visit/foo', null]; } }