Merge pull request #2035 from acelaya-forks/feature/improve-rules-persistence

Remove name and uniqueness in redirect condition table
This commit is contained in:
Alejandro Celaya 2024-02-29 09:26:51 +01:00 committed by GitHub
commit a7cde9364a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 36 additions and 104 deletions

View File

@ -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"

View File

@ -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,

View File

@ -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();
}; };

View File

@ -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, [

View File

@ -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();

View File

@ -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,

View File

@ -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'];
}
} }

View File

@ -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',

View File

@ -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);