Merge pull request #2452 from acelaya-forks/feature/invokable-command-poc

Use invokable commands approach on some API console commands
This commit is contained in:
Alejandro Celaya
2025-06-26 08:46:20 +02:00
committed by GitHub
6 changed files with 82 additions and 103 deletions

View File

@@ -54,11 +54,11 @@
"spiral/roadrunner-cli": "^2.7",
"spiral/roadrunner-http": "^3.5",
"spiral/roadrunner-jobs": "^4.6",
"symfony/console": "^7.2",
"symfony/filesystem": "^7.2",
"symfony/console": "^7.3",
"symfony/filesystem": "^7.3",
"symfony/lock": "7.1.6",
"symfony/process": "^7.2",
"symfony/string": "^7.2"
"symfony/process": "^7.3",
"symfony/string": "^7.3"
},
"require-dev": {
"devizzent/cebe-php-openapi": "^1.1.2",
@@ -73,7 +73,7 @@
"roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.4.2",
"shlinkio/shlink-test-utils": "^4.3.1",
"symfony/var-dumper": "^7.2",
"symfony/var-dumper": "^7.3",
"veewee/composer-run-parallel": "^1.4"
},
"conflict": {

View File

@@ -7,16 +7,40 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
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\InputArgument;
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 Shlinkio\Shlink\Core\ArrayUtils\map;
use function sprintf;
#[AsCommand(
name: DisableKeyCommand::NAME,
description: 'Disables an API key by name or plain-text key (providing a plain-text key is DEPRECATED)',
help: <<<HELP
The <info>%command.name%</info> command allows you to disable an existing API key, via its name or the
plain-text key.
If no arguments are provided, you will be prompted to select one of the existing non-disabled API keys.
<info>%command.full_name%</info>
You can optionally pass the API key name to be disabled. In that case <comment>--by-name</comment> is also
required, to indicate the first argument is the API key name and not the plain-text key:
<info>%command.full_name% the_key_name --by-name</info>
You can pass the plain-text key to be disabled, but that is <options=bold>DEPRECATED</>. In next major version,
the argument will always be assumed to be the name:
<info>%command.full_name% d6b6c60e-edcd-4e43-96ad-fa6b7014c143</info>
HELP,
)]
class DisableKeyCommand extends Command
{
public const string NAME = 'api-key:disable';
@@ -26,47 +50,9 @@ class DisableKeyCommand extends Command
parent::__construct();
}
protected function configure(): void
{
$help = <<<HELP
The <info>%command.name%</info> command allows you to disable an existing API key, via its name or the
plain-text key.
If no arguments are provided, you will be prompted to select one of the existing non-disabled API keys.
<info>%command.full_name%</info>
You can optionally pass the API key name to be disabled. In that case <comment>--by-name</comment> is also
required, to indicate the first argument is the API key name and not the plain-text key:
<info>%command.full_name% the_key_name --by-name</info>
You can pass the plain-text key to be disabled, but that is <options=bold>DEPRECATED</>. In next major version,
the argument will always be assumed to be the name:
<info>%command.full_name% d6b6c60e-edcd-4e43-96ad-fa6b7014c143</info>
HELP;
$this
->setName(self::NAME)
->setDescription('Disables an API key by name or plain-text key (providing a plain-text key is DEPRECATED)')
->addArgument(
'keyOrName',
InputArgument::OPTIONAL,
'The API key to disable. Pass `--by-name` to indicate this value is the name and not the key.',
)
->addOption(
'by-name',
mode: InputOption::VALUE_NONE,
description: 'Indicates the first argument is the API key name, not the plain-text key.',
)
->setHelp($help);
}
protected function interact(InputInterface $input, OutputInterface $output): void
{
$keyOrName = $input->getArgument('keyOrName');
$keyOrName = $input->getArgument('key-or-name');
if ($keyOrName === null) {
$apiKeys = $this->apiKeyService->listKeys(enabledOnly: true);
@@ -75,18 +61,21 @@ class DisableKeyCommand extends Command
map($apiKeys, static fn (ApiKey $apiKey) => $apiKey->name),
);
$input->setArgument('keyOrName', $name);
$input->setArgument('key-or-name', $name);
$input->setOption('by-name', true);
}
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$keyOrName = $input->getArgument('keyOrName');
$byName = $input->getOption('by-name');
$io = new SymfonyStyle($input, $output);
if (! $keyOrName) {
public function __invoke(
SymfonyStyle $io,
#[Argument(
description: 'The API key to disable. Pass `--by-name` to indicate this value is the name and not the key.',
)]
string|null $keyOrName = null,
#[Option(description: 'Indicates the first argument is the API key name, not the plain-text key.')]
bool $byName = false,
): int {
if ($keyOrName === null) {
$io->warning('An API key name was not provided.');
return Command::INVALID;
}

View File

@@ -8,16 +8,20 @@ use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
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 array_filter;
use function array_map;
use function implode;
use function sprintf;
#[AsCommand(
name: ListKeysCommand::NAME,
description: 'Lists all the available API keys.',
)]
class ListKeysCommand extends Command
{
private const string ERROR_STRING_PATTERN = '<fg=red>%s</>';
@@ -31,23 +35,14 @@ class ListKeysCommand extends Command
parent::__construct();
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('Lists all the available API keys.')
->addOption(
'enabled-only',
'e',
InputOption::VALUE_NONE,
'Tells if only enabled API keys should be returned.',
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$enabledOnly = $input->getOption('enabled-only');
public function __invoke(
SymfonyStyle $io,
#[Option(
description: 'Tells if only enabled API keys should be returned.',
shortcut: 'e',
)]
bool $enabledOnly = false,
): int {
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
$expiration = $apiKey->expirationDate;
$messagePattern = $this->determineMessagePattern($apiKey);
@@ -65,7 +60,7 @@ class ListKeysCommand extends Command
return $rowData;
}, $this->apiKeyService->listKeys($enabledOnly));
ShlinkTable::withRowSeparators($output)->render(array_filter([
ShlinkTable::withRowSeparators($io)->render(array_filter([
'Name',
! $enabledOnly ? 'Is enabled' : null,
'Expiration date',

View File

@@ -8,14 +8,19 @@ use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\Renaming;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function Shlinkio\Shlink\Core\ArrayUtils\map;
#[AsCommand(
name: RenameApiKeyCommand::NAME,
description: 'Renames an API key by name',
)]
class RenameApiKeyCommand extends Command
{
public const string NAME = 'api-key:rename';
@@ -25,20 +30,11 @@ class RenameApiKeyCommand extends Command
parent::__construct();
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('Renames an API key by name')
->addArgument('oldName', InputArgument::REQUIRED, 'Current name of the API key to rename')
->addArgument('newName', InputArgument::REQUIRED, 'New name to set to the API key');
}
protected function interact(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output);
$oldName = $input->getArgument('oldName');
$newName = $input->getArgument('newName');
$oldName = $input->getArgument('old-name');
$newName = $input->getArgument('new-name');
if ($oldName === null) {
$apiKeys = $this->apiKeyService->listKeys();
@@ -47,7 +43,7 @@ class RenameApiKeyCommand extends Command
map($apiKeys, static fn (ApiKey $apiKey) => $apiKey->name),
);
$input->setArgument('oldName', $requestedOldName);
$input->setArgument('old-name', $requestedOldName);
}
if ($newName === null) {
@@ -58,16 +54,15 @@ class RenameApiKeyCommand extends Command
: throw new InvalidArgumentException('The new name cannot be empty'),
);
$input->setArgument('newName', $requestedNewName);
$input->setArgument('new-name', $requestedNewName);
}
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$oldName = $input->getArgument('oldName');
$newName = $input->getArgument('newName');
public function __invoke(
SymfonyStyle $io,
#[Argument(description: 'Current name of the API key to rename')] string $oldName,
#[Argument(description: 'New name to set to the API key')] string $newName,
): int {
$this->apiKeyService->renameApiKey(Renaming::fromNames($oldName, $newName));
$io->success('API key properly renamed');

View File

@@ -35,7 +35,7 @@ class DisableKeyCommandTest extends TestCase
$this->apiKeyService->expects($this->never())->method('disableByName');
$exitCode = $this->commandTester->execute([
'keyOrName' => $apiKey,
'key-or-name' => $apiKey,
]);
$output = $this->commandTester->getDisplay();
@@ -51,7 +51,7 @@ class DisableKeyCommandTest extends TestCase
$this->apiKeyService->expects($this->never())->method('disableByKey');
$exitCode = $this->commandTester->execute([
'keyOrName' => $name,
'key-or-name' => $name,
'--by-name' => true,
]);
$output = $this->commandTester->getDisplay();
@@ -71,7 +71,7 @@ class DisableKeyCommandTest extends TestCase
$this->apiKeyService->expects($this->never())->method('disableByName');
$exitCode = $this->commandTester->execute([
'keyOrName' => $apiKey,
'key-or-name' => $apiKey,
]);
$output = $this->commandTester->getDisplay();
@@ -90,7 +90,7 @@ class DisableKeyCommandTest extends TestCase
$this->apiKeyService->expects($this->never())->method('disableByKey');
$exitCode = $this->commandTester->execute([
'keyOrName' => $name,
'key-or-name' => $name,
'--by-name' => true,
]);
$output = $this->commandTester->getDisplay();

View File

@@ -43,7 +43,7 @@ class RenameApiKeyCommandTest extends TestCase
$this->commandTester->setInputs([$oldName]);
$this->commandTester->execute([
'newName' => $newName,
'new-name' => $newName,
]);
}
@@ -60,7 +60,7 @@ class RenameApiKeyCommandTest extends TestCase
$this->commandTester->setInputs([$newName]);
$this->commandTester->execute([
'oldName' => $oldName,
'old-name' => $oldName,
]);
}
@@ -76,8 +76,8 @@ class RenameApiKeyCommandTest extends TestCase
);
$this->commandTester->execute([
'oldName' => $oldName,
'newName' => $newName,
'old-name' => $oldName,
'new-name' => $newName,
]);
}
}