mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-11 17:44:44 +08:00
Create endpoint to set redirect rules for a short URL
This commit is contained in:
@@ -36,6 +36,11 @@ class ShortUrlRedirectRule extends AbstractEntity implements JsonSerializable
|
||||
);
|
||||
}
|
||||
|
||||
public function clearConditions(): void
|
||||
{
|
||||
$this->conditions->clear();
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
|
||||
25
module/Core/src/RedirectRule/Model/RedirectRulesData.php
Normal file
25
module/Core/src/RedirectRule/Model/RedirectRulesData.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\RedirectRule\Model;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Model\Validation\RedirectRulesInputFilter;
|
||||
|
||||
readonly class RedirectRulesData
|
||||
{
|
||||
private function __construct(public array $rules)
|
||||
{
|
||||
}
|
||||
|
||||
public static function fromRawData(array $rawData): self
|
||||
{
|
||||
$inputFilter = RedirectRulesInputFilter::initialize($rawData);
|
||||
if (! $inputFilter->isValid()) {
|
||||
throw ValidationException::fromInputFilter($inputFilter);
|
||||
}
|
||||
|
||||
return new self($inputFilter->getValue(RedirectRulesInputFilter::REDIRECT_RULES));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\RedirectRule\Model\Validation;
|
||||
|
||||
use Laminas\InputFilter\CollectionInputFilter;
|
||||
use Laminas\InputFilter\InputFilter;
|
||||
use Laminas\Validator\Callback;
|
||||
use Laminas\Validator\InArray;
|
||||
use Shlinkio\Shlink\Common\Validation\InputFactory;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectConditionType;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
class RedirectRulesInputFilter extends InputFilter
|
||||
{
|
||||
public const REDIRECT_RULES = 'redirectRules';
|
||||
|
||||
public const RULE_PRIORITY = 'priority';
|
||||
public const RULE_LONG_URL = 'longUrl';
|
||||
public const RULE_CONDITIONS = 'conditions';
|
||||
|
||||
public const CONDITION_TYPE = 'type';
|
||||
public const CONDITION_MATCH_VALUE = 'matchValue';
|
||||
public const CONDITION_MATCH_KEY = 'matchKey';
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function initialize(array $rawData): self
|
||||
{
|
||||
$redirectRulesInputFilter = new CollectionInputFilter();
|
||||
$redirectRulesInputFilter->setInputFilter(self::createRedirectRuleInputFilter());
|
||||
|
||||
$instance = new self();
|
||||
$instance->add($redirectRulesInputFilter, self::REDIRECT_RULES);
|
||||
|
||||
$instance->setData($rawData);
|
||||
return $instance;
|
||||
}
|
||||
|
||||
private static function createRedirectRuleInputFilter(): InputFilter
|
||||
{
|
||||
$redirectRuleInputFilter = new InputFilter();
|
||||
|
||||
$redirectRuleInputFilter->add(InputFactory::numeric(self::RULE_PRIORITY, required: true));
|
||||
|
||||
$longUrl = InputFactory::basic(self::RULE_LONG_URL, required: true);
|
||||
$longUrl->setValidatorChain(ShortUrlInputFilter::longUrlValidators());
|
||||
$redirectRuleInputFilter->add($longUrl);
|
||||
|
||||
$conditionsInputFilter = new CollectionInputFilter();
|
||||
$conditionsInputFilter->setInputFilter(self::createRedirectConditionInputFilter())
|
||||
->setIsRequired(true);
|
||||
$redirectRuleInputFilter->add($conditionsInputFilter, self::RULE_CONDITIONS);
|
||||
|
||||
return $redirectRuleInputFilter;
|
||||
}
|
||||
|
||||
private static function createRedirectConditionInputFilter(): InputFilter
|
||||
{
|
||||
$redirectConditionInputFilter = new InputFilter();
|
||||
|
||||
$type = InputFactory::basic(self::CONDITION_TYPE, required: true);
|
||||
$type->getValidatorChain()->attach(new InArray([
|
||||
'haystack' => enumValues(RedirectConditionType::class),
|
||||
'strict' => InArray::COMPARE_STRICT,
|
||||
]));
|
||||
$redirectConditionInputFilter->add($type);
|
||||
|
||||
$value = InputFactory::basic(self::CONDITION_MATCH_VALUE, required: true);
|
||||
$value->getValidatorChain()->attach(new Callback(function (string $value, array $context) {
|
||||
if ($context[self::CONDITION_TYPE] === RedirectConditionType::DEVICE->value) {
|
||||
return contains($value, enumValues(DeviceType::class));
|
||||
}
|
||||
|
||||
return true;
|
||||
}));
|
||||
$redirectConditionInputFilter->add($value);
|
||||
|
||||
$redirectConditionInputFilter->add(
|
||||
InputFactory::basic(self::CONDITION_MATCH_KEY, required: true)->setAllowEmpty(true),
|
||||
);
|
||||
|
||||
return $redirectConditionInputFilter;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,18 @@
|
||||
|
||||
namespace Shlinkio\Shlink\Core\RedirectRule;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Entity\RedirectCondition;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectConditionType;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectRulesData;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Model\Validation\RedirectRulesInputFilter;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
|
||||
use function array_map;
|
||||
|
||||
readonly class ShortUrlRedirectRuleService implements ShortUrlRedirectRuleServiceInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em)
|
||||
@@ -22,4 +30,52 @@ readonly class ShortUrlRedirectRuleService implements ShortUrlRedirectRuleServic
|
||||
orderBy: ['priority' => 'ASC'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ShortUrlRedirectRule[]
|
||||
*/
|
||||
public function setRulesForShortUrl(ShortUrl $shortUrl, RedirectRulesData $data): array
|
||||
{
|
||||
return $this->em->wrapInTransaction(function () use ($shortUrl, $data): array {
|
||||
// First, delete existing rules for the short URL
|
||||
$oldRules = $this->rulesForShortUrl($shortUrl);
|
||||
foreach ($oldRules as $oldRule) {
|
||||
$oldRule->clearConditions(); // This will trigger the orphan removal of old conditions
|
||||
$this->em->remove($oldRule);
|
||||
}
|
||||
$this->em->flush();
|
||||
|
||||
// Then insert new rules
|
||||
$rules = [];
|
||||
foreach ($data->rules as $rule) {
|
||||
$rule = new ShortUrlRedirectRule(
|
||||
shortUrl: $shortUrl,
|
||||
priority: $rule[RedirectRulesInputFilter::RULE_PRIORITY],
|
||||
longUrl: $rule[RedirectRulesInputFilter::RULE_LONG_URL],
|
||||
conditions: new ArrayCollection(array_map(
|
||||
fn (array $conditionData) => $this->createCondition($conditionData),
|
||||
$rule[RedirectRulesInputFilter::RULE_CONDITIONS],
|
||||
)),
|
||||
);
|
||||
|
||||
$rules[] = $rule;
|
||||
$this->em->persist($rule);
|
||||
}
|
||||
|
||||
return $rules;
|
||||
});
|
||||
}
|
||||
|
||||
private function createCondition(array $rawConditionData): RedirectCondition
|
||||
{
|
||||
$type = RedirectConditionType::from($rawConditionData[RedirectRulesInputFilter::CONDITION_TYPE]);
|
||||
$value = $rawConditionData[RedirectRulesInputFilter::CONDITION_MATCH_VALUE];
|
||||
$key = $rawConditionData[RedirectRulesInputFilter::CONDITION_MATCH_KEY];
|
||||
|
||||
return match ($type) {
|
||||
RedirectConditionType::DEVICE => RedirectCondition::forDevice(DeviceType::from($value)),
|
||||
RedirectConditionType::LANGUAGE => RedirectCondition::forLanguage($value),
|
||||
RedirectConditionType::QUERY_PARAM => RedirectCondition::forQueryParam($key, $value),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Shlinkio\Shlink\Core\RedirectRule;
|
||||
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectRulesData;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
|
||||
interface ShortUrlRedirectRuleServiceInterface
|
||||
@@ -11,4 +12,9 @@ interface ShortUrlRedirectRuleServiceInterface
|
||||
* @return ShortUrlRedirectRule[]
|
||||
*/
|
||||
public function rulesForShortUrl(ShortUrl $shortUrl): array;
|
||||
|
||||
/**
|
||||
* @return ShortUrlRedirectRule[]
|
||||
*/
|
||||
public function setRulesForShortUrl(ShortUrl $shortUrl, RedirectRulesData $data): array;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ class ShortUrlInputFilter extends InputFilter
|
||||
private function initializeForEdition(bool $requireLongUrl = false): void
|
||||
{
|
||||
$longUrlInput = InputFactory::basic(self::LONG_URL, required: $requireLongUrl);
|
||||
$longUrlInput->getValidatorChain()->merge($this->longUrlValidators());
|
||||
$longUrlInput->getValidatorChain()->merge(self::longUrlValidators(allowNull: ! $requireLongUrl));
|
||||
$this->add($longUrlInput);
|
||||
|
||||
$validSince = InputFactory::basic(self::VALID_SINCE);
|
||||
@@ -124,7 +124,7 @@ class ShortUrlInputFilter extends InputFilter
|
||||
$this->add($apiKeyInput);
|
||||
}
|
||||
|
||||
private function longUrlValidators(bool $allowNull = false): Validator\ValidatorChain
|
||||
public static function longUrlValidators(bool $allowNull = false): Validator\ValidatorChain
|
||||
{
|
||||
$emptyModifiers = [
|
||||
Validator\NotEmpty::OBJECT,
|
||||
|
||||
Reference in New Issue
Block a user