diff --git a/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php b/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php index 19790e3a..ac05ee7b 100644 --- a/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php +++ b/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php @@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\CLI\Command\Domain; use Shlinkio\Shlink\CLI\Command\Visit\VisitsCommandUtils; use Shlinkio\Shlink\CLI\Input\VisitsListInput; -use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Model\VisitsParams; @@ -37,9 +36,7 @@ class GetDomainVisitsCommand extends Command #[MapInput] VisitsListInput $input, ): int { $paginator = $this->visitsHelper->visitsForDomain($domain, new VisitsParams($input->dateRange())); - [$rows, $headers] = VisitsCommandUtils::resolveRowsAndHeaders($paginator, $this->mapExtraFields(...)); - - ShlinkTable::default($io)->render($headers, $rows); + VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...)); return self::SUCCESS; } diff --git a/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php b/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php index 54f6c019..b0e4cce0 100644 --- a/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php +++ b/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php @@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl; use Shlinkio\Shlink\CLI\Command\Visit\VisitsCommandUtils; use Shlinkio\Shlink\CLI\Input\VisitsListInput; -use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Visit\Model\VisitsParams; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; @@ -39,9 +38,8 @@ class GetShortUrlVisitsCommand extends Command $identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, $domain); $dateRange = $input->dateRange(); $paginator = $this->visitsHelper->visitsForShortUrl($identifier, new VisitsParams($dateRange)); - [$rows, $headers] = VisitsCommandUtils::resolveRowsAndHeaders($paginator); - ShlinkTable::default($io)->render($headers, $rows); + VisitsCommandUtils::renderOutput($io, $input, $paginator); return self::SUCCESS; } diff --git a/module/CLI/src/Command/Tag/GetTagVisitsCommand.php b/module/CLI/src/Command/Tag/GetTagVisitsCommand.php index 426e253e..529fb536 100644 --- a/module/CLI/src/Command/Tag/GetTagVisitsCommand.php +++ b/module/CLI/src/Command/Tag/GetTagVisitsCommand.php @@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\CLI\Command\Tag; use Shlinkio\Shlink\CLI\Command\Visit\VisitsCommandUtils; use Shlinkio\Shlink\CLI\Input\VisitsListInput; -use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\Core\Domain\Entity\Domain; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; use Shlinkio\Shlink\Core\Visit\Entity\Visit; @@ -47,9 +46,8 @@ class GetTagVisitsCommand extends Command dateRange: $input->dateRange(), domain: $domain, )); - [$rows, $headers] = VisitsCommandUtils::resolveRowsAndHeaders($paginator, $this->mapExtraFields(...)); - ShlinkTable::default($io)->render($headers, $rows); + VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...)); return self::SUCCESS; } diff --git a/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php index 3620bbd3..2ff8aa52 100644 --- a/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php +++ b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\Visit; use Shlinkio\Shlink\CLI\Input\VisitsListInput; -use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\Core\Domain\Entity\Domain; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; use Shlinkio\Shlink\Core\Visit\Entity\Visit; @@ -43,9 +42,7 @@ class GetNonOrphanVisitsCommand extends Command dateRange: $input->dateRange(), domain: $domain, )); - [$rows, $headers] = VisitsCommandUtils::resolveRowsAndHeaders($paginator, $this->mapExtraFields(...)); - - ShlinkTable::default($io)->render($headers, $rows); + VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...)); return self::SUCCESS; } diff --git a/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php b/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php index d6c12c5c..d1ce8d66 100644 --- a/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php +++ b/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\Visit; use Shlinkio\Shlink\CLI\Input\VisitsListInput; -use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\Core\Domain\Entity\Domain; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams; @@ -43,9 +42,7 @@ class GetOrphanVisitsCommand extends Command domain: $domain, type: $type, )); - [$rows, $headers] = VisitsCommandUtils::resolveRowsAndHeaders($paginator, $this->mapExtraFields(...)); - - ShlinkTable::default($io)->render($headers, $rows); + VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...)); return self::SUCCESS; } diff --git a/module/CLI/src/Command/Visit/VisitsCommandUtils.php b/module/CLI/src/Command/Visit/VisitsCommandUtils.php index 57ef2ec6..8089b02c 100644 --- a/module/CLI/src/Command/Visit/VisitsCommandUtils.php +++ b/module/CLI/src/Command/Visit/VisitsCommandUtils.php @@ -4,8 +4,13 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\Visit; +use Shlinkio\Shlink\CLI\Input\VisitsListFormat; +use Shlinkio\Shlink\CLI\Input\VisitsListInput; +use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\Common\Paginator\Paginator; +use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtils; use Shlinkio\Shlink\Core\Visit\Entity\Visit; +use Symfony\Component\Console\Output\OutputInterface; use function array_keys; use function array_map; @@ -18,14 +23,70 @@ class VisitsCommandUtils * @param Paginator $paginator * @param null|callable(Visit $visits): array $mapExtraFields */ - public static function resolveRowsAndHeaders(Paginator $paginator, callable|null $mapExtraFields = null): array + public static function renderOutput( + OutputInterface $output, + VisitsListInput $inputData, + Paginator $paginator, + callable|null $mapExtraFields = null, + ): void { + if ($inputData->format !== VisitsListFormat::FULL) { + // Avoid running out of memory by loading visits in chunks + $paginator->setMaxPerPage(1000); + } + + match ($inputData->format) { + VisitsListFormat::CSV => self::renderCSVOutput($output, $paginator, $mapExtraFields), + default => self::renderHumanFriendlyOutput($output, $paginator, $mapExtraFields), + }; + } + + /** + * @param Paginator $paginator + * @param null|callable(Visit $visits): array $mapExtraFields + */ + private static function renderCSVOutput( + OutputInterface $output, + Paginator $paginator, + callable|null $mapExtraFields, + ): void { + // TODO + } + + /** + * @param Paginator $paginator + * @param null|callable(Visit $visits): array $mapExtraFields + */ + private static function renderHumanFriendlyOutput( + OutputInterface $output, + Paginator $paginator, + callable|null $mapExtraFields, + ): void { + $page = 1; + do { + $paginator->setCurrentPage($page); + $page++; + + [$rows, $headers] = self::resolveRowsAndHeaders($paginator, $mapExtraFields); + ShlinkTable::default($output)->render( + $headers, + $rows, + footerTitle: PagerfantaUtils::formatCurrentPageMessage($paginator, 'Page %s of %s'), + ); + } while ($paginator->hasNextPage()); + } + + /** + * @param Paginator $paginator + * @param null|callable(Visit $visits): array $mapExtraFields + */ + private static function resolveRowsAndHeaders(Paginator $paginator, callable|null $mapExtraFields): array { - $extraKeys = []; + $extraKeys = null; $mapExtraFields ??= static fn (Visit $_) => []; $rows = array_map(function (Visit $visit) use (&$extraKeys, $mapExtraFields) { $extraFields = $mapExtraFields($visit); - $extraKeys = array_keys($extraFields); + $extraKeys ??= array_keys($extraFields); $rowData = [ 'referer' => $visit->referer, @@ -40,7 +101,7 @@ class VisitsCommandUtils // Filter out unknown keys return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]); }, [...$paginator->getCurrentPageResults()]); - $extra = array_map(camelCaseToHumanFriendly(...), $extraKeys); + $extra = array_map(camelCaseToHumanFriendly(...), $extraKeys ?? []); return [ $rows, diff --git a/module/CLI/src/Input/VisitsListFormat.php b/module/CLI/src/Input/VisitsListFormat.php new file mode 100644 index 00000000..acb95301 --- /dev/null +++ b/module/CLI/src/Input/VisitsListFormat.php @@ -0,0 +1,18 @@ +value . '", "' . VisitsListFormat::PAGINATED->value . '" or "' + . VisitsListFormat::CSV->value . '")', + shortcut: 'f', + )] + public VisitsListFormat $format = VisitsListFormat::FULL; + public function dateRange(): DateRange { return buildDateRange( diff --git a/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php b/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php index ddf55283..d5abbb22 100644 --- a/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php +++ b/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php @@ -61,7 +61,7 @@ class GetDomainVisitsCommandTest extends TestCase | Referer | Date | User agent | Country | City | Short Url | +---------+---------------------------+------------+---------+--------+---------------+ | foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | the_short_url | - +---------+---------------------------+------------+---------+--------+---------------+ + +---------+-------------------------- Page 1 of 1 -+---------+--------+---------------+ OUTPUT, $output, diff --git a/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php index 3fd53c48..04a081c0 100644 --- a/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php @@ -101,7 +101,7 @@ class GetShortUrlVisitsCommandTest extends TestCase | Referer | Date | User agent | Country | City | +---------+---------------------------+------------+---------+--------+ | foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | - +---------+---------------------------+------------+---------+--------+ + +---------+------------------ Page 1 of 1 ---------+---------+--------+ OUTPUT, $output, diff --git a/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php b/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php index 30580951..426ff29b 100644 --- a/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php +++ b/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php @@ -58,7 +58,7 @@ class GetTagVisitsCommandTest extends TestCase | Referer | Date | User agent | Country | City | Short Url | +---------+---------------------------+------------+---------+--------+---------------+ | foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | the_short_url | - +---------+---------------------------+------------+---------+--------+---------------+ + +---------+-------------------------- Page 1 of 1 -+---------+--------+---------------+ OUTPUT, $output, diff --git a/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php b/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php index f583d063..b6e973a9 100644 --- a/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php +++ b/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php @@ -57,7 +57,7 @@ class GetNonOrphanVisitsCommandTest extends TestCase | Referer | Date | User agent | Country | City | Short Url | +---------+---------------------------+------------+---------+--------+---------------+ | foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | the_short_url | - +---------+---------------------------+------------+---------+--------+---------------+ + +---------+-------------------------- Page 1 of 1 -+---------+--------+---------------+ OUTPUT, $output, diff --git a/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php b/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php index bf453c65..1633eee8 100644 --- a/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php +++ b/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php @@ -55,7 +55,7 @@ class GetOrphanVisitsCommandTest extends TestCase | Referer | Date | User agent | Country | City | Type | +---------+---------------------------+------------+---------+--------+----------+ | foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | base_url | - +---------+---------------------------+------------+---------+--------+----------+ + +---------+----------------------- Page 1 of 1 ----+---------+--------+----------+ OUTPUT, $output,