diff --git a/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php index e6a11ea1..c3b4ba0c 100644 --- a/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php @@ -4,53 +4,40 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\ShortUrl; -use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput; use Shlinkio\Shlink\Core\Exception; use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier; +use Symfony\Component\Console\Attribute\Argument; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use function sprintf; +#[AsCommand(name: DeleteShortUrlCommand::NAME, description: 'Deletes a short URL')] class DeleteShortUrlCommand extends Command { public const string NAME = 'short-url:delete'; - private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput; - public function __construct(private readonly DeleteShortUrlServiceInterface $deleteShortUrlService) { parent::__construct(); - $this->shortUrlIdentifierInput = new ShortUrlIdentifierInput( - $this, - shortCodeDesc: 'The short code for the short URL to be deleted', - domainDesc: 'The domain if the short code does not belong to the default one', - ); } - protected function configure(): void - { - $this - ->setName(self::NAME) - ->setDescription('Deletes a short URL') - ->addOption( - 'ignore-threshold', - 'i', - InputOption::VALUE_NONE, - 'Ignores the safety visits threshold check, which could make short URLs with many visits to be ' - . 'accidentally deleted', - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - $identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input); - $ignoreThreshold = $input->getOption('ignore-threshold'); + public function __invoke( + SymfonyStyle $io, + #[Argument('The short code for the short URL to be deleted')] string $shortCode, + #[Option('The domain if the short code does not belong to the default one', shortcut: 'd')] + string|null $domain = null, + #[Option( + 'Ignores the safety visits threshold check, which could make short URLs with many visits to be ' + . 'accidentally deleted', + shortcut: 'i', + )] + bool $ignoreThreshold = false, + ): int { + $identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, $domain); try { $this->runDelete($io, $identifier, $ignoreThreshold); diff --git a/module/CLI/src/Command/ShortUrl/DeleteShortUrlVisitsCommand.php b/module/CLI/src/Command/ShortUrl/DeleteShortUrlVisitsCommand.php index 4c238f31..16667eb3 100644 --- a/module/CLI/src/Command/ShortUrl/DeleteShortUrlVisitsCommand.php +++ b/module/CLI/src/Command/ShortUrl/DeleteShortUrlVisitsCommand.php @@ -4,41 +4,44 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\ShortUrl; -use Shlinkio\Shlink\CLI\Command\Visit\AbstractDeleteVisitsCommand; -use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput; +use Shlinkio\Shlink\CLI\Command\Util\CommandUtils; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; +use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface; -use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Attribute\Argument; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Style\SymfonyStyle; use function sprintf; -class DeleteShortUrlVisitsCommand extends AbstractDeleteVisitsCommand +#[AsCommand(DeleteShortUrlVisitsCommand::NAME, 'Deletes visits from a short URL')] +class DeleteShortUrlVisitsCommand extends Command { public const string NAME = 'short-url:visits-delete'; - private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput; - public function __construct(private readonly ShortUrlVisitsDeleterInterface $deleter) { parent::__construct(); - $this->shortUrlIdentifierInput = new ShortUrlIdentifierInput( - $this, - shortCodeDesc: 'The short code for the short URL which visits will be deleted', - domainDesc: 'The domain if the short code does not belong to the default one', + } + + public function __invoke( + SymfonyStyle $io, + #[Argument('The short code for the short URL which visits will be deleted')] string $shortCode, + #[Option('The domain if the short code does not belong to the default one', shortcut: 'd')] + string|null $domain = null, + ): int { + $identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, $domain); + return CommandUtils::executeWithWarning( + 'You are about to delete all visits for a short URL. This operation cannot be undone', + $io, + fn () => $this->deleteVisits($io, $identifier), ); } - protected function configure(): void + private function deleteVisits(SymfonyStyle $io, ShortUrlIdentifier $identifier): int { - $this - ->setName(self::NAME) - ->setDescription('Deletes visits from a short URL'); - } - - protected function doExecute(InputInterface $input, SymfonyStyle $io): int - { - $identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input); try { $result = $this->deleter->deleteShortUrlVisits($identifier); $io->success(sprintf('Successfully deleted %s visits', $result->affectedItems)); @@ -49,9 +52,4 @@ class DeleteShortUrlVisitsCommand extends AbstractDeleteVisitsCommand return self::INVALID; } } - - protected function getWarningMessage(): string - { - return 'You are about to delete all visits for a short URL. This operation cannot be undone.'; - } } diff --git a/module/CLI/src/Command/ShortUrl/EditShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/EditShortUrlCommand.php index 7bdd82e2..f4e3b06e 100644 --- a/module/CLI/src/Command/ShortUrl/EditShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/EditShortUrlCommand.php @@ -36,8 +36,8 @@ class EditShortUrlCommand extends Command public function __invoke( SymfonyStyle $io, - #[Argument('The short code to edit')] string $shortCode, #[MapInput] ShortUrlDataInput $data, + #[Argument('The short code to edit')] string $shortCode, #[Option('The domain to which the short URL is attached', shortcut: 'd')] string|null $domain = null, #[Option('The long URL to set', shortcut: 'l')] string|null $longUrl = null, ): int { diff --git a/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php b/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php index b6bf71f7..b70ba70e 100644 --- a/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php @@ -4,60 +4,42 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\ShortUrl; -use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; +use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface; +use Symfony\Component\Console\Attribute\Argument; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Ask; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use function sprintf; +#[AsCommand(ResolveUrlCommand::NAME, 'Returns the long URL behind a short code')] class ResolveUrlCommand extends Command { - public const string NAME = 'short-url:parse'; - - private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput; + public const string NAME = 'short-url:resolve'; public function __construct(private readonly ShortUrlResolverInterface $urlResolver) { parent::__construct(); - $this->shortUrlIdentifierInput = new ShortUrlIdentifierInput( - $this, - shortCodeDesc: 'The short code to parse', - domainDesc: 'The domain to which the short URL is attached.', - ); } - protected function configure(): void - { - $this - ->setName(self::NAME) - ->setDescription('Returns the long URL behind a short code'); - } - - protected function interact(InputInterface $input, OutputInterface $output): void - { - $shortCode = $this->shortUrlIdentifierInput->shortCode($input); - if (! empty($shortCode)) { - return; - } - - $io = new SymfonyStyle($input, $output); - $shortCode = $io->ask('A short code was not provided. Which short code do you want to parse?'); - if (! empty($shortCode)) { - $input->setArgument('shortCode', $shortCode); - } - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); + public function __invoke( + SymfonyStyle $io, + #[ + Argument('The short code to resolve'), + Ask('A short code was not provided. Which short code do you want to resolve?'), + ] + string $shortCode, + #[Option('The domain to which the short URL is attached', shortcut: 'd')] string|null $domain = null, + ): int { + $identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, $domain); try { - $url = $this->urlResolver->resolveShortUrl($this->shortUrlIdentifierInput->toShortUrlIdentifier($input)); - $output->writeln(sprintf('Long URL: %s', $url->getLongUrl())); + $url = $this->urlResolver->resolveShortUrl($identifier); + $io->writeln(sprintf('Long URL: %s', $url->getLongUrl())); return self::SUCCESS; } catch (ShortUrlNotFoundException $e) { $io->error($e->getMessage()); diff --git a/module/CLI/src/Command/Util/CommandUtils.php b/module/CLI/src/Command/Util/CommandUtils.php new file mode 100644 index 00000000..76085f1a --- /dev/null +++ b/module/CLI/src/Command/Util/CommandUtils.php @@ -0,0 +1,28 @@ +warning($warning); + if (! $io->confirm('Do you want to proceed?', default: false)) { + $io->info('Operation aborted'); + return Command::SUCCESS; + } + + return $callback(); + } +} diff --git a/module/CLI/src/Command/Visit/AbstractDeleteVisitsCommand.php b/module/CLI/src/Command/Visit/AbstractDeleteVisitsCommand.php deleted file mode 100644 index d8ef98e3..00000000 --- a/module/CLI/src/Command/Visit/AbstractDeleteVisitsCommand.php +++ /dev/null @@ -1,34 +0,0 @@ -confirm($io)) { - $io->info('Operation aborted'); - return self::SUCCESS; - } - - return $this->doExecute($input, $io); - } - - private function confirm(SymfonyStyle $io): bool - { - $io->warning($this->getWarningMessage()); - return $io->confirm('Continue deleting visits?', false); - } - - abstract protected function doExecute(InputInterface $input, SymfonyStyle $io): int; - - abstract protected function getWarningMessage(): string; -} diff --git a/module/CLI/src/Command/Visit/DeleteOrphanVisitsCommand.php b/module/CLI/src/Command/Visit/DeleteOrphanVisitsCommand.php index 77fefaaa..654b92bf 100644 --- a/module/CLI/src/Command/Visit/DeleteOrphanVisitsCommand.php +++ b/module/CLI/src/Command/Visit/DeleteOrphanVisitsCommand.php @@ -4,13 +4,16 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\Visit; +use Shlinkio\Shlink\CLI\Command\Util\CommandUtils; use Shlinkio\Shlink\Core\Visit\VisitsDeleterInterface; -use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Style\SymfonyStyle; use function sprintf; -class DeleteOrphanVisitsCommand extends AbstractDeleteVisitsCommand +#[AsCommand(DeleteOrphanVisitsCommand::NAME, 'Deletes all orphan visits')] +class DeleteOrphanVisitsCommand extends Command { public const string NAME = 'visit:orphan-delete'; @@ -19,23 +22,20 @@ class DeleteOrphanVisitsCommand extends AbstractDeleteVisitsCommand parent::__construct(); } - protected function configure(): void + public function __invoke(SymfonyStyle $io): int { - $this - ->setName(self::NAME) - ->setDescription('Deletes all orphan visits'); + return CommandUtils::executeWithWarning( + 'You are about to delete all orphan visits. This operation cannot be undone', + $io, + fn () => $this->deleteVisits($io), + ); } - protected function doExecute(InputInterface $input, SymfonyStyle $io): int + private function deleteVisits(SymfonyStyle $io): int { $result = $this->deleter->deleteOrphanVisits(); $io->success(sprintf('Successfully deleted %s visits', $result->affectedItems)); return self::SUCCESS; } - - protected function getWarningMessage(): string - { - return 'You are about to delete all orphan visits. This operation cannot be undone.'; - } } diff --git a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php index 62872123..c8d94388 100644 --- a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php @@ -39,7 +39,7 @@ class DeleteShortUrlCommandTest extends TestCase $this->isFalse(), ); - $this->commandTester->execute(['shortCode' => $shortCode]); + $this->commandTester->execute(['short-code' => $shortCode]); $output = $this->commandTester->getDisplay(); self::assertStringContainsString( @@ -58,7 +58,7 @@ class DeleteShortUrlCommandTest extends TestCase $this->isFalse(), )->willThrowException(Exception\ShortUrlNotFoundException::fromNotFound($identifier)); - $this->commandTester->execute(['shortCode' => $shortCode]); + $this->commandTester->execute(['short-code' => $shortCode]); $output = $this->commandTester->getDisplay(); self::assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output); @@ -88,7 +88,7 @@ class DeleteShortUrlCommandTest extends TestCase }); $this->commandTester->setInputs($retryAnswer); - $this->commandTester->execute(['shortCode' => $shortCode]); + $this->commandTester->execute(['short-code' => $shortCode]); $output = $this->commandTester->getDisplay(); self::assertStringContainsString(sprintf( @@ -118,7 +118,7 @@ class DeleteShortUrlCommandTest extends TestCase )); $this->commandTester->setInputs(['no']); - $this->commandTester->execute(['shortCode' => $shortCode]); + $this->commandTester->execute(['short-code' => $shortCode]); $output = $this->commandTester->getDisplay(); self::assertStringContainsString(sprintf( diff --git a/module/CLI/test/Command/ShortUrl/DeleteShortUrlVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/DeleteShortUrlVisitsCommandTest.php index 3efa94b5..6f066d0f 100644 --- a/module/CLI/test/Command/ShortUrl/DeleteShortUrlVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/DeleteShortUrlVisitsCommandTest.php @@ -36,7 +36,7 @@ class DeleteShortUrlVisitsCommandTest extends TestCase $this->deleter->expects($this->never())->method('deleteShortUrlVisits'); $this->commandTester->setInputs($input); - $exitCode = $this->commandTester->execute(['shortCode' => 'foo']); + $exitCode = $this->commandTester->execute(['short-code' => 'foo']); $output = $this->commandTester->getDisplay(); self::assertEquals(Command::SUCCESS, $exitCode); @@ -67,8 +67,8 @@ class DeleteShortUrlVisitsCommandTest extends TestCase public static function provideErrorArgs(): iterable { - yield 'domain' => [['shortCode' => 'foo'], 'Short URL not found for "foo"']; - yield 'no domain' => [['shortCode' => 'foo', '--domain' => 's.test'], 'Short URL not found for "s.test/foo"']; + yield 'domain' => [['short-code' => 'foo'], 'Short URL not found for "foo"']; + yield 'no domain' => [['short-code' => 'foo', '--domain' => 's.test'], 'Short URL not found for "s.test/foo"']; } #[Test] @@ -77,7 +77,7 @@ class DeleteShortUrlVisitsCommandTest extends TestCase $this->deleter->expects($this->once())->method('deleteShortUrlVisits')->willReturn(new BulkDeleteResult(5)); $this->commandTester->setInputs(['yes']); - $exitCode = $this->commandTester->execute(['shortCode' => 'foo']); + $exitCode = $this->commandTester->execute(['short-code' => 'foo']); $output = $this->commandTester->getDisplay(); self::assertEquals(Command::SUCCESS, $exitCode); diff --git a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php index 742ae05c..30060dd6 100644 --- a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php @@ -40,7 +40,7 @@ class ResolveUrlCommandTest extends TestCase ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), )->willReturn($shortUrl); - $this->commandTester->execute(['shortCode' => $shortCode]); + $this->commandTester->execute(['short-code' => $shortCode]); $output = $this->commandTester->getDisplay(); self::assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output); } @@ -68,7 +68,7 @@ class ResolveUrlCommandTest extends TestCase ShortUrlNotFoundException::fromNotFound($identifier), ); - $this->commandTester->execute(['shortCode' => $shortCode]); + $this->commandTester->execute(['short-code' => $shortCode]); $output = $this->commandTester->getDisplay(); self::assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output); }