From 900de9e8008119a76529a2a6dd4f133782bc7c57 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 3 Jan 2026 11:45:29 +0100 Subject: [PATCH] Extend and normalize output from visits console commands --- CHANGELOG.md | 4 + module/CLI/config/dependencies.config.php | 6 +- .../Command/Domain/GetDomainVisitsCommand.php | 19 +---- .../src/Command/Tag/GetTagVisitsCommand.php | 19 +---- .../Visit/GetNonOrphanVisitsCommand.php | 19 +---- .../Command/Visit/GetOrphanVisitsCommand.php | 11 +-- .../src/Command/Visit/VisitsCommandUtils.php | 75 ++++++++----------- .../Domain/GetDomainVisitsCommandTest.php | 25 +++---- .../ShortUrl/GetShortUrlVisitsCommandTest.php | 16 ++-- .../Command/Tag/GetTagVisitsCommandTest.php | 23 +++--- .../Visit/GetNonOrphanVisitsCommandTest.php | 23 +++--- .../Visit/GetOrphanVisitsCommandTest.php | 13 ++-- module/Core/functions/array-utils.php | 31 -------- module/Core/src/Matomo/MatomoVisitSender.php | 2 +- module/Core/src/Visit/Entity/Visit.php | 7 +- .../src/Visit/Geolocation/VisitLocator.php | 2 +- 16 files changed, 100 insertions(+), 195 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f3d4b4af..b4750c066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this Instead, if you have more than 1 proxy in front of Shlink, you should provide `TRUSTED_PROXIES` env var, with either a comma-separated list of the IP addresses of your proxies, or a number indicating how many proxies are there in front of Shlink. +* [#2311](https://github.com/shlinkio/shlink/issues/2311) All visits-related commands now return more information, and columns are arranged slightly differently. + + Among other things, they now always return the type of the visit, region, visited URL, redirected URL and whether the visit comes from a potential bot or not. + * [#2540](https://github.com/shlinkio/shlink/issues/2540) Update Symfony packages to 8.0. * [#2512](https://github.com/shlinkio/shlink/issues/2512) Make all remaining console commands invokable. diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index a9bb47f23..365b094ec 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -107,7 +107,7 @@ ], Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\Visit\DeleteOrphanVisitsCommand::class => [Visit\VisitsDeleter::class], - Command\Visit\GetNonOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class], + Command\Visit\GetNonOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class], Command\Api\DisableKeyCommand::class => [ApiKeyService::class], @@ -119,11 +119,11 @@ Command\Tag\ListTagsCommand::class => [TagService::class], Command\Tag\RenameTagCommand::class => [TagService::class], Command\Tag\DeleteTagsCommand::class => [TagService::class], - Command\Tag\GetTagVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class], + Command\Tag\GetTagVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\Domain\ListDomainsCommand::class => [DomainService::class], Command\Domain\DomainRedirectsCommand::class => [DomainService::class], - Command\Domain\GetDomainVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class], + Command\Domain\GetDomainVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\RedirectRule\ManageRedirectRulesCommand::class => [ ShortUrl\ShortUrlResolver::class, diff --git a/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php b/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php index ac05ee7b2..9f4ada0fc 100644 --- a/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php +++ b/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php @@ -6,8 +6,6 @@ use Shlinkio\Shlink\CLI\Command\Visit\VisitsCommandUtils; use Shlinkio\Shlink\CLI\Input\VisitsListInput; -use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; -use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Model\VisitsParams; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Symfony\Component\Console\Attribute\Argument; @@ -22,10 +20,8 @@ class GetDomainVisitsCommand extends Command { public const string NAME = 'domain:visits'; - public function __construct( - private readonly VisitsStatsHelperInterface $visitsHelper, - private readonly ShortUrlStringifierInterface $shortUrlStringifier, - ) { + public function __construct(private readonly VisitsStatsHelperInterface $visitsHelper) + { parent::__construct(); } @@ -36,17 +32,8 @@ public function __invoke( #[MapInput] VisitsListInput $input, ): int { $paginator = $this->visitsHelper->visitsForDomain($domain, new VisitsParams($input->dateRange())); - VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...)); + VisitsCommandUtils::renderOutput($io, $input, $paginator); return self::SUCCESS; } - - /** - * @return array - */ - protected function mapExtraFields(Visit $visit): array - { - $shortUrl = $visit->shortUrl; - return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)]; - } } diff --git a/module/CLI/src/Command/Tag/GetTagVisitsCommand.php b/module/CLI/src/Command/Tag/GetTagVisitsCommand.php index 529fb5361..0719af2bd 100644 --- a/module/CLI/src/Command/Tag/GetTagVisitsCommand.php +++ b/module/CLI/src/Command/Tag/GetTagVisitsCommand.php @@ -7,8 +7,6 @@ use Shlinkio\Shlink\CLI\Command\Visit\VisitsCommandUtils; use Shlinkio\Shlink\CLI\Input\VisitsListInput; 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\Attribute\Argument; @@ -24,10 +22,8 @@ class GetTagVisitsCommand extends Command { public const string NAME = 'tag:visits'; - public function __construct( - private readonly VisitsStatsHelperInterface $visitsHelper, - private readonly ShortUrlStringifierInterface $shortUrlStringifier, - ) { + public function __construct(private readonly VisitsStatsHelperInterface $visitsHelper) + { parent::__construct(); } @@ -47,17 +43,8 @@ public function __invoke( domain: $domain, )); - VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...)); + VisitsCommandUtils::renderOutput($io, $input, $paginator); return self::SUCCESS; } - - /** - * @return 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/GetNonOrphanVisitsCommand.php b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php index 2ff8aa520..86f5b94ee 100644 --- a/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php +++ b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php @@ -6,8 +6,6 @@ use Shlinkio\Shlink\CLI\Input\VisitsListInput; 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\Attribute\AsCommand; @@ -21,10 +19,8 @@ class GetNonOrphanVisitsCommand extends Command { public const string NAME = 'visit:non-orphan'; - public function __construct( - private readonly VisitsStatsHelperInterface $visitsHelper, - private readonly ShortUrlStringifierInterface $shortUrlStringifier, - ) { + public function __construct(private readonly VisitsStatsHelperInterface $visitsHelper) + { parent::__construct(); } @@ -42,17 +38,8 @@ public function __invoke( dateRange: $input->dateRange(), domain: $domain, )); - VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...)); + VisitsCommandUtils::renderOutput($io, $input, $paginator); return self::SUCCESS; } - - /** - * @return 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/GetOrphanVisitsCommand.php b/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php index d1ce8d665..5ad4a4608 100644 --- a/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php +++ b/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php @@ -6,7 +6,6 @@ use Shlinkio\Shlink\CLI\Input\VisitsListInput; use Shlinkio\Shlink\Core\Domain\Entity\Domain; -use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams; use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitType; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; @@ -42,16 +41,8 @@ public function __invoke( domain: $domain, type: $type, )); - VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...)); + VisitsCommandUtils::renderOutput($io, $input, $paginator); return self::SUCCESS; } - - /** - * @return array - */ - private function mapExtraFields(Visit $visit): array - { - return ['type' => $visit->type->value]; - } } diff --git a/module/CLI/src/Command/Visit/VisitsCommandUtils.php b/module/CLI/src/Command/Visit/VisitsCommandUtils.php index 0eadc2c43..765b27c0a 100644 --- a/module/CLI/src/Command/Visit/VisitsCommandUtils.php +++ b/module/CLI/src/Command/Visit/VisitsCommandUtils.php @@ -13,16 +13,12 @@ use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Symfony\Component\Console\Output\OutputInterface; -use function array_keys; use function array_map; -use function Shlinkio\Shlink\Core\ArrayUtils\select_keys; -use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly; class VisitsCommandUtils { /** * @param Paginator $paginator - * @param null|callable(Visit $visits): array $mapExtraFields */ public static function renderOutput( OutputInterface $output, @@ -36,25 +32,21 @@ public static function renderOutput( } match ($inputData->format) { - VisitsListFormat::CSV => self::renderCSVOutput($output, $paginator, $mapExtraFields), - default => self::renderHumanFriendlyOutput($output, $paginator, $mapExtraFields), + VisitsListFormat::CSV => self::renderCSVOutput($output, $paginator), + default => self::renderHumanFriendlyOutput($output, $paginator), }; } /** * @param Paginator $paginator - * @param null|callable(Visit $visits): array $mapExtraFields */ - private static function renderCSVOutput( - OutputInterface $output, - Paginator $paginator, - callable|null $mapExtraFields, - ): void { + private static function renderCSVOutput(OutputInterface $output, Paginator $paginator): void + { $page = 1; do { $paginator->setCurrentPage($page); - [$rows, $headers] = self::resolveRowsAndHeaders($paginator, $mapExtraFields); + [$rows, $headers] = self::resolveRowsAndHeaders($paginator); $csv = Writer::fromString(); if ($page === 1) { $csv->insertOne($headers); @@ -69,19 +61,15 @@ private static function renderCSVOutput( /** * @param Paginator $paginator - * @param null|callable(Visit $visits): array $mapExtraFields */ - private static function renderHumanFriendlyOutput( - OutputInterface $output, - Paginator $paginator, - callable|null $mapExtraFields, - ): void { + private static function renderHumanFriendlyOutput(OutputInterface $output, Paginator $paginator): void + { $page = 1; do { $paginator->setCurrentPage($page); $page++; - [$rows, $headers] = self::resolveRowsAndHeaders($paginator, $mapExtraFields); + [$rows, $headers] = self::resolveRowsAndHeaders($paginator); ShlinkTable::default($output)->render( $headers, $rows, @@ -92,35 +80,38 @@ private static function renderHumanFriendlyOutput( /** * @param Paginator $paginator - * @param null|callable(Visit $visits): array $mapExtraFields */ - private static function resolveRowsAndHeaders(Paginator $paginator, callable|null $mapExtraFields): array + private static function resolveRowsAndHeaders(Paginator $paginator): array { - $extraKeys = null; - $mapExtraFields ??= static fn (Visit $_) => []; - - $rows = array_map(function (Visit $visit) use (&$extraKeys, $mapExtraFields) { - $extraFields = $mapExtraFields($visit); - $extraKeys ??= array_keys($extraFields); + $headers = [ + 'Date', + 'Potential bot', + 'User agent', + 'Referer', + 'Country', + 'Region', + 'City', + 'Visited URL', + 'Redirect URL', + 'Type', + ]; + $rows = array_map(function (Visit $visit) { + $visitLocation = $visit->visitLocation; - $rowData = [ - 'referer' => $visit->referer, + return [ 'date' => $visit->date->toAtomString(), + 'potentialBot' => $visit->potentialBot ? 'Potential bot' : '', 'userAgent' => $visit->userAgent, - 'potentialBot' => $visit->potentialBot, - 'country' => $visit->getVisitLocation()->countryName ?? 'Unknown', - 'city' => $visit->getVisitLocation()->cityName ?? 'Unknown', - ...$extraFields, + 'referer' => $visit->referer, + 'country' => $visitLocation->countryName ?? 'Unknown', + 'region' => $visitLocation->regionName ?? 'Unknown', + 'city' => $visitLocation->cityName ?? 'Unknown', + 'visitedUrl' => $visit->visitedUrl ?? 'Unknown', + 'redirectUrl' => $visit->redirectUrl ?? 'Unknown', + 'type' => $visit->type->value, ]; - - // 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 [$rows, $headers]; } } diff --git a/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php b/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php index d5abbb22b..9b4e2f3f8 100644 --- a/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php +++ b/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php @@ -11,10 +11,10 @@ use Shlinkio\Shlink\CLI\Command\Domain\GetDomainVisitsCommand; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; -use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation; use Shlinkio\Shlink\Core\Visit\Model\Visitor; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Shlinkio\Shlink\IpGeolocation\Model\Location; use ShlinkioTest\Shlink\CLI\Util\CliTestUtils; @@ -24,16 +24,11 @@ class GetDomainVisitsCommandTest extends TestCase { private CommandTester $commandTester; private MockObject & VisitsStatsHelperInterface $visitsHelper; - private MockObject & ShortUrlStringifierInterface $stringifier; protected function setUp(): void { $this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class); - $this->stringifier = $this->createMock(ShortUrlStringifierInterface::class); - - $this->commandTester = CliTestUtils::testerForCommand( - new GetDomainVisitsCommand($this->visitsHelper, $this->stringifier), - ); + $this->commandTester = CliTestUtils::testerForCommand(new GetDomainVisitsCommand($this->visitsHelper)); } #[Test] @@ -48,22 +43,22 @@ public function outputIsProperlyGenerated(): void $domain, $this->anything(), )->willReturn(new Paginator(new ArrayAdapter([$visit]))); - $this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn( - 'the_short_url', - ); $this->commandTester->execute(['domain' => $domain]); $output = $this->commandTester->getDisplay(); + $type = VisitType::VALID_SHORT_URL->value; self::assertEquals( + // phpcs:disable Generic.Files.LineLength <<date->toAtomString()} | bar | Spain | Madrid | the_short_url | - +---------+-------------------------- Page 1 of 1 -+---------+--------+---------------+ + +---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+ + | Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type | + +---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+ + | {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} | + +---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+ OUTPUT, + // phpcs:enable $output, ); } diff --git a/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php index 8e94b0439..9fff4607e 100644 --- a/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php @@ -107,20 +107,22 @@ public static function provideOutput(): iterable { yield 'regular' => [ VisitsListFormat::FULL, + // phpcs:disable Generic.Files.LineLength static fn (Chronos $date) => <<toAtomString()} | bar | Spain | Madrid | - +---------+------------------ Page 1 of 1 ---------+---------+--------+ + +---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+ + | Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type | + +---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+ + | {$date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | valid_short_url | + +---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+ OUTPUT, + // phpcs:enable ]; yield 'CSV' => [ VisitsListFormat::CSV, static fn (Chronos $date) => <<toAtomString()},bar,Spain,Madrid + Date,"Potential bot","User agent",Referer,Country,Region,City,"Visited URL","Redirect URL",Type + {$date->toAtomString()},,bar,foo,Spain,,Madrid,,Unknown,valid_short_url OUTPUT, ]; diff --git a/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php b/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php index 426ff29bd..5e280d5c6 100644 --- a/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php +++ b/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php @@ -11,10 +11,10 @@ use Shlinkio\Shlink\CLI\Command\Tag\GetTagVisitsCommand; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; -use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation; use Shlinkio\Shlink\Core\Visit\Model\Visitor; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Shlinkio\Shlink\IpGeolocation\Model\Location; use ShlinkioTest\Shlink\CLI\Util\CliTestUtils; @@ -24,16 +24,11 @@ class GetTagVisitsCommandTest extends TestCase { private CommandTester $commandTester; private MockObject & VisitsStatsHelperInterface $visitsHelper; - private MockObject & ShortUrlStringifierInterface $stringifier; protected function setUp(): void { $this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class); - $this->stringifier = $this->createMock(ShortUrlStringifierInterface::class); - - $this->commandTester = CliTestUtils::testerForCommand( - new GetTagVisitsCommand($this->visitsHelper, $this->stringifier), - ); + $this->commandTester = CliTestUtils::testerForCommand(new GetTagVisitsCommand($this->visitsHelper)); } #[Test] @@ -47,20 +42,22 @@ public function outputIsProperlyGenerated(): void $this->visitsHelper->expects($this->once())->method('visitsForTag')->with($tag, $this->anything())->willReturn( new Paginator(new ArrayAdapter([$visit])), ); - $this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn('the_short_url'); $this->commandTester->execute(['tag' => $tag]); $output = $this->commandTester->getDisplay(); + $type = VisitType::VALID_SHORT_URL->value; self::assertEquals( + // phpcs:disable Generic.Files.LineLength <<date->toAtomString()} | bar | Spain | Madrid | the_short_url | - +---------+-------------------------- Page 1 of 1 -+---------+--------+---------------+ + +---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+ + | Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type | + +---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+ + | {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} | + +---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+ OUTPUT, + // phpcs:enable $output, ); } diff --git a/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php b/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php index b6e973a92..31042c82f 100644 --- a/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php +++ b/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php @@ -11,10 +11,10 @@ use Shlinkio\Shlink\CLI\Command\Visit\GetNonOrphanVisitsCommand; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; -use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation; use Shlinkio\Shlink\Core\Visit\Model\Visitor; +use Shlinkio\Shlink\Core\Visit\Model\VisitType; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Shlinkio\Shlink\IpGeolocation\Model\Location; use ShlinkioTest\Shlink\CLI\Util\CliTestUtils; @@ -24,16 +24,11 @@ class GetNonOrphanVisitsCommandTest extends TestCase { private CommandTester $commandTester; private MockObject & VisitsStatsHelperInterface $visitsHelper; - private MockObject & ShortUrlStringifierInterface $stringifier; protected function setUp(): void { $this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class); - $this->stringifier = $this->createMock(ShortUrlStringifierInterface::class); - - $this->commandTester = CliTestUtils::testerForCommand( - new GetNonOrphanVisitsCommand($this->visitsHelper, $this->stringifier), - ); + $this->commandTester = CliTestUtils::testerForCommand(new GetNonOrphanVisitsCommand($this->visitsHelper)); } #[Test] @@ -46,20 +41,22 @@ public function outputIsProperlyGenerated(): void $this->visitsHelper->expects($this->once())->method('nonOrphanVisits')->withAnyParameters()->willReturn( new Paginator(new ArrayAdapter([$visit])), ); - $this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn('the_short_url'); $this->commandTester->execute([]); $output = $this->commandTester->getDisplay(); + $type = VisitType::VALID_SHORT_URL->value; self::assertEquals( + // phpcs:disable Generic.Files.LineLength <<date->toAtomString()} | bar | Spain | Madrid | the_short_url | - +---------+-------------------------- Page 1 of 1 -+---------+--------+---------------+ + +---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+ + | Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type | + +---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+ + | {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} | + +---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+ OUTPUT, + // phpcs:enable $output, ); } diff --git a/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php b/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php index 1633eee84..28f9531b3 100644 --- a/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php +++ b/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php @@ -48,16 +48,19 @@ public function outputIsProperlyGenerated(array $args, bool $includesType): void $this->commandTester->execute($args); $output = $this->commandTester->getDisplay(); + $type = OrphanVisitType::BASE_URL->value; self::assertEquals( + // phpcs:disable Generic.Files.LineLength <<date->toAtomString()} | bar | Spain | Madrid | base_url | - +---------+----------------------- Page 1 of 1 ----+---------+--------+----------+ + +---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+----------+ + | Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type | + +---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+----------+ + | {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} | + +---------------------------+---------------+------------+--- Page 1 of 1 ---+--------+--------+-------------+--------------+----------+ OUTPUT, + // phpcs:enable $output, ); } diff --git a/module/Core/functions/array-utils.php b/module/Core/functions/array-utils.php index 3e0010a25..fe2ffae65 100644 --- a/module/Core/functions/array-utils.php +++ b/module/Core/functions/array-utils.php @@ -4,12 +4,8 @@ namespace Shlinkio\Shlink\Core\ArrayUtils; -use function array_filter; -use function array_reduce; use function in_array; -use const ARRAY_FILTER_USE_KEY; - /** * @template T * @param T $value @@ -20,18 +16,6 @@ function contains(mixed $value, array $array): bool return in_array($value, $array, strict: true); } -/** - * @param array[] $multiArray - */ -function flatten(array $multiArray): array -{ - return array_reduce( - $multiArray, - static fn (array $carry, array $value) => [...$carry, ...$value], - initial: [], - ); -} - /** * Checks if a callback returns true for at least one item in a collection. * @param callable(mixed $value, mixed $key): bool $callback @@ -62,21 +46,6 @@ function every(iterable $collection, callable $callback): bool return true; } -/** - * Returns an array containing only those entries in the array whose key is in the supplied keys. - */ -function select_keys(array $array, array $keys): array -{ - return array_filter( - $array, - static fn (string $key) => contains( - $key, - $keys, - ), - ARRAY_FILTER_USE_KEY, - ); -} - /** * @template T * @template R diff --git a/module/Core/src/Matomo/MatomoVisitSender.php b/module/Core/src/Matomo/MatomoVisitSender.php index 6a32c2a52..bd2bf8485 100644 --- a/module/Core/src/Matomo/MatomoVisitSender.php +++ b/module/Core/src/Matomo/MatomoVisitSender.php @@ -58,7 +58,7 @@ public function sendVisit(Visit $visit, string|null $originalIpAddress = null): ->setUrlReferrer($visit->referer) ->setForceVisitDateTime($visit->date->setTimezone('UTC')->toDateTimeString()); - $location = $visit->getVisitLocation(); + $location = $visit->visitLocation; if ($location !== null) { $tracker ->setCity($location->cityName) diff --git a/module/Core/src/Visit/Entity/Visit.php b/module/Core/src/Visit/Entity/Visit.php index b396bc07e..1cd76c7e4 100644 --- a/module/Core/src/Visit/Entity/Visit.php +++ b/module/Core/src/Visit/Entity/Visit.php @@ -29,7 +29,7 @@ private function __construct( public readonly string|null $remoteAddr = null, public readonly string|null $visitedUrl = null, public readonly string|null $redirectUrl = null, - private VisitLocation|null $visitLocation = null, + private(set) VisitLocation|null $visitLocation = null, public readonly Chronos $date = new Chronos(), ) { } @@ -124,11 +124,6 @@ public function hasRemoteAddr(): bool return ! empty($this->remoteAddr); } - public function getVisitLocation(): VisitLocation|null - { - return $this->visitLocation; - } - public function locate(VisitLocation $visitLocation): self { $this->visitLocation = $visitLocation; diff --git a/module/Core/src/Visit/Geolocation/VisitLocator.php b/module/Core/src/Visit/Geolocation/VisitLocator.php index 48245a29a..5480a53d9 100644 --- a/module/Core/src/Visit/Geolocation/VisitLocator.php +++ b/module/Core/src/Visit/Geolocation/VisitLocator.php @@ -72,7 +72,7 @@ private function locateVisits(iterable $results, VisitGeolocationHelperInterface private function locateVisit(Visit $visit, VisitLocation $location, VisitGeolocationHelperInterface $helper): void { - $prevLocation = $visit->getVisitLocation(); + $prevLocation = $visit->visitLocation; $visit->locate($location); $this->em->persist($visit);