mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
Merge pull request #2035 from acelaya-forks/feature/improve-rules-persistence
Remove name and uniqueness in redirect condition table
This commit is contained in:
commit
a7cde9364a
@ -50,13 +50,11 @@
|
|||||||
"priority": 1,
|
"priority": 1,
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
"name": "device-android",
|
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"matchValue": "android",
|
"matchValue": "android",
|
||||||
"matchKey": null
|
"matchKey": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "language-en-US",
|
|
||||||
"type": "language",
|
"type": "language",
|
||||||
"matchValue": "en-US",
|
"matchValue": "en-US",
|
||||||
"matchKey": null
|
"matchKey": null
|
||||||
@ -68,7 +66,6 @@
|
|||||||
"priority": 2,
|
"priority": 2,
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
"name": "language-fr",
|
|
||||||
"type": "language",
|
"type": "language",
|
||||||
"matchValue": "fr",
|
"matchValue": "fr",
|
||||||
"matchKey": null
|
"matchKey": null
|
||||||
@ -80,13 +77,11 @@
|
|||||||
"priority": 3,
|
"priority": 3,
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
"name": "query-foo-bar",
|
|
||||||
"type": "query",
|
"type": "query",
|
||||||
"matchKey": "foo",
|
"matchKey": "foo",
|
||||||
"matchValue": "bar"
|
"matchValue": "bar"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "query-hello-world",
|
|
||||||
"type": "query",
|
"type": "query",
|
||||||
"matchKey": "hello",
|
"matchKey": "hello",
|
||||||
"matchValue": "world"
|
"matchValue": "world"
|
||||||
|
@ -22,13 +22,6 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
|||||||
->option('unsigned', true)
|
->option('unsigned', true)
|
||||||
->build();
|
->build();
|
||||||
|
|
||||||
fieldWithUtf8Charset($builder->createField('name', Types::STRING), $emConfig)
|
|
||||||
->columnName('name')
|
|
||||||
->length(512)
|
|
||||||
->build();
|
|
||||||
|
|
||||||
$builder->addUniqueConstraint(['name'], 'UQ_name');
|
|
||||||
|
|
||||||
(new FieldBuilder($builder, [
|
(new FieldBuilder($builder, [
|
||||||
'fieldName' => 'type',
|
'fieldName' => 'type',
|
||||||
'type' => Types::STRING,
|
'type' => Types::STRING,
|
||||||
|
@ -33,10 +33,15 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
|||||||
->addJoinColumn('short_url_id', 'id', nullable: false, onDelete: 'CASCADE')
|
->addJoinColumn('short_url_id', 'id', nullable: false, onDelete: 'CASCADE')
|
||||||
->build();
|
->build();
|
||||||
|
|
||||||
|
// We treat this ManyToMany relation as a unidirectional OneToMany, where conditions are persisted and deleted
|
||||||
|
// together with the rule
|
||||||
$builder->createManyToMany('conditions', RedirectRule\Entity\RedirectCondition::class)
|
$builder->createManyToMany('conditions', RedirectRule\Entity\RedirectCondition::class)
|
||||||
->setJoinTable(determineTableName('redirect_conditions_in_short_url_redirect_rules', $emConfig))
|
->setJoinTable(determineTableName('redirect_conditions_in_short_url_redirect_rules', $emConfig))
|
||||||
->addInverseJoinColumn('redirect_condition_id', 'id', onDelete: 'CASCADE')
|
->addInverseJoinColumn('redirect_condition_id', 'id', onDelete: 'CASCADE')
|
||||||
->addJoinColumn('short_url_redirect_rule_id', 'id', onDelete: 'CASCADE')
|
->addJoinColumn('short_url_redirect_rule_id', 'id', onDelete: 'CASCADE')
|
||||||
->fetchEager() // Always fetch the corresponding conditions when loading a rule
|
->fetchEager() // Always fetch the corresponding conditions when loading a rule
|
||||||
|
->setOrderBy(['id' => 'ASC']) // Ensure a reliable order in the list of conditions
|
||||||
|
->cascadePersist() // Create automatically with the rule
|
||||||
|
->orphanRemoval() // Remove conditions when they are not linked to any rule
|
||||||
->build();
|
->build();
|
||||||
};
|
};
|
||||||
|
@ -34,8 +34,6 @@ final class Version20240224115725 extends AbstractMigration
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$redirectConditions = $this->createTableWithId($schema, 'redirect_conditions');
|
$redirectConditions = $this->createTableWithId($schema, 'redirect_conditions');
|
||||||
$redirectConditions->addColumn('name', Types::STRING, ['length' => 512]);
|
|
||||||
$redirectConditions->addUniqueIndex(['name'], 'UQ_name');
|
|
||||||
|
|
||||||
$redirectConditions->addColumn('type', Types::STRING, ['length' => 255]);
|
$redirectConditions->addColumn('type', Types::STRING, ['length' => 255]);
|
||||||
$redirectConditions->addColumn('match_key', Types::STRING, [
|
$redirectConditions->addColumn('match_key', Types::STRING, [
|
||||||
|
@ -16,36 +16,7 @@ final class Version20240226214216 extends AbstractMigration
|
|||||||
{
|
{
|
||||||
$this->skipIf(! $schema->hasTable('device_long_urls'));
|
$this->skipIf(! $schema->hasTable('device_long_urls'));
|
||||||
|
|
||||||
// First create redirect conditions for all device types
|
// Insert a rule per every device_long_url, and link it to the corresponding condition
|
||||||
$qb = $this->connection->createQueryBuilder();
|
|
||||||
$devices = $qb->select('device_type')
|
|
||||||
->distinct()
|
|
||||||
->from('device_long_urls')
|
|
||||||
->executeQuery();
|
|
||||||
|
|
||||||
$conditionIds = [];
|
|
||||||
while ($deviceRow = $devices->fetchAssociative()) {
|
|
||||||
$deviceType = $deviceRow['device_type'];
|
|
||||||
$conditionQb = $this->connection->createQueryBuilder();
|
|
||||||
$conditionQb->insert('redirect_conditions')
|
|
||||||
->values([
|
|
||||||
'name' => ':name',
|
|
||||||
'type' => ':type',
|
|
||||||
'match_value' => ':match_value',
|
|
||||||
'match_key' => ':match_key',
|
|
||||||
])
|
|
||||||
->setParameters([
|
|
||||||
'name' => 'device-' . $deviceType,
|
|
||||||
'type' => 'device',
|
|
||||||
'match_value' => $deviceType,
|
|
||||||
'match_key' => null,
|
|
||||||
])
|
|
||||||
->executeStatement();
|
|
||||||
$id = $this->connection->lastInsertId();
|
|
||||||
$conditionIds[$deviceType] = $id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then insert a rule per every device_long_url, and link it to the corresponding condition
|
|
||||||
$qb = $this->connection->createQueryBuilder();
|
$qb = $this->connection->createQueryBuilder();
|
||||||
$rules = $qb->select('short_url_id', 'device_type', 'long_url')
|
$rules = $qb->select('short_url_id', 'device_type', 'long_url')
|
||||||
->from('device_long_urls')
|
->from('device_long_urls')
|
||||||
@ -71,6 +42,22 @@ final class Version20240226214216 extends AbstractMigration
|
|||||||
->executeStatement();
|
->executeStatement();
|
||||||
$ruleId = $this->connection->lastInsertId();
|
$ruleId = $this->connection->lastInsertId();
|
||||||
|
|
||||||
|
$deviceType = $ruleRow['device_type'];
|
||||||
|
$conditionQb = $this->connection->createQueryBuilder();
|
||||||
|
$conditionQb->insert('redirect_conditions')
|
||||||
|
->values([
|
||||||
|
'type' => ':type',
|
||||||
|
'match_value' => ':match_value',
|
||||||
|
'match_key' => ':match_key',
|
||||||
|
])
|
||||||
|
->setParameters([
|
||||||
|
'type' => 'device',
|
||||||
|
'match_value' => $deviceType,
|
||||||
|
'match_key' => null,
|
||||||
|
])
|
||||||
|
->executeStatement();
|
||||||
|
$conditionId = $this->connection->lastInsertId();
|
||||||
|
|
||||||
$relationQb = $this->connection->createQueryBuilder();
|
$relationQb = $this->connection->createQueryBuilder();
|
||||||
$relationQb->insert('redirect_conditions_in_short_url_redirect_rules')
|
$relationQb->insert('redirect_conditions_in_short_url_redirect_rules')
|
||||||
->values([
|
->values([
|
||||||
@ -78,7 +65,7 @@ final class Version20240226214216 extends AbstractMigration
|
|||||||
'short_url_redirect_rule_id' => ':short_url_redirect_rule_id',
|
'short_url_redirect_rule_id' => ':short_url_redirect_rule_id',
|
||||||
])
|
])
|
||||||
->setParameters([
|
->setParameters([
|
||||||
'redirect_condition_id' => $conditionIds[$ruleRow['device_type']],
|
'redirect_condition_id' => $conditionId,
|
||||||
'short_url_redirect_rule_id' => $ruleId,
|
'short_url_redirect_rule_id' => $ruleId,
|
||||||
])
|
])
|
||||||
->executeStatement();
|
->executeStatement();
|
||||||
|
@ -12,14 +12,12 @@ use function Shlinkio\Shlink\Core\acceptLanguageToLocales;
|
|||||||
use function Shlinkio\Shlink\Core\ArrayUtils\some;
|
use function Shlinkio\Shlink\Core\ArrayUtils\some;
|
||||||
use function Shlinkio\Shlink\Core\normalizeLocale;
|
use function Shlinkio\Shlink\Core\normalizeLocale;
|
||||||
use function Shlinkio\Shlink\Core\splitLocale;
|
use function Shlinkio\Shlink\Core\splitLocale;
|
||||||
use function sprintf;
|
|
||||||
use function strtolower;
|
use function strtolower;
|
||||||
use function trim;
|
use function trim;
|
||||||
|
|
||||||
class RedirectCondition extends AbstractEntity implements JsonSerializable
|
class RedirectCondition extends AbstractEntity implements JsonSerializable
|
||||||
{
|
{
|
||||||
private function __construct(
|
private function __construct(
|
||||||
public readonly string $name,
|
|
||||||
private readonly RedirectConditionType $type,
|
private readonly RedirectConditionType $type,
|
||||||
private readonly string $matchValue,
|
private readonly string $matchValue,
|
||||||
private readonly ?string $matchKey = null,
|
private readonly ?string $matchKey = null,
|
||||||
@ -28,26 +26,17 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
|
|||||||
|
|
||||||
public static function forQueryParam(string $param, string $value): self
|
public static function forQueryParam(string $param, string $value): self
|
||||||
{
|
{
|
||||||
$type = RedirectConditionType::QUERY_PARAM;
|
return new self(RedirectConditionType::QUERY_PARAM, $value, $param);
|
||||||
$name = sprintf('%s-%s-%s', $type->value, $param, $value);
|
|
||||||
|
|
||||||
return new self($name, $type, $value, $param);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function forLanguage(string $language): self
|
public static function forLanguage(string $language): self
|
||||||
{
|
{
|
||||||
$type = RedirectConditionType::LANGUAGE;
|
return new self(RedirectConditionType::LANGUAGE, $language);
|
||||||
$name = sprintf('%s-%s', $type->value, $language);
|
|
||||||
|
|
||||||
return new self($name, $type, $language);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function forDevice(DeviceType $device): self
|
public static function forDevice(DeviceType $device): self
|
||||||
{
|
{
|
||||||
$type = RedirectConditionType::DEVICE;
|
return new self(RedirectConditionType::DEVICE, $device->value);
|
||||||
$name = sprintf('%s-%s', $type->value, $device->value);
|
|
||||||
|
|
||||||
return new self($name, $type, $device->value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,7 +92,6 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
|
|||||||
public function jsonSerialize(): array
|
public function jsonSerialize(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => $this->name,
|
|
||||||
'type' => $this->type->value,
|
'type' => $this->type->value,
|
||||||
'matchKey' => $this->matchKey,
|
'matchKey' => $this->matchKey,
|
||||||
'matchValue' => $this->matchValue,
|
'matchValue' => $this->matchValue,
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
namespace ShlinkioTest\Shlink\Core\RedirectRule\Entity;
|
namespace ShlinkioTest\Shlink\Core\RedirectRule\Entity;
|
||||||
|
|
||||||
use Laminas\Diactoros\ServerRequestFactory;
|
use Laminas\Diactoros\ServerRequestFactory;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\Attributes\TestWith;
|
use PHPUnit\Framework\Attributes\TestWith;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@ -71,21 +70,4 @@ class RedirectConditionTest extends TestCase
|
|||||||
|
|
||||||
self::assertEquals($expected, $result);
|
self::assertEquals($expected, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test, DataProvider('provideNames')]
|
|
||||||
public function generatesExpectedName(RedirectCondition $condition, string $expectedName): void
|
|
||||||
{
|
|
||||||
self::assertEquals($expectedName, $condition->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function provideNames(): iterable
|
|
||||||
{
|
|
||||||
yield [RedirectCondition::forLanguage('es-ES'), 'language-es-ES'];
|
|
||||||
yield [RedirectCondition::forLanguage('en_UK'), 'language-en_UK'];
|
|
||||||
yield [RedirectCondition::forQueryParam('foo', 'bar'), 'query-foo-bar'];
|
|
||||||
yield [RedirectCondition::forQueryParam('baz', 'foo'), 'query-baz-foo'];
|
|
||||||
yield [RedirectCondition::forDevice(DeviceType::ANDROID), 'device-android'];
|
|
||||||
yield [RedirectCondition::forDevice(DeviceType::IOS), 'device-ios'];
|
|
||||||
yield [RedirectCondition::forDevice(DeviceType::DESKTOP), 'device-desktop'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,13 +13,11 @@ use function sprintf;
|
|||||||
class ListRedirectRulesTest extends ApiTestCase
|
class ListRedirectRulesTest extends ApiTestCase
|
||||||
{
|
{
|
||||||
private const LANGUAGE_EN_CONDITION = [
|
private const LANGUAGE_EN_CONDITION = [
|
||||||
'name' => 'language-en',
|
|
||||||
'type' => 'language',
|
'type' => 'language',
|
||||||
'matchKey' => null,
|
'matchKey' => null,
|
||||||
'matchValue' => 'en',
|
'matchValue' => 'en',
|
||||||
];
|
];
|
||||||
private const QUERY_FOO_BAR_CONDITION = [
|
private const QUERY_FOO_BAR_CONDITION = [
|
||||||
'name' => 'query-foo-bar',
|
|
||||||
'type' => 'query',
|
'type' => 'query',
|
||||||
'matchKey' => 'foo',
|
'matchKey' => 'foo',
|
||||||
'matchValue' => 'bar',
|
'matchValue' => 'bar',
|
||||||
@ -54,13 +52,12 @@ class ListRedirectRulesTest extends ApiTestCase
|
|||||||
'longUrl' => 'https://example.com/multiple-query-params',
|
'longUrl' => 'https://example.com/multiple-query-params',
|
||||||
'priority' => 2,
|
'priority' => 2,
|
||||||
'conditions' => [
|
'conditions' => [
|
||||||
self::QUERY_FOO_BAR_CONDITION,
|
|
||||||
[
|
[
|
||||||
'name' => 'query-hello-world',
|
|
||||||
'type' => 'query',
|
'type' => 'query',
|
||||||
'matchKey' => 'hello',
|
'matchKey' => 'hello',
|
||||||
'matchValue' => 'world',
|
'matchValue' => 'world',
|
||||||
],
|
],
|
||||||
|
self::QUERY_FOO_BAR_CONDITION,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -73,7 +70,6 @@ class ListRedirectRulesTest extends ApiTestCase
|
|||||||
'priority' => 4,
|
'priority' => 4,
|
||||||
'conditions' => [
|
'conditions' => [
|
||||||
[
|
[
|
||||||
'name' => 'device-android',
|
|
||||||
'type' => 'device',
|
'type' => 'device',
|
||||||
'matchKey' => null,
|
'matchKey' => null,
|
||||||
'matchValue' => 'android',
|
'matchValue' => 'android',
|
||||||
@ -85,7 +81,6 @@ class ListRedirectRulesTest extends ApiTestCase
|
|||||||
'priority' => 5,
|
'priority' => 5,
|
||||||
'conditions' => [
|
'conditions' => [
|
||||||
[
|
[
|
||||||
'name' => 'device-ios',
|
|
||||||
'type' => 'device',
|
'type' => 'device',
|
||||||
'matchKey' => null,
|
'matchKey' => null,
|
||||||
'matchValue' => 'ios',
|
'matchValue' => 'ios',
|
||||||
|
@ -25,27 +25,14 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
|
|||||||
/** @var ShortUrl $defShortUrl */
|
/** @var ShortUrl $defShortUrl */
|
||||||
$defShortUrl = $this->getReference('def456_short_url');
|
$defShortUrl = $this->getReference('def456_short_url');
|
||||||
|
|
||||||
$englishCondition = RedirectCondition::forLanguage('en');
|
|
||||||
$manager->persist($englishCondition);
|
|
||||||
|
|
||||||
$fooQueryCondition = RedirectCondition::forQueryParam('foo', 'bar');
|
|
||||||
$manager->persist($fooQueryCondition);
|
|
||||||
|
|
||||||
$helloQueryCondition = RedirectCondition::forQueryParam('hello', 'world');
|
|
||||||
$manager->persist($helloQueryCondition);
|
|
||||||
|
|
||||||
$androidCondition = RedirectCondition::forDevice(DeviceType::ANDROID);
|
|
||||||
$manager->persist($androidCondition);
|
|
||||||
|
|
||||||
$iosCondition = RedirectCondition::forDevice(DeviceType::IOS);
|
|
||||||
$manager->persist($iosCondition);
|
|
||||||
|
|
||||||
// Create rules disordered to make sure the order by priority works
|
// Create rules disordered to make sure the order by priority works
|
||||||
$multipleQueryParamsRule = new ShortUrlRedirectRule(
|
$multipleQueryParamsRule = new ShortUrlRedirectRule(
|
||||||
shortUrl: $defShortUrl,
|
shortUrl: $defShortUrl,
|
||||||
priority: 2,
|
priority: 2,
|
||||||
longUrl: 'https://example.com/multiple-query-params',
|
longUrl: 'https://example.com/multiple-query-params',
|
||||||
conditions: new ArrayCollection([$helloQueryCondition, $fooQueryCondition]),
|
conditions: new ArrayCollection(
|
||||||
|
[RedirectCondition::forQueryParam('hello', 'world'), RedirectCondition::forQueryParam('foo', 'bar')],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
$manager->persist($multipleQueryParamsRule);
|
$manager->persist($multipleQueryParamsRule);
|
||||||
|
|
||||||
@ -53,7 +40,9 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
|
|||||||
shortUrl: $defShortUrl,
|
shortUrl: $defShortUrl,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
longUrl: 'https://example.com/english-and-foo-query',
|
longUrl: 'https://example.com/english-and-foo-query',
|
||||||
conditions: new ArrayCollection([$englishCondition, $fooQueryCondition]),
|
conditions: new ArrayCollection(
|
||||||
|
[RedirectCondition::forLanguage('en'), RedirectCondition::forQueryParam('foo', 'bar')],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
$manager->persist($englishAndFooQueryRule);
|
$manager->persist($englishAndFooQueryRule);
|
||||||
|
|
||||||
@ -61,7 +50,7 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
|
|||||||
shortUrl: $defShortUrl,
|
shortUrl: $defShortUrl,
|
||||||
priority: 4,
|
priority: 4,
|
||||||
longUrl: 'https://blog.alejandrocelaya.com/android',
|
longUrl: 'https://blog.alejandrocelaya.com/android',
|
||||||
conditions: new ArrayCollection([$androidCondition]),
|
conditions: new ArrayCollection([RedirectCondition::forDevice(DeviceType::ANDROID)]),
|
||||||
);
|
);
|
||||||
$manager->persist($androidRule);
|
$manager->persist($androidRule);
|
||||||
|
|
||||||
@ -69,7 +58,7 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
|
|||||||
shortUrl: $defShortUrl,
|
shortUrl: $defShortUrl,
|
||||||
priority: 3,
|
priority: 3,
|
||||||
longUrl: 'https://example.com/only-english',
|
longUrl: 'https://example.com/only-english',
|
||||||
conditions: new ArrayCollection([$englishCondition]),
|
conditions: new ArrayCollection([RedirectCondition::forLanguage('en')]),
|
||||||
);
|
);
|
||||||
$manager->persist($onlyEnglishRule);
|
$manager->persist($onlyEnglishRule);
|
||||||
|
|
||||||
@ -77,7 +66,7 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
|
|||||||
shortUrl: $defShortUrl,
|
shortUrl: $defShortUrl,
|
||||||
priority: 5,
|
priority: 5,
|
||||||
longUrl: 'https://blog.alejandrocelaya.com/ios',
|
longUrl: 'https://blog.alejandrocelaya.com/ios',
|
||||||
conditions: new ArrayCollection([$iosCondition]),
|
conditions: new ArrayCollection([RedirectCondition::forDevice(DeviceType::IOS)]),
|
||||||
);
|
);
|
||||||
$manager->persist($iosRule);
|
$manager->persist($iosRule);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user