From a45550b0c60fba317e638cceaaeab2b65c5c35f0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 3 Mar 2024 09:48:56 +0100 Subject: [PATCH] Extract logic to determine list of rules from ManageRedirectRulesCommand to a helper service --- module/CLI/config/dependencies.config.php | 6 +- .../ManageRedirectRulesCommand.php | 216 +---------------- module/CLI/src/Input/DateOption.php | 10 +- .../src/RedirectRule/RedirectRuleHandler.php | 221 ++++++++++++++++++ .../RedirectRuleHandlerInterface.php | 20 ++ 5 files changed, 251 insertions(+), 222 deletions(-) create mode 100644 module/CLI/src/RedirectRule/RedirectRuleHandler.php create mode 100644 module/CLI/src/RedirectRule/RedirectRuleHandlerInterface.php diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index c2b61e19..0c709788 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -11,7 +11,7 @@ use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory; use Shlinkio\Shlink\Core\Domain\DomainService; use Shlinkio\Shlink\Core\Options\TrackingOptions; use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; -use Shlinkio\Shlink\Core\RedirectRule; +use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleService; use Shlinkio\Shlink\Core\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier; use Shlinkio\Shlink\Core\Tag\TagService; @@ -34,6 +34,7 @@ return [ PhpExecutableFinder::class => InvokableFactory::class, GeoLite\GeolocationDbUpdater::class => ConfigAbstractFactory::class, + RedirectRule\RedirectRuleHandler::class => InvokableFactory::class, Util\ProcessRunner::class => ConfigAbstractFactory::class, ApiKey\RoleResolver::class => ConfigAbstractFactory::class, @@ -122,7 +123,8 @@ return [ Command\RedirectRule\ManageRedirectRulesCommand::class => [ ShortUrl\ShortUrlResolver::class, - RedirectRule\ShortUrlRedirectRuleService::class, + ShortUrlRedirectRuleService::class, + RedirectRule\RedirectRuleHandler::class, ], Command\Db\CreateDatabaseCommand::class => [ diff --git a/module/CLI/src/Command/RedirectRule/ManageRedirectRulesCommand.php b/module/CLI/src/Command/RedirectRule/ManageRedirectRulesCommand.php index ee8dc328..e36fcf59 100644 --- a/module/CLI/src/Command/RedirectRule/ManageRedirectRulesCommand.php +++ b/module/CLI/src/Command/RedirectRule/ManageRedirectRulesCommand.php @@ -4,18 +4,11 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\RedirectRule; -use Doctrine\Common\Collections\ArrayCollection; +use Shlinkio\Shlink\CLI\RedirectRule\RedirectRuleHandlerInterface; use Shlinkio\Shlink\CLI\Util\ExitCode; -use Shlinkio\Shlink\Core\Exception\InvalidArgumentException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; -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\ShortUrlRedirectRuleServiceInterface; -use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -24,22 +17,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use function array_flip; -use function array_slice; -use function array_values; -use function count; -use function implode; -use function is_numeric; -use function max; -use function min; -use function Shlinkio\Shlink\Core\ArrayUtils\map; -use function Shlinkio\Shlink\Core\enumValues; use function sprintf; -use function str_pad; -use function strlen; -use function trim; - -use const STR_PAD_LEFT; class ManageRedirectRulesCommand extends Command { @@ -48,6 +26,7 @@ class ManageRedirectRulesCommand extends Command public function __construct( protected readonly ShortUrlResolverInterface $shortUrlResolver, protected readonly ShortUrlRedirectRuleServiceInterface $ruleService, + protected readonly RedirectRuleHandlerInterface $ruleHandler, ) { parent::__construct(); } @@ -73,7 +52,7 @@ class ManageRedirectRulesCommand extends Command return ExitCode::EXIT_FAILURE; } - $rulesToSave = $this->processRules($shortUrl, $io, $this->ruleService->rulesForShortUrl($shortUrl)); + $rulesToSave = $this->ruleHandler->manageRules($io, $shortUrl, $this->ruleService->rulesForShortUrl($shortUrl)); if ($rulesToSave !== null) { $this->ruleService->saveRulesForShortUrl($shortUrl, $rulesToSave); $io->success('Rules properly saved'); @@ -81,193 +60,4 @@ class ManageRedirectRulesCommand extends Command return ExitCode::EXIT_SUCCESS; } - - /** - * @param ShortUrlRedirectRule[] $rules - * @return ShortUrlRedirectRule[]|null - */ - private function processRules(ShortUrl $shortUrl, SymfonyStyle $io, array $rules): ?array - { - $amountOfRules = count($rules); - - if ($amountOfRules === 0) { - $io->comment('No rules found.'); - } else { - $listing = map( - $rules, - function (ShortUrlRedirectRule $rule, string|int|float $index) use ($amountOfRules): array { - $priority = ((int) $index) + 1; - $conditions = $rule->mapConditions(static fn (RedirectCondition $condition): string => sprintf( - '%s', - $condition->toHumanFriendly(), - )); - - return [ - str_pad((string) $priority, strlen((string) $amountOfRules), '0', STR_PAD_LEFT), - implode(' AND ', $conditions), - $rule->longUrl, - ]; - }, - ); - $io->table(['Priority', 'Conditions', 'Redirect to'], $listing); - } - - $action = $io->choice( - 'What do you want to do next?', - [ - 'Add new rule', - 'Remove existing rule', - 'Re-arrange rule', - 'Discard changes', - 'Save and exit', - ], - 'Save and exit', - ); - - return match ($action) { - 'Add new rule' => $this->processRules($shortUrl, $io, $this->addRule($shortUrl, $io, $rules)), - 'Remove existing rule' => $this->processRules($shortUrl, $io, $this->removeRule($io, $rules)), - 'Re-arrange rule' => $this->processRules($shortUrl, $io, $this->reArrangeRule($io, $rules)), - 'Save and exit' => $rules, - default => null, - }; - } - - /** - * @param ShortUrlRedirectRule[] $currentRules - */ - private function addRule(ShortUrl $shortUrl, SymfonyStyle $io, array $currentRules): array - { - $higherPriority = count($currentRules); - $priority = $this->askPriority($io, $higherPriority + 1); - $longUrl = $this->askLongUrl($io); - $conditions = []; - - do { - $type = RedirectConditionType::from( - $io->choice('Type of the condition?', enumValues(RedirectConditionType::class)), - ); - $conditions[] = match ($type) { - RedirectConditionType::DEVICE => RedirectCondition::forDevice( - DeviceType::from($io->choice('Device to match?', enumValues(DeviceType::class))), - ), - RedirectConditionType::LANGUAGE => RedirectCondition::forLanguage( - $this->askMandatory('Language to match?', $io), - ), - RedirectConditionType::QUERY_PARAM => RedirectCondition::forQueryParam( - $this->askMandatory('Query param name?', $io), - $this->askOptional('Query param value?', $io), - ), - }; - - $continue = $io->confirm('Do you want to add another condition?'); - } while ($continue); - - $newRule = new ShortUrlRedirectRule($shortUrl, $priority, $longUrl, new ArrayCollection($conditions)); - $rulesBefore = array_slice($currentRules, 0, $priority - 1); - $rulesAfter = array_slice($currentRules, $priority - 1); - - return [...$rulesBefore, $newRule, ...$rulesAfter]; - } - - /** - * @param ShortUrlRedirectRule[] $currentRules - */ - private function removeRule(SymfonyStyle $io, array $currentRules): array - { - if (empty($currentRules)) { - $io->warning('There are no rules to remove'); - return $currentRules; - } - - $index = $this->askRule('What rule do you want to delete?', $io, $currentRules); - unset($currentRules[$index]); - return array_values($currentRules); - } - - /** - * @param ShortUrlRedirectRule[] $currentRules - */ - private function reArrangeRule(SymfonyStyle $io, array $currentRules): array - { - if (empty($currentRules)) { - $io->warning('There are no rules to re-arrange'); - return $currentRules; - } - - $oldIndex = $this->askRule('What rule do you want to re-arrange?', $io, $currentRules); - $newIndex = $this->askPriority($io, count($currentRules)) - 1; - - // Temporarily get rule from array and unset it - $rule = $currentRules[$oldIndex]; - unset($currentRules[$oldIndex]); - - // Reindex remaining rules - $currentRules = array_values($currentRules); - - $rulesBefore = array_slice($currentRules, 0, $newIndex); - $rulesAfter = array_slice($currentRules, $newIndex); - - return [...$rulesBefore, $rule, ...$rulesAfter]; - } - - /** - * @param ShortUrlRedirectRule[] $currentRules - */ - private function askRule(string $message, SymfonyStyle $io, array $currentRules): int - { - $choices = []; - foreach ($currentRules as $index => $rule) { - $choices[$rule->longUrl] = $index + 1; - } - - $resp = $io->choice($message, array_flip($choices)); - return $choices[$resp] - 1; - } - - private function askPriority(SymfonyStyle $io, int $max): int - { - return $io->ask( - 'Rule priority (the lower the value, the higher the priority)', - (string) $max, - function (string $answer) use ($max): int { - if (! is_numeric($answer)) { - throw new InvalidArgumentException('The priority must be a numeric positive value'); - } - - $priority = (int) $answer; - return max(1, min($max, $priority)); - }, - ); - } - - private function askLongUrl(SymfonyStyle $io): string - { - return $io->ask( - 'Long URL to redirect when the rule matches', - validator: function (string $answer): string { - $validator = ShortUrlInputFilter::longUrlValidators(); - if (! $validator->isValid($answer)) { - throw new InvalidArgumentException(implode(', ', $validator->getMessages())); - } - - return $answer; - }, - ); - } - - private function askMandatory(string $message, SymfonyStyle $io): string - { - return $io->ask($message, validator: function (?string $answer): string { - if ($answer === null) { - throw new InvalidArgumentException('The value is mandatory'); - } - return trim($answer); - }); - } - - private function askOptional(string $message, SymfonyStyle $io): string - { - return $io->ask($message, validator: fn (?string $answer) => $answer === null ? '' : trim($answer)); - } } diff --git a/module/CLI/src/Input/DateOption.php b/module/CLI/src/Input/DateOption.php index 41407d23..6183a6c5 100644 --- a/module/CLI/src/Input/DateOption.php +++ b/module/CLI/src/Input/DateOption.php @@ -14,14 +14,10 @@ use Throwable; use function is_string; use function sprintf; -class DateOption +readonly class DateOption { - public function __construct( - private readonly Command $command, - private readonly string $name, - string $shortcut, - string $description, - ) { + public function __construct(private Command $command, private string $name, string $shortcut, string $description) + { $command->addOption($name, $shortcut, InputOption::VALUE_REQUIRED, $description); } diff --git a/module/CLI/src/RedirectRule/RedirectRuleHandler.php b/module/CLI/src/RedirectRule/RedirectRuleHandler.php new file mode 100644 index 00000000..8b1592b7 --- /dev/null +++ b/module/CLI/src/RedirectRule/RedirectRuleHandler.php @@ -0,0 +1,221 @@ +newLine(); + $io->text('// No rules found.'); + } else { + $listing = map( + $rules, + function (ShortUrlRedirectRule $rule, string|int|float $index) use ($amountOfRules): array { + $priority = ((int) $index) + 1; + $conditions = $rule->mapConditions(static fn (RedirectCondition $condition): string => sprintf( + '%s', + $condition->toHumanFriendly(), + )); + + return [ + str_pad((string) $priority, strlen((string) $amountOfRules), '0', STR_PAD_LEFT), + implode(' AND ', $conditions), + $rule->longUrl, + ]; + }, + ); + $io->table(['Priority', 'Conditions', 'Redirect to'], $listing); + } + + $action = $io->choice( + 'What do you want to do next?', + [ + 'Add new rule', + 'Remove existing rule', + 'Re-arrange rule', + 'Discard changes', + 'Save and exit', + ], + 'Save and exit', + ); + + return match ($action) { + 'Add new rule' => $this->manageRules($io, $shortUrl, $this->addRule($shortUrl, $io, $rules)), + 'Remove existing rule' => $this->manageRules($io, $shortUrl, $this->removeRule($io, $rules)), + 'Re-arrange rule' => $this->manageRules($io, $shortUrl, $this->reArrangeRule($io, $rules)), + 'Save and exit' => $rules, + default => null, + }; + } + + /** + * @param ShortUrlRedirectRule[] $currentRules + */ + private function addRule(ShortUrl $shortUrl, StyleInterface $io, array $currentRules): array + { + $higherPriority = count($currentRules); + $priority = $this->askPriority($io, $higherPriority + 1); + $longUrl = $this->askLongUrl($io); + $conditions = []; + + do { + $type = RedirectConditionType::from( + $io->choice('Type of the condition?', enumValues(RedirectConditionType::class)), + ); + $conditions[] = match ($type) { + RedirectConditionType::DEVICE => RedirectCondition::forDevice( + DeviceType::from($io->choice('Device to match?', enumValues(DeviceType::class))), + ), + RedirectConditionType::LANGUAGE => RedirectCondition::forLanguage( + $this->askMandatory('Language to match?', $io), + ), + RedirectConditionType::QUERY_PARAM => RedirectCondition::forQueryParam( + $this->askMandatory('Query param name?', $io), + $this->askOptional('Query param value?', $io), + ), + }; + + $continue = $io->confirm('Do you want to add another condition?'); + } while ($continue); + + $newRule = new ShortUrlRedirectRule($shortUrl, $priority, $longUrl, new ArrayCollection($conditions)); + $rulesBefore = array_slice($currentRules, 0, $priority - 1); + $rulesAfter = array_slice($currentRules, $priority - 1); + + return [...$rulesBefore, $newRule, ...$rulesAfter]; + } + + /** + * @param ShortUrlRedirectRule[] $currentRules + */ + private function removeRule(StyleInterface $io, array $currentRules): array + { + if (empty($currentRules)) { + $io->warning('There are no rules to remove'); + return $currentRules; + } + + $index = $this->askRule('What rule do you want to delete?', $io, $currentRules); + unset($currentRules[$index]); + return array_values($currentRules); + } + + /** + * @param ShortUrlRedirectRule[] $currentRules + */ + private function reArrangeRule(StyleInterface $io, array $currentRules): array + { + if (empty($currentRules)) { + $io->warning('There are no rules to re-arrange'); + return $currentRules; + } + + $oldIndex = $this->askRule('What rule do you want to re-arrange?', $io, $currentRules); + $newIndex = $this->askPriority($io, count($currentRules)) - 1; + + // Temporarily get rule from array and unset it + $rule = $currentRules[$oldIndex]; + unset($currentRules[$oldIndex]); + + // Reindex remaining rules + $currentRules = array_values($currentRules); + + $rulesBefore = array_slice($currentRules, 0, $newIndex); + $rulesAfter = array_slice($currentRules, $newIndex); + + return [...$rulesBefore, $rule, ...$rulesAfter]; + } + + /** + * @param ShortUrlRedirectRule[] $currentRules + */ + private function askRule(string $message, StyleInterface $io, array $currentRules): int + { + $choices = []; + foreach ($currentRules as $index => $rule) { + $choices[$rule->longUrl] = $index + 1; + } + + $resp = $io->choice($message, array_flip($choices)); + return $choices[$resp] - 1; + } + + private function askPriority(StyleInterface $io, int $max): int + { + return $io->ask( + 'Rule priority (the lower the value, the higher the priority)', + (string) $max, + function (string $answer) use ($max): int { + if (! is_numeric($answer)) { + throw new InvalidArgumentException('The priority must be a numeric positive value'); + } + + $priority = (int) $answer; + return max(1, min($max, $priority)); + }, + ); + } + + private function askLongUrl(StyleInterface $io): string + { + return $io->ask( + 'Long URL to redirect when the rule matches', + validator: function (string $answer): string { + $validator = ShortUrlInputFilter::longUrlValidators(); + if (! $validator->isValid($answer)) { + throw new InvalidArgumentException(implode(', ', $validator->getMessages())); + } + + return $answer; + }, + ); + } + + private function askMandatory(string $message, StyleInterface $io): string + { + return $io->ask($message, validator: function (?string $answer): string { + if ($answer === null) { + throw new InvalidArgumentException('The value is mandatory'); + } + return trim($answer); + }); + } + + private function askOptional(string $message, StyleInterface $io): string + { + return $io->ask($message, validator: fn (?string $answer) => $answer === null ? '' : trim($answer)); + } +} diff --git a/module/CLI/src/RedirectRule/RedirectRuleHandlerInterface.php b/module/CLI/src/RedirectRule/RedirectRuleHandlerInterface.php new file mode 100644 index 00000000..16022768 --- /dev/null +++ b/module/CLI/src/RedirectRule/RedirectRuleHandlerInterface.php @@ -0,0 +1,20 @@ +