mirror of
https://github.com/shlinkio/shlink.git
synced 2024-10-22 20:25:35 +03:00
Created persistence for device long URLs
This commit is contained in:
parent
5f2f179581
commit
12150f775d
14 changed files with 209 additions and 38 deletions
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
53
data/migrations/Version20230103105343.php
Normal file
53
data/migrations/Version20230103105343.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20230103105343 extends AbstractMigration
|
||||
{
|
||||
private const TABLE_NAME = 'device_long_urls';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
|
||||
return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->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();
|
||||
};
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<BackedEnum> $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)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
28
module/Core/src/Model/DeviceType.php
Normal file
28
module/Core/src/Model/DeviceType.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Model;
|
||||
|
||||
use Detection\MobileDetect;
|
||||
|
||||
enum DeviceType: string
|
||||
{
|
||||
case ANDROID = 'android';
|
||||
case IOS = 'ios';
|
||||
case DESKTOP = 'desktop';
|
||||
|
||||
public static function matchFromUserAgent(string $userAgent): ?self
|
||||
{
|
||||
$detect = new MobileDetect(null, $userAgent); // @phpstan-ignore-line
|
||||
|
||||
return match (true) {
|
||||
// $detect->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,
|
||||
};
|
||||
}
|
||||
}
|
18
module/Core/src/ShortUrl/Entity/DeviceLongUrl.php
Normal file
18
module/Core/src/ShortUrl/Entity/DeviceLongUrl.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Entity;
|
||||
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
|
||||
class DeviceLongUrl extends AbstractEntity
|
||||
{
|
||||
private function __construct(
|
||||
public readonly ShortUrl $shortUrl,
|
||||
public readonly DeviceType $deviceType,
|
||||
public readonly string $longUrl,
|
||||
) {
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
namespace Shlinkio\Shlink\Core\ShortUrl\Model;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Functional\map;
|
||||
|
||||
enum OrderableField: string
|
||||
{
|
||||
|
@ -14,14 +13,6 @@ enum OrderableField: string
|
|||
case VISITS = 'visits';
|
||||
case NON_BOT_VISITS = 'nonBotVisits';
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return map(self::cases(), static fn (OrderableField $field) => $field->value);
|
||||
}
|
||||
|
||||
public static function isBasicField(string $value): bool
|
||||
{
|
||||
return contains(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
43
module/Core/test/Functions/FunctionsTest.php
Normal file
43
module/Core/test/Functions/FunctionsTest.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Functions;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\OrderableField;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||
|
||||
use function Functional\map;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
class FunctionsTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideEnums
|
||||
*/
|
||||
public function enumValuesReturnsExpectedValueForEnum(string $enum, array $expectedValues): void
|
||||
{
|
||||
self::assertEquals($expectedValues, enumValues($enum));
|
||||
}
|
||||
|
||||
public function provideEnums(): iterable
|
||||
{
|
||||
yield EnvVars::class => [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),
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue