mirror of
https://github.com/shlinkio/shlink.git
synced 2025-12-15 14:55:04 -06:00
Migrate ListShortUrlsCommand to symfony/console attributes
This commit is contained in:
parent
89419e278c
commit
a75ee138e1
116
module/CLI/src/Command/ShortUrl/Input/ShortUrlsParamsInput.php
Normal file
116
module/CLI/src/Command/ShortUrl/Input/ShortUrlsParamsInput.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl\Input;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||
use Shlinkio\Shlink\CLI\Input\InputUtils;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlsParamsInputFilter;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function array_unique;
|
||||
|
||||
/**
|
||||
* Input arguments and options for short-url:list command
|
||||
* @see ListShortUrlsCommand
|
||||
*/
|
||||
final class ShortUrlsParamsInput
|
||||
{
|
||||
#[Option('The first page to list (10 items per page unless "--all" is provided).', shortcut: 'p')]
|
||||
public int $page = 1;
|
||||
|
||||
#[Option(
|
||||
'Disables pagination and just displays all existing URLs. Caution! If the amount of short URLs is big,this '
|
||||
. 'may end up failing due to memory usage.',
|
||||
)]
|
||||
public bool $all = false;
|
||||
|
||||
#[Option('Only return short URLs older than this date', shortcut: 's')]
|
||||
public string|null $startDate = null;
|
||||
|
||||
#[Option('Only return short URLs newer than this date', shortcut: 'e')]
|
||||
public string|null $endDate = null;
|
||||
|
||||
#[Option('A query used to filter results by searching for it on the longUrl and shortCode fields', shortcut: 'st')]
|
||||
public string|null $searchTerm = null;
|
||||
|
||||
#[Option(
|
||||
'Used to filter results by domain. Use ' . Domain::DEFAULT_AUTHORITY . ' keyword to filter by default domain',
|
||||
shortcut: 'd',
|
||||
)]
|
||||
public string|null $domain = null;
|
||||
|
||||
/** @var string[] */
|
||||
#[Option('A list of tags that short URLs need to include', name: 'tag', shortcut: 't')]
|
||||
public array $tags = [];
|
||||
|
||||
#[Option('If --tag is provided, returns only short URLs including ALL of them')]
|
||||
public bool $tagsAll = false;
|
||||
|
||||
/** @var string[] */
|
||||
#[Option('A list of tags that short URLs should NOT include', name: 'exclude-tag', shortcut: 'et')]
|
||||
public array $excludeTags = [];
|
||||
|
||||
#[Option('If --exclude-tag is provided, returns only short URLs not including ANY of them')]
|
||||
public bool $excludeTagsAll = false;
|
||||
|
||||
#[Option('Excludes short URLs which reached their max amount of visits')]
|
||||
public bool $excludeMaxVisitsReached = false;
|
||||
|
||||
#[Option('Excludes short URLs which have a "validUntil" date in the past')]
|
||||
public bool $excludePastValidUntil = false;
|
||||
|
||||
#[Option(
|
||||
'Field name to order the list by. Set the dir by optionally passing ASC or DESC after "-": --orderBy=tags-ASC',
|
||||
shortcut: 'o',
|
||||
)]
|
||||
public string|null $orderBy = null;
|
||||
|
||||
#[Option('List only short URLs created by the API key matching provided name', shortcut: 'kn')]
|
||||
public string|null $apiKeyName = null;
|
||||
|
||||
#[Option('Whether to display the tags or not')]
|
||||
public bool $showTags = false;
|
||||
|
||||
#[Option(
|
||||
'Whether to display the domain or not. Those belonging to default domain will have value '
|
||||
. '"' . Domain::DEFAULT_AUTHORITY . '"',
|
||||
)]
|
||||
public bool $showDomain = false;
|
||||
|
||||
#[Option('Whether to display the API key name from which the URL was generated or not', shortcut: 'k')]
|
||||
public bool $showApiKey = false;
|
||||
|
||||
public function toArray(OutputInterface $output): array
|
||||
{
|
||||
$tagsMode = $this->tagsAll ? TagsMode::ALL->value : TagsMode::ANY->value;
|
||||
$excludeTagsMode = $this->excludeTagsAll ? TagsMode::ALL->value : TagsMode::ANY->value;
|
||||
|
||||
$data = [
|
||||
ShortUrlsParamsInputFilter::PAGE => $this->page,
|
||||
ShortUrlsParamsInputFilter::SEARCH_TERM => $this->searchTerm,
|
||||
ShortUrlsParamsInputFilter::DOMAIN => $this->domain,
|
||||
ShortUrlsParamsInputFilter::TAGS => array_unique($this->tags),
|
||||
ShortUrlsParamsInputFilter::TAGS_MODE => $tagsMode,
|
||||
ShortUrlsParamsInputFilter::EXCLUDE_TAGS => array_unique($this->excludeTags),
|
||||
ShortUrlsParamsInputFilter::EXCLUDE_TAGS_MODE => $excludeTagsMode,
|
||||
ShortUrlsParamsInputFilter::ORDER_BY => $this->orderBy,
|
||||
ShortUrlsParamsInputFilter::START_DATE => InputUtils::processDate('start-date', $this->startDate, $output),
|
||||
ShortUrlsParamsInputFilter::END_DATE => InputUtils::processDate('end-date', $this->endDate, $output),
|
||||
ShortUrlsParamsInputFilter::EXCLUDE_MAX_VISITS_REACHED => $this->excludeMaxVisitsReached,
|
||||
ShortUrlsParamsInputFilter::EXCLUDE_PAST_VALID_UNTIL => $this->excludePastValidUntil,
|
||||
ShortUrlsParamsInputFilter::API_KEY_NAME => $this->apiKeyName,
|
||||
];
|
||||
|
||||
if ($this->all) {
|
||||
$data[ShortUrlsParamsInputFilter::ITEMS_PER_PAGE] = Paginator::ALL_ITEMS;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@ -4,9 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Input\EndDateOption;
|
||||
use Shlinkio\Shlink\CLI\Input\StartDateOption;
|
||||
use Shlinkio\Shlink\CLI\Input\TagsOption;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\Input\ShortUrlsParamsInput;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtils;
|
||||
@ -14,165 +12,43 @@ use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlWithDeps;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlsParamsInputFilter;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Attribute\MapInput;
|
||||
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_keys;
|
||||
use function array_pad;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\map;
|
||||
use function sprintf;
|
||||
|
||||
#[AsCommand(name: ListShortUrlsCommand::NAME, description: 'List all short URLs')]
|
||||
class ListShortUrlsCommand extends Command
|
||||
{
|
||||
public const string NAME = 'short-url:list';
|
||||
|
||||
private readonly StartDateOption $startDateOption;
|
||||
private readonly EndDateOption $endDateOption;
|
||||
private readonly TagsOption $tagsOption;
|
||||
|
||||
public function __construct(
|
||||
private readonly ShortUrlListServiceInterface $shortUrlService,
|
||||
private readonly ShortUrlDataTransformerInterface $transformer,
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->startDateOption = new StartDateOption($this, 'short URLs');
|
||||
$this->endDateOption = new EndDateOption($this, 'short URLs');
|
||||
$this->tagsOption = new TagsOption($this, 'A list of tags that short URLs need to include.');
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('List all short URLs')
|
||||
->addOption(
|
||||
'page',
|
||||
'p',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The first page to list (10 items per page unless "--all" is provided).',
|
||||
'1',
|
||||
)
|
||||
->addOption(
|
||||
'search-term',
|
||||
'st',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields.',
|
||||
)
|
||||
->addOption(
|
||||
'domain',
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Used to filter results by domain. Use DEFAULT keyword to filter by default domain',
|
||||
)
|
||||
->addOption(
|
||||
'tags-all',
|
||||
mode: InputOption::VALUE_NONE,
|
||||
description: 'If --tag is provided, returns only short URLs including ALL of them',
|
||||
)
|
||||
->addOption(
|
||||
'exclude-tag',
|
||||
'et',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'A list of tags that short URLs should not have.',
|
||||
)
|
||||
->addOption(
|
||||
'exclude-tags-all',
|
||||
mode: InputOption::VALUE_NONE,
|
||||
description: 'If --exclude-tag is provided, returns only short URLs not including ANY of them',
|
||||
)
|
||||
->addOption(
|
||||
'exclude-max-visits-reached',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Excludes short URLs which reached their max amount of visits.',
|
||||
)
|
||||
->addOption(
|
||||
'exclude-past-valid-until',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Excludes short URLs which have a "validUntil" date in the past.',
|
||||
)
|
||||
->addOption(
|
||||
'order-by',
|
||||
'o',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The field from which you want to order by. '
|
||||
. 'Define ordering dir by passing ASC or DESC after "-" or ",".',
|
||||
)
|
||||
->addOption(
|
||||
'api-key-name',
|
||||
'kn',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'List only short URLs created by the API key matching provided name.',
|
||||
)
|
||||
->addOption(
|
||||
'show-tags',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Whether to display the tags or not.',
|
||||
)
|
||||
->addOption(
|
||||
'show-domain',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Whether to display the domain or not. Those belonging to default domain will have value "DEFAULT".',
|
||||
)
|
||||
->addOption(
|
||||
'show-api-key',
|
||||
'k',
|
||||
InputOption::VALUE_NONE,
|
||||
'Whether to display the API key name from which the URL was generated or not.',
|
||||
)
|
||||
->addOption(
|
||||
'all',
|
||||
'a',
|
||||
InputOption::VALUE_NONE,
|
||||
'Disables pagination and just displays all existing URLs. Caution! If the amount of short URLs is big,'
|
||||
. ' this may end up failing due to memory usage.',
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$page = (int) $input->getOption('page');
|
||||
$tagsMode = $input->getOption('tags-all') === true ? TagsMode::ALL->value : TagsMode::ANY->value;
|
||||
$excludeTagsMode = $input->getOption('exclude-tags-all') === true ? TagsMode::ALL->value : TagsMode::ANY->value;
|
||||
|
||||
$data = [
|
||||
ShortUrlsParamsInputFilter::SEARCH_TERM => $input->getOption('search-term'),
|
||||
ShortUrlsParamsInputFilter::DOMAIN => $input->getOption('domain'),
|
||||
ShortUrlsParamsInputFilter::TAGS => $this->tagsOption->get($input),
|
||||
ShortUrlsParamsInputFilter::TAGS_MODE => $tagsMode,
|
||||
ShortUrlsParamsInputFilter::EXCLUDE_TAGS => $input->getOption('exclude-tag'),
|
||||
ShortUrlsParamsInputFilter::EXCLUDE_TAGS_MODE => $excludeTagsMode,
|
||||
ShortUrlsParamsInputFilter::ORDER_BY => $this->processOrderBy($input),
|
||||
ShortUrlsParamsInputFilter::START_DATE => $this->startDateOption->get($input, $output)?->toAtomString(),
|
||||
ShortUrlsParamsInputFilter::END_DATE => $this->endDateOption->get($input, $output)?->toAtomString(),
|
||||
ShortUrlsParamsInputFilter::EXCLUDE_MAX_VISITS_REACHED => $input->getOption('exclude-max-visits-reached'),
|
||||
ShortUrlsParamsInputFilter::EXCLUDE_PAST_VALID_UNTIL => $input->getOption('exclude-past-valid-until'),
|
||||
ShortUrlsParamsInputFilter::API_KEY_NAME => $input->getOption('api-key-name'),
|
||||
];
|
||||
|
||||
$all = $input->getOption('all');
|
||||
if ($all) {
|
||||
$data[ShortUrlsParamsInputFilter::ITEMS_PER_PAGE] = Paginator::ALL_ITEMS;
|
||||
}
|
||||
public function __invoke(
|
||||
SymfonyStyle $io,
|
||||
InputInterface $input,
|
||||
#[MapInput] ShortUrlsParamsInput $paramsInput,
|
||||
): int {
|
||||
$page = $paramsInput->page;
|
||||
$data = $paramsInput->toArray($io);
|
||||
|
||||
$columnsMap = $this->resolveColumnsMap($input);
|
||||
do {
|
||||
$data[ShortUrlsParamsInputFilter::PAGE] = $page;
|
||||
$result = $this->renderPage($output, $columnsMap, ShortUrlsParams::fromRawData($data), $all);
|
||||
$result = $this->renderPage($io, $columnsMap, ShortUrlsParams::fromRawData($data), $paramsInput->all);
|
||||
$page++;
|
||||
|
||||
$continue = $result->hasNextPage() && $io->confirm(
|
||||
@ -213,17 +89,6 @@ class ListShortUrlsCommand extends Command
|
||||
return $shortUrls;
|
||||
}
|
||||
|
||||
private function processOrderBy(InputInterface $input): string|null
|
||||
{
|
||||
$orderBy = $input->getOption('order-by');
|
||||
if (empty($orderBy)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$field, $dir] = array_pad(explode(',', $orderBy), 2, null);
|
||||
return $dir === null ? $field : sprintf('%s-%s', $field, $dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, callable(array $serializedShortUrl, ShortUrl $shortUrl): ?string>
|
||||
*/
|
||||
|
||||
@ -12,6 +12,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Throwable;
|
||||
|
||||
use function is_string;
|
||||
use function Shlinkio\Shlink\Core\normalizeOptionalDate;
|
||||
use function sprintf;
|
||||
|
||||
readonly class DateOption
|
||||
@ -29,7 +30,7 @@ readonly class DateOption
|
||||
}
|
||||
|
||||
try {
|
||||
return Chronos::parse($value);
|
||||
return normalizeOptionalDate($value);
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln(sprintf(
|
||||
'<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>',
|
||||
|
||||
37
module/CLI/src/Input/InputUtils.php
Normal file
37
module/CLI/src/Input/InputUtils.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Throwable;
|
||||
|
||||
use function Shlinkio\Shlink\Core\normalizeOptionalDate;
|
||||
use function sprintf;
|
||||
|
||||
final class InputUtils
|
||||
{
|
||||
/**
|
||||
* Process a date provided via input params, and format it as ATOM.
|
||||
* A warning is printed if the date cannot be parsed, returning `null` in that case.
|
||||
*/
|
||||
public static function processDate(string $name, string|null $value, OutputInterface $output): string|null
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return normalizeOptionalDate($value)->toAtomString();
|
||||
} catch (Throwable) {
|
||||
$output->writeln(sprintf(
|
||||
'<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>',
|
||||
$name,
|
||||
$value,
|
||||
));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -306,8 +306,6 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
{
|
||||
yield [[], null];
|
||||
yield [['--order-by' => 'visits'], 'visits'];
|
||||
yield [['--order-by' => 'longUrl,ASC'], 'longUrl-ASC'];
|
||||
yield [['--order-by' => 'shortCode,DESC'], 'shortCode-DESC'];
|
||||
yield [['--order-by' => 'title-DESC'], 'title-DESC'];
|
||||
}
|
||||
|
||||
|
||||
47
module/CLI/test/Input/InputUtilsTest.php
Normal file
47
module/CLI/test/Input/InputUtilsTest.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Input;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\TestWith;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Input\InputUtils;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class InputUtilsTest extends TestCase
|
||||
{
|
||||
private MockObject & OutputInterface $input;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->input = $this->createMock(OutputInterface::class);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestWith([null], 'null')]
|
||||
#[TestWith([''], 'empty string')]
|
||||
public function processDateReturnsNullForEmptyDates(string|null $date): void
|
||||
{
|
||||
self::assertNull(InputUtils::processDate('name', $date, $this->input));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function processDateReturnsAtomFormatedForValidDates(): void
|
||||
{
|
||||
$date = '2025-01-20';
|
||||
self::assertEquals(Chronos::parse($date)->toAtomString(), InputUtils::processDate('name', $date, $this->input));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function warningIsPrintedWhenDateIsInvalid(): void
|
||||
{
|
||||
$this->input->expects($this->once())->method('writeln')->with(
|
||||
'<comment>> Ignored provided "name" since its value "invalid" is not a valid date. <</comment>',
|
||||
);
|
||||
self::assertNull(InputUtils::processDate('name', 'invalid', $this->input));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user