Merge pull request #809 from acelaya-forks/feature/list-all-command

Feature/list all command
This commit is contained in:
Alejandro Celaya 2020-07-14 15:50:29 +02:00 committed by GitHub
commit 8a811c5b33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 80 additions and 23 deletions

View file

@ -18,6 +18,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
* [#734](https://github.com/shlinkio/shlink/issues/734) Added support to redirect to deeplinks and other links with schemas different from `http` and `https`.
* [#709](https://github.com/shlinkio/shlink/issues/709) Added multi-architecture builds for the docker image.
* [#707](https://github.com/shlinkio/shlink/issues/707) Added `--all` flag to `short-urls:list` command, which will print all existing URLs in one go, with no pagination.
It has one limitation, though. Because of the way the CLI tooling works, all rows in the table must be loaded in memory. If the amount of URLs is too high, the command may fail due to too much memory usage.
#### Changed
* [#508](https://github.com/shlinkio/shlink/issues/508) Added mutation checks to database tests.

View file

@ -11,7 +11,6 @@ use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
@ -61,7 +60,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
'page',
'p',
InputOption::VALUE_REQUIRED,
sprintf('The first page to list (%s items per page)', ShortUrlRepositoryAdapter::ITEMS_PER_PAGE),
'The first page to list (10 items per page unless "--all" is provided)',
'1',
)
->addOption(
@ -82,7 +81,14 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
InputOption::VALUE_REQUIRED,
'The field from which we want to order by. Pass ASC or DESC separated by a comma',
)
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not');
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags 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 getStartDateDesc(): string
@ -104,24 +110,32 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$tags = $input->getOption('tags');
$tags = ! empty($tags) ? explode(',', $tags) : [];
$showTags = (bool) $input->getOption('showTags');
$all = (bool) $input->getOption('all');
$startDate = $this->getDateOption($input, $output, 'startDate');
$endDate = $this->getDateOption($input, $output, 'endDate');
$orderBy = $this->processOrderBy($input);
$data = [
ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
ShortUrlsParamsInputFilter::TAGS => $tags,
ShortUrlsOrdering::ORDER_BY => $orderBy,
ShortUrlsParamsInputFilter::START_DATE => $startDate !== null ? $startDate->toAtomString() : null,
ShortUrlsParamsInputFilter::END_DATE => $endDate !== null ? $endDate->toAtomString() : null,
];
if ($all) {
$data[ShortUrlsParamsInputFilter::ITEMS_PER_PAGE] = -1;
}
do {
$result = $this->renderPage($output, $showTags, ShortUrlsParams::fromRawData([
ShortUrlsParamsInputFilter::PAGE => $page,
ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
ShortUrlsParamsInputFilter::TAGS => $tags,
ShortUrlsOrdering::ORDER_BY => $orderBy,
ShortUrlsParamsInputFilter::START_DATE => $startDate !== null ? $startDate->toAtomString() : null,
ShortUrlsParamsInputFilter::END_DATE => $endDate !== null ? $endDate->toAtomString() : null,
]));
$data[ShortUrlsParamsInputFilter::PAGE] = $page;
$result = $this->renderPage($output, $showTags, ShortUrlsParams::fromRawData($data), $all);
$page++;
$continue = $this->isLastPage($result)
? false
: $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
$continue = ! $this->isLastPage($result) && $io->confirm(
sprintf('Continue with page <options=bold>%s</>?', $page),
false,
);
} while ($continue);
$io->newLine();
@ -130,7 +144,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
return ExitCodes::EXIT_SUCCESS;
}
private function renderPage(OutputInterface $output, bool $showTags, ShortUrlsParams $params): Paginator
private function renderPage(OutputInterface $output, bool $showTags, ShortUrlsParams $params, bool $all): Paginator
{
$result = $this->shortUrlService->listShortUrls($params);
@ -151,7 +165,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$rows[] = array_values(array_intersect_key($shortUrl, array_flip(self::COLUMNS_WHITELIST)));
}
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
ShlinkTable::fromOutput($output)->render($headers, $rows, $all ? null : $this->formatCurrentPageMessage(
$result,
'Page %s of %s',
));

View file

@ -192,4 +192,22 @@ class ListShortUrlsCommandTest extends TestCase
yield [['--orderBy' => 'foo,ASC'], ['foo' => 'ASC']];
yield [['--orderBy' => 'bar,DESC'], ['bar' => 'DESC']];
}
/** @test */
public function requestingAllElementsWillSetItemsPerPage(): void
{
$listShortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData([
'page' => 1,
'searchTerm' => null,
'tags' => [],
'startDate' => null,
'endDate' => null,
'orderBy' => null,
'itemsPerPage' => -1,
]))->willReturn(new Paginator(new ArrayAdapter()));
$this->commandTester->execute(['--all' => true]);
$listShortUrls->shouldHaveBeenCalledOnce();
}
}

View file

@ -12,11 +12,14 @@ use function Shlinkio\Shlink\Core\parseDateField;
final class ShortUrlsParams
{
public const DEFAULT_ITEMS_PER_PAGE = 10;
private int $page;
private ?string $searchTerm;
private array $tags;
private ShortUrlsOrdering $orderBy;
private ?DateRange $dateRange;
private ?int $itemsPerPage = null;
private function __construct()
{
@ -56,6 +59,9 @@ final class ShortUrlsParams
parseDateField($inputFilter->getValue(ShortUrlsParamsInputFilter::END_DATE)),
);
$this->orderBy = ShortUrlsOrdering::fromRawData($query);
$this->itemsPerPage = (int) (
$inputFilter->getValue(ShortUrlsParamsInputFilter::ITEMS_PER_PAGE) ?? self::DEFAULT_ITEMS_PER_PAGE
);
}
public function page(): int
@ -63,6 +69,11 @@ final class ShortUrlsParams
return $this->page;
}
public function itemsPerPage(): int
{
return $this->itemsPerPage;
}
public function searchTerm(): ?string
{
return $this->searchTerm;

View file

@ -10,8 +10,6 @@ use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
class ShortUrlRepositoryAdapter implements AdapterInterface
{
public const ITEMS_PER_PAGE = 10;
private ShortUrlRepositoryInterface $repository;
private ShortUrlsParams $params;

View file

@ -44,7 +44,7 @@ class ShortUrlService implements ShortUrlServiceInterface
/** @var ShortUrlRepository $repo */
$repo = $this->em->getRepository(ShortUrl::class);
$paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $params));
$paginator->setItemCountPerPage(ShortUrlRepositoryAdapter::ITEMS_PER_PAGE)
$paginator->setItemCountPerPage($params->itemsPerPage())
->setCurrentPageNumber($params->page());
return $paginator;

View file

@ -5,10 +5,13 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Validation;
use Laminas\Filter;
use Laminas\InputFilter\Input;
use Laminas\InputFilter\InputFilter;
use Laminas\Validator;
use Shlinkio\Shlink\Common\Validation;
use function is_numeric;
class ShortUrlsParamsInputFilter extends InputFilter
{
use Validation\InputFactoryTrait;
@ -18,6 +21,7 @@ class ShortUrlsParamsInputFilter extends InputFilter
public const TAGS = 'tags';
public const START_DATE = 'startDate';
public const END_DATE = 'endDate';
public const ITEMS_PER_PAGE = 'itemsPerPage';
public function __construct(array $data)
{
@ -32,14 +36,22 @@ class ShortUrlsParamsInputFilter extends InputFilter
$this->add($this->createInput(self::SEARCH_TERM, false));
$page = $this->createInput(self::PAGE, false);
$page->getValidatorChain()->attach(new Validator\Digits())
->attach(new Validator\GreaterThan(['min' => 1, 'inclusive' => true]));
$this->add($page);
$this->add($this->createNumericInput(self::PAGE, 1));
$tags = $this->createArrayInput(self::TAGS, false);
$tags->getFilterChain()->attach(new Filter\StringToLower())
->attach(new Filter\PregReplace(['pattern' => '/ /', 'replacement' => '-']));
$this->add($tags);
$this->add($this->createNumericInput(self::ITEMS_PER_PAGE, -1));
}
private function createNumericInput(string $name, int $min): Input
{
$input = $this->createInput($name, false);
$input->getValidatorChain()->attach(new Validator\Callback(fn ($value) => is_numeric($value)))
->attach(new Validator\GreaterThan(['min' => $min, 'inclusive' => true]));
return $input;
}
}