diff --git a/composer.json b/composer.json index eeda898b..8d82756f 100644 --- a/composer.json +++ b/composer.json @@ -44,8 +44,8 @@ "shlinkio/shlink-common": "dev-main#f2550b5 as 7.3.0", "shlinkio/shlink-config": "dev-main#fb186e4 as 4.1.0", "shlinkio/shlink-event-dispatcher": "dev-main#54d4701 as 4.4.0", - "shlinkio/shlink-importer": "dev-main#4498f0a as 5.7.0", - "shlinkio/shlink-installer": "dev-develop#40e08cb as 10.0.0", + "shlinkio/shlink-importer": "dev-main#af03f6b as 5.7.0", + "shlinkio/shlink-installer": "dev-develop#a225b16 as 10.0.0", "shlinkio/shlink-ip-geolocation": "dev-main#e0c45b2 as 5.0.0", "shlinkio/shlink-json": "dev-main#7c096d6 as 1.3.0", "spiral/roadrunner": "^2025.1", diff --git a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php index 5916fc52..0bce0feb 100644 --- a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php +++ b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php @@ -15,11 +15,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use function array_keys; -use function array_map; use function Shlinkio\Shlink\Common\buildDateRange; -use function Shlinkio\Shlink\Core\ArrayUtils\select_keys; -use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly; abstract class AbstractVisitsListCommand extends Command { @@ -38,44 +34,13 @@ abstract class AbstractVisitsListCommand extends Command $startDate = $this->startDateOption->get($input, $output); $endDate = $this->endDateOption->get($input, $output); $paginator = $this->getVisitsPaginator($input, buildDateRange($startDate, $endDate)); - [$rows, $headers] = $this->resolveRowsAndHeaders($paginator); + [$rows, $headers] = VisitsCommandUtils::resolveRowsAndHeaders($paginator, $this->mapExtraFields(...)); ShlinkTable::default($output)->render($headers, $rows); return self::SUCCESS; } - /** - * @param Paginator $paginator - */ - private function resolveRowsAndHeaders(Paginator $paginator): array - { - $extraKeys = []; - $rows = array_map(function (Visit $visit) use (&$extraKeys) { - $extraFields = $this->mapExtraFields($visit); - $extraKeys = array_keys($extraFields); - - $rowData = [ - 'referer' => $visit->referer, - 'date' => $visit->date->toAtomString(), - 'userAgent' => $visit->userAgent, - 'potentialBot' => $visit->potentialBot, - 'country' => $visit->getVisitLocation()->countryName ?? 'Unknown', - 'city' => $visit->getVisitLocation()->cityName ?? 'Unknown', - ...$extraFields, - ]; - - // Filter out unknown keys - return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]); - }, [...$paginator->getCurrentPageResults()]); - $extra = array_map(camelCaseToHumanFriendly(...), $extraKeys); - - return [ - $rows, - ['Referer', 'Date', 'User agent', 'Country', 'City', ...$extra], - ]; - } - /** * @return Paginator */ diff --git a/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php b/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php index f76a4dbc..e3e98fc6 100644 --- a/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php +++ b/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php @@ -17,7 +17,7 @@ use function sprintf; #[AsCommand( DownloadGeoLiteDbCommand::NAME, - 'Checks if the GeoLite2 db file is too old or it does not exist, and tries to download an up-to-date copy if so.', + 'Checks if the GeoLite2 db file is too old or it does not exist, and tries to download an up-to-date copy if so', )] class DownloadGeoLiteDbCommand extends Command implements GeolocationDownloadProgressHandlerInterface { diff --git a/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php index 1b40d55e..6291d6db 100644 --- a/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php +++ b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php @@ -4,57 +4,56 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\Visit; -use Shlinkio\Shlink\CLI\Input\DomainOption; -use Shlinkio\Shlink\Common\Paginator\Paginator; -use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\CLI\Input\VisitsDateRangeInput; +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; use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; -use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\MapInput; +use Symfony\Component\Console\Attribute\Option; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Style\SymfonyStyle; -use function sprintf; - -class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand +#[AsCommand(GetNonOrphanVisitsCommand::NAME, 'Returns the list of non-orphan visits')] +class GetNonOrphanVisitsCommand extends Command { public const string NAME = 'visit:non-orphan'; - private readonly DomainOption $domainOption; - public function __construct( - VisitsStatsHelperInterface $visitsHelper, + private readonly VisitsStatsHelperInterface $visitsHelper, private readonly ShortUrlStringifierInterface $shortUrlStringifier, ) { - parent::__construct($visitsHelper); - $this->domainOption = new DomainOption($this, sprintf( - 'Return visits that belong to this domain only. Use %s keyword for visits in default domain', - Domain::DEFAULT_AUTHORITY, - )); + parent::__construct(); } - protected function configure(): void - { - $this - ->setName(self::NAME) - ->setDescription('Returns the list of non-orphan visits.'); - } - - /** - * @return Paginator - */ - protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator - { - return $this->visitsHelper->nonOrphanVisits(new WithDomainVisitsParams( - dateRange: $dateRange, - domain: $this->domainOption->get($input), + public function __invoke( + SymfonyStyle $io, + #[MapInput] VisitsDateRangeInput $dateRangeInput, + #[Option( + 'Return visits that belong to this domain only. Use ' . Domain::DEFAULT_AUTHORITY . ' keyword for visits ' + . 'in default domain', + shortcut: 'd', + )] + string|null $domain = null, + ): int { + $paginator = $this->visitsHelper->nonOrphanVisits(new WithDomainVisitsParams( + dateRange: $dateRangeInput->toDateRange(), + domain: $domain, )); + [$rows, $headers] = VisitsCommandUtils::resolveRowsAndHeaders($paginator, $this->mapExtraFields(...)); + + ShlinkTable::default($io)->render($headers, $rows); + + return self::SUCCESS; } /** * @return array */ - protected function mapExtraFields(Visit $visit): array + private function mapExtraFields(Visit $visit): array { $shortUrl = $visit->shortUrl; return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)]; diff --git a/module/CLI/src/Command/Visit/VisitsCommandUtils.php b/module/CLI/src/Command/Visit/VisitsCommandUtils.php new file mode 100644 index 00000000..55f425c7 --- /dev/null +++ b/module/CLI/src/Command/Visit/VisitsCommandUtils.php @@ -0,0 +1,48 @@ + $paginator + * @param callable(Visit $visits): array $mapExtraFields + */ + public static function resolveRowsAndHeaders(Paginator $paginator, callable $mapExtraFields): array + { + $extraKeys = []; + $rows = array_map(function (Visit $visit) use (&$extraKeys, $mapExtraFields) { + $extraFields = $mapExtraFields($visit); + $extraKeys = array_keys($extraFields); + + $rowData = [ + 'referer' => $visit->referer, + 'date' => $visit->date->toAtomString(), + 'userAgent' => $visit->userAgent, + 'potentialBot' => $visit->potentialBot, + 'country' => $visit->getVisitLocation()->countryName ?? 'Unknown', + 'city' => $visit->getVisitLocation()->cityName ?? 'Unknown', + ...$extraFields, + ]; + + // Filter out unknown keys + return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]); + }, [...$paginator->getCurrentPageResults()]); + $extra = array_map(camelCaseToHumanFriendly(...), $extraKeys); + + return [ + $rows, + ['Referer', 'Date', 'User agent', 'Country', 'City', ...$extra], + ]; + } +} diff --git a/module/CLI/src/Input/VisitsDateRangeInput.php b/module/CLI/src/Input/VisitsDateRangeInput.php new file mode 100644 index 00000000..189fa0b7 --- /dev/null +++ b/module/CLI/src/Input/VisitsDateRangeInput.php @@ -0,0 +1,28 @@ +startDate), + endDate: normalizeOptionalDate($this->endDate), + ); + } +}