From 12150f775ddacace6c02c8cb0c812ce9826d950a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 3 Jan 2023 13:45:39 +0100 Subject: [PATCH] Created persistence for device long URLs --- composer.json | 1 + config/config.php | 3 +- data/migrations/Version20230103105343.php | 53 +++++++++++++++++++ ...ink.Core.ShortUrl.Entity.DeviceLongUrl.php | 41 ++++++++++++++ ...o.Shlink.Core.ShortUrl.Entity.ShortUrl.php | 2 +- module/Core/functions/functions.php | 18 +++++++ module/Core/src/Config/EnvVars.php | 10 ---- module/Core/src/Model/DeviceType.php | 28 ++++++++++ .../src/ShortUrl/Entity/DeviceLongUrl.php | 18 +++++++ .../src/ShortUrl/Model/OrderableField.php | 9 ---- module/Core/src/ShortUrl/Model/TagsMode.php | 7 --- .../Validation/ShortUrlsParamsInputFilter.php | 6 ++- module/Core/test/Config/EnvVarsTest.php | 8 --- module/Core/test/Functions/FunctionsTest.php | 43 +++++++++++++++ 14 files changed, 209 insertions(+), 38 deletions(-) create mode 100644 data/migrations/Version20230103105343.php create mode 100644 module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.DeviceLongUrl.php create mode 100644 module/Core/src/Model/DeviceType.php create mode 100644 module/Core/src/ShortUrl/Entity/DeviceLongUrl.php create mode 100644 module/Core/test/Functions/FunctionsTest.php diff --git a/composer.json b/composer.json index 85608742..1ad2e7ab 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,7 @@ "mezzio/mezzio-problem-details": "^1.7", "mezzio/mezzio-swoole": "^4.5", "mlocati/ip-lib": "^1.18", + "mobiledetect/mobiledetectlib": "^3.74", "ocramius/proxy-manager": "^2.14", "pagerfanta/core": "^3.6", "php-middleware/request-id": "^4.1", diff --git a/config/config.php b/config/config.php index 8fe311a0..e0ec6c23 100644 --- a/config/config.php +++ b/config/config.php @@ -15,6 +15,7 @@ use function class_exists; use function Shlinkio\Shlink\Config\env; use function Shlinkio\Shlink\Config\openswooleIsInstalled; use function Shlinkio\Shlink\Config\runningInRoadRunner; +use function Shlinkio\Shlink\Core\enumValues; use const PHP_SAPI; @@ -23,7 +24,7 @@ $enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoad return (new ConfigAggregator\ConfigAggregator([ ! $isTestEnv - ? new EnvVarLoaderProvider('config/params/generated_config.php', Core\Config\EnvVars::values()) + ? new EnvVarLoaderProvider('config/params/generated_config.php', enumValues(Core\Config\EnvVars::class)) : new ConfigAggregator\ArrayProvider([]), Mezzio\ConfigProvider::class, Mezzio\Router\ConfigProvider::class, diff --git a/data/migrations/Version20230103105343.php b/data/migrations/Version20230103105343.php new file mode 100644 index 00000000..c61a8a94 --- /dev/null +++ b/data/migrations/Version20230103105343.php @@ -0,0 +1,53 @@ +skipIf($schema->hasTable(self::TABLE_NAME)); + + $table = $schema->createTable(self::TABLE_NAME); + $table->addColumn('id', Types::BIGINT, [ + 'unsigned' => true, + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->setPrimaryKey(['id']); + + $table->addColumn('device_type', Types::STRING, ['length' => 255]); + $table->addColumn('long_url', Types::STRING, ['length' => 2048]); + $table->addColumn('short_url_id', Types::BIGINT, [ + 'unsigned' => true, + 'notnull' => true, + ]); + + $table->addForeignKeyConstraint('short_urls', ['short_url_id'], ['id'], [ + 'onDelete' => 'CASCADE', + 'onUpdate' => 'RESTRICT', + ]); + + $table->addUniqueIndex(['device_type', 'short_url_id'], 'UQ_device_type_per_short_url'); + } + + public function down(Schema $schema): void + { + $this->skipIf(! $schema->hasTable(self::TABLE_NAME)); + $schema->dropTable(self::TABLE_NAME); + } + + public function isTransactional(): bool + { + return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform); + } +} diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.DeviceLongUrl.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.DeviceLongUrl.php new file mode 100644 index 00000000..8de69c18 --- /dev/null +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.DeviceLongUrl.php @@ -0,0 +1,41 @@ +setTable(determineTableName('device_long_urls', $emConfig)); + + $builder->createField('id', Types::BIGINT) + ->columnName('id') + ->makePrimaryKey() + ->generatedValue('IDENTITY') + ->option('unsigned', true) + ->build(); + + (new FieldBuilder($builder, [ + 'fieldName' => 'deviceType', + 'type' => Types::STRING, + 'enumType' => DeviceType::class, + ]))->columnName('device_type') + ->length(255) + ->build(); + + fieldWithUtf8Charset($builder->createField('longUrl', Types::STRING), $emConfig) + ->columnName('long_url') + ->length(2048) + ->build(); + + $builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class) + ->addJoinColumn('short_url_id', 'id', false, false, 'CASCADE') + ->build(); +}; diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.ShortUrl.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.ShortUrl.php index 6b769f34..b67996ae 100644 --- a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.ShortUrl.php +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.ShortUrl.php @@ -24,7 +24,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void { ->build(); fieldWithUtf8Charset($builder->createField('longUrl', Types::STRING), $emConfig) - ->columnName('original_url') + ->columnName('original_url') // Rename to long_url some day? ¯\_(ツ)_/¯ ->length(2048) ->build(); diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index 9d0b8d68..574d604c 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core; +use BackedEnum; use Cake\Chronos\Chronos; use Cake\Chronos\ChronosInterface; use DateTimeInterface; @@ -16,6 +17,7 @@ use PUGX\Shortid\Factory as ShortIdFactory; use Shlinkio\Shlink\Common\Util\DateRange; use function date_default_timezone_get; +use function Functional\map; use function Functional\reduce_left; use function is_array; use function print_r; @@ -159,3 +161,19 @@ function toProblemDetailsType(string $errorCode): string { return sprintf('https://shlink.io/api/error/%s', $errorCode); } + +/** + * @param class-string $enum + * @return string[] + */ +function enumValues(string $enum): array +{ + static $cache; + if ($cache === null) { + $cache = []; + } + + return $cache[$enum] ?? ( + $cache[$enum] = map($enum::cases(), static fn (BackedEnum $type) => (string) $type->value) + ); +} diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index 228a5921..75454ecc 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Config; -use function Functional\map; use function Shlinkio\Shlink\Config\env; enum EnvVars: string @@ -77,13 +76,4 @@ enum EnvVars: string { return $this->loadFromEnv() !== null; } - - /** - * @return string[] - */ - public static function values(): array - { - static $values; - return $values ?? ($values = map(self::cases(), static fn (EnvVars $envVar) => $envVar->value)); - } } diff --git a/module/Core/src/Model/DeviceType.php b/module/Core/src/Model/DeviceType.php new file mode 100644 index 00000000..df4a1838 --- /dev/null +++ b/module/Core/src/Model/DeviceType.php @@ -0,0 +1,28 @@ +is('iOS') && $detect->isTablet() => self::IOS, // TODO To detect iPad only +// $detect->is('iOS') && ! $detect->isTablet() => self::IOS, // TODO To detect iPhone only +// $detect->is('androidOS') && $detect->isTablet() => self::ANDROID, // TODO To detect Android tablets +// $detect->is('androidOS') && ! $detect->isTablet() => self::ANDROID, // TODO To detect Android phones + $detect->is('iOS') => self::IOS, // Detects both iPhone and iPad + $detect->is('androidOS') => self::ANDROID, // Detects both android phones and android tablets + ! $detect->isMobile() && ! $detect->isTablet() => self::DESKTOP, + default => null, + }; + } +} diff --git a/module/Core/src/ShortUrl/Entity/DeviceLongUrl.php b/module/Core/src/ShortUrl/Entity/DeviceLongUrl.php new file mode 100644 index 00000000..faf9bcc3 --- /dev/null +++ b/module/Core/src/ShortUrl/Entity/DeviceLongUrl.php @@ -0,0 +1,18 @@ + $field->value); - } - public static function isBasicField(string $value): bool { return contains( diff --git a/module/Core/src/ShortUrl/Model/TagsMode.php b/module/Core/src/ShortUrl/Model/TagsMode.php index 01cdcc3b..593d6d83 100644 --- a/module/Core/src/ShortUrl/Model/TagsMode.php +++ b/module/Core/src/ShortUrl/Model/TagsMode.php @@ -4,15 +4,8 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ShortUrl\Model; -use function Functional\map; - enum TagsMode: string { case ANY = 'any'; case ALL = 'all'; - - public static function values(): array - { - return map(self::cases(), static fn (TagsMode $mode) => $mode->value); - } } diff --git a/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php b/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php index cb120e8e..d7cda41e 100644 --- a/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php +++ b/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php @@ -11,6 +11,8 @@ use Shlinkio\Shlink\Common\Validation; use Shlinkio\Shlink\Core\ShortUrl\Model\OrderableField; use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode; +use function Shlinkio\Shlink\Core\enumValues; + class ShortUrlsParamsInputFilter extends InputFilter { use Validation\InputFactoryTrait; @@ -46,12 +48,12 @@ class ShortUrlsParamsInputFilter extends InputFilter $tagsMode = $this->createInput(self::TAGS_MODE, false); $tagsMode->getValidatorChain()->attach(new InArray([ - 'haystack' => TagsMode::values(), + 'haystack' => enumValues(TagsMode::class), 'strict' => InArray::COMPARE_STRICT, ])); $this->add($tagsMode); - $this->add($this->createOrderByInput(self::ORDER_BY, OrderableField::values())); + $this->add($this->createOrderByInput(self::ORDER_BY, enumValues(OrderableField::class))); $this->add($this->createBooleanInput(self::EXCLUDE_MAX_VISITS_REACHED, false)); $this->add($this->createBooleanInput(self::EXCLUDE_PAST_VALID_UNTIL, false)); diff --git a/module/Core/test/Config/EnvVarsTest.php b/module/Core/test/Config/EnvVarsTest.php index ff4878de..6d4b1394 100644 --- a/module/Core/test/Config/EnvVarsTest.php +++ b/module/Core/test/Config/EnvVarsTest.php @@ -7,7 +7,6 @@ namespace ShlinkioTest\Shlink\Core\Config; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Config\EnvVars; -use function Functional\map; use function putenv; class EnvVarsTest extends TestCase @@ -59,11 +58,4 @@ class EnvVarsTest extends TestCase yield 'DB_DRIVER without default' => [EnvVars::DB_DRIVER, null, null]; yield 'DB_DRIVER with default' => [EnvVars::DB_DRIVER, 'foobar', 'foobar']; } - - /** @test */ - public function allValuesCanBeListed(): void - { - $expected = map(EnvVars::cases(), static fn (EnvVars $envVar) => $envVar->value); - self::assertEquals(EnvVars::values(), $expected); - } } diff --git a/module/Core/test/Functions/FunctionsTest.php b/module/Core/test/Functions/FunctionsTest.php new file mode 100644 index 00000000..5ba6a7db --- /dev/null +++ b/module/Core/test/Functions/FunctionsTest.php @@ -0,0 +1,43 @@ + [EnvVars::class, map(EnvVars::cases(), static fn (EnvVars $envVar) => $envVar->value)]; + yield VisitType::class => [ + VisitType::class, + map(VisitType::cases(), static fn (VisitType $envVar) => $envVar->value), + ]; + yield DeviceType::class => [ + DeviceType::class, + map(DeviceType::cases(), static fn (DeviceType $envVar) => $envVar->value), + ]; + yield OrderableField::class => [ + OrderableField::class, + map(OrderableField::cases(), static fn (OrderableField $envVar) => $envVar->value), + ]; + } +}