diff --git a/CHANGELOG.md b/CHANGELOG.md index 901d5e60..fe469f58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added -* *Nothing* +* [#1280](https://github.com/shlinkio/shlink/issues/1280) Added missing visit-related commands. + + Now you can run `tag:visits`, `domain:visits`, `visit:orphan` or `visit:non-orphan` to get the corresponding list of visits from the command line. ### Changed * *Nothing* diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 2b5b5afd..7629d855 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -11,11 +11,13 @@ return [ Command\ShortUrl\CreateShortUrlCommand::NAME => Command\ShortUrl\CreateShortUrlCommand::class, Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class, Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class, - Command\ShortUrl\GetVisitsCommand::NAME => Command\ShortUrl\GetVisitsCommand::class, + Command\ShortUrl\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::class, Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class, Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class, Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class, + Command\Visit\GetOrphanVisitsCommand::NAME => Command\Visit\GetOrphanVisitsCommand::class, + Command\Visit\GetNonOrphanVisitsCommand::NAME => Command\Visit\GetNonOrphanVisitsCommand::class, Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class, Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class, @@ -24,9 +26,11 @@ return [ Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class, Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class, Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class, + Command\Tag\GetTagVisitsCommand::NAME => Command\Tag\GetTagVisitsCommand::class, Command\Domain\ListDomainsCommand::NAME => Command\Domain\ListDomainsCommand::class, Command\Domain\DomainRedirectsCommand::NAME => Command\Domain\DomainRedirectsCommand::class, + Command\Domain\GetDomainVisitsCommand::NAME => Command\Domain\GetDomainVisitsCommand::class, Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class, Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class, diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 137bdd7a..933affd0 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -42,11 +42,13 @@ return [ Command\ShortUrl\CreateShortUrlCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class, - Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class, + Command\ShortUrl\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class, Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class, Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class, + Command\Visit\GetOrphanVisitsCommand::class => ConfigAbstractFactory::class, + Command\Visit\GetNonOrphanVisitsCommand::class => ConfigAbstractFactory::class, Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class, Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class, @@ -55,12 +57,14 @@ return [ Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class, Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class, Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class, + Command\Tag\GetTagVisitsCommand::class => ConfigAbstractFactory::class, Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class, Command\Db\MigrateDatabaseCommand::class => ConfigAbstractFactory::class, Command\Domain\ListDomainsCommand::class => ConfigAbstractFactory::class, Command\Domain\DomainRedirectsCommand::class => ConfigAbstractFactory::class, + Command\Domain\GetDomainVisitsCommand::class => ConfigAbstractFactory::class, ], ], @@ -85,7 +89,7 @@ return [ Service\ShortUrlService::class, ShortUrlDataTransformer::class, ], - Command\ShortUrl\GetVisitsCommand::class => [Visit\VisitsStatsHelper::class], + Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class], Command\Visit\DownloadGeoLiteDbCommand::class => [Util\GeolocationDbUpdater::class], @@ -94,6 +98,8 @@ return [ IpLocationResolverInterface::class, LockFactory::class, ], + Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class], + Command\Visit\GetNonOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class], Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class], Command\Api\DisableKeyCommand::class => [ApiKeyService::class], @@ -102,9 +108,11 @@ return [ 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\Domain\ListDomainsCommand::class => [DomainService::class], Command\Domain\DomainRedirectsCommand::class => [DomainService::class], + Command\Domain\GetDomainVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class], Command\Db\CreateDatabaseCommand::class => [ LockFactory::class, diff --git a/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php b/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php new file mode 100644 index 00000000..00c811c1 --- /dev/null +++ b/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php @@ -0,0 +1,50 @@ +setName(self::NAME) + ->setDescription('Returns the list of visits for provided domain.') + ->addArgument('domain', InputArgument::REQUIRED, 'The domain which visits we want to get.'); + } + + protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator + { + $domain = $input->getArgument('domain'); + return $this->visitsHelper->visitsForDomain($domain, new VisitsParams($dateRange)); + } + + /** + * @return array + */ + protected function mapExtraFields(Visit $visit): array + { + $shortUrl = $visit->getShortUrl(); + return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)]; + } +} diff --git a/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php b/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php new file mode 100644 index 00000000..49c390f8 --- /dev/null +++ b/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php @@ -0,0 +1,59 @@ +setName(self::NAME) + ->setDescription('Returns the detailed visits information for provided short code') + ->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get.') + ->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain for the short code.'); + } + + protected function interact(InputInterface $input, OutputInterface $output): void + { + $shortCode = $input->getArgument('shortCode'); + if (! empty($shortCode)) { + return; + } + + $io = new SymfonyStyle($input, $output); + $shortCode = $io->ask('A short code was not provided. Which short code do you want to use?'); + if (! empty($shortCode)) { + $input->setArgument('shortCode', $shortCode); + } + } + + protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator + { + $identifier = ShortUrlIdentifier::fromCli($input); + return $this->visitsHelper->visitsForShortUrl($identifier, new VisitsParams($dateRange)); + } + + /** + * @return array + */ + protected function mapExtraFields(Visit $visit): array + { + return []; + } +} diff --git a/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php b/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php deleted file mode 100644 index bb2f0229..00000000 --- a/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php +++ /dev/null @@ -1,88 +0,0 @@ -setName(self::NAME) - ->setDescription('Returns the detailed visits information for provided short code') - ->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get.') - ->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain for the short code.'); - } - - protected function getStartDateDesc(string $optionName): string - { - return sprintf('Allows to filter visits, returning only those older than "%s".', $optionName); - } - - protected function getEndDateDesc(string $optionName): string - { - return sprintf('Allows to filter visits, returning only those newer than "%s".', $optionName); - } - - protected function interact(InputInterface $input, OutputInterface $output): void - { - $shortCode = $input->getArgument('shortCode'); - if (! empty($shortCode)) { - return; - } - - $io = new SymfonyStyle($input, $output); - $shortCode = $io->ask('A short code was not provided. Which short code do you want to use?'); - if (! empty($shortCode)) { - $input->setArgument('shortCode', $shortCode); - } - } - - protected function execute(InputInterface $input, OutputInterface $output): ?int - { - $identifier = ShortUrlIdentifier::fromCli($input); - $startDate = $this->getStartDateOption($input, $output); - $endDate = $this->getEndDateOption($input, $output); - - $paginator = $this->visitsHelper->visitsForShortUrl( - $identifier, - new VisitsParams(buildDateRange($startDate, $endDate)), - ); - - $rows = map($paginator->getCurrentPageResults(), function (Visit $visit) { - $rowData = $visit->jsonSerialize(); - $rowData['country'] = ($visit->getVisitLocation() ?? new UnknownVisitLocation())->getCountryName(); - return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']); - }); - ShlinkTable::default($output)->render(['Referer', 'Date', 'User agent', 'Country'], $rows); - - return ExitCodes::EXIT_SUCCESS; - } -} diff --git a/module/CLI/src/Command/Tag/GetTagVisitsCommand.php b/module/CLI/src/Command/Tag/GetTagVisitsCommand.php new file mode 100644 index 00000000..ac0157bc --- /dev/null +++ b/module/CLI/src/Command/Tag/GetTagVisitsCommand.php @@ -0,0 +1,50 @@ +setName(self::NAME) + ->setDescription('Returns the list of visits for provided tag.') + ->addArgument('tag', InputArgument::REQUIRED, 'The tag which visits we want to get.'); + } + + protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator + { + $tag = $input->getArgument('tag'); + return $this->visitsHelper->visitsForTag($tag, new VisitsParams($dateRange)); + } + + /** + * @return array + */ + protected function mapExtraFields(Visit $visit): array + { + $shortUrl = $visit->getShortUrl(); + return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)]; + } +} diff --git a/module/CLI/src/Command/Tag/RenameTagCommand.php b/module/CLI/src/Command/Tag/RenameTagCommand.php index 23c1568d..85377a18 100644 --- a/module/CLI/src/Command/Tag/RenameTagCommand.php +++ b/module/CLI/src/Command/Tag/RenameTagCommand.php @@ -19,7 +19,7 @@ class RenameTagCommand extends Command { public const NAME = 'tag:rename'; - public function __construct(private TagServiceInterface $tagService) + public function __construct(private readonly TagServiceInterface $tagService) { parent::__construct(); } diff --git a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php new file mode 100644 index 00000000..257c7f26 --- /dev/null +++ b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php @@ -0,0 +1,83 @@ +getStartDateOption($input, $output); + $endDate = $this->getEndDateOption($input, $output); + $paginator = $this->getVisitsPaginator($input, buildDateRange($startDate, $endDate)); + [$rows, $headers] = $this->resolveRowsAndHeaders($paginator); + + ShlinkTable::default($output)->render($headers, $rows); + + return ExitCodes::EXIT_SUCCESS; + } + + private function resolveRowsAndHeaders(Paginator $paginator): array + { + $extraKeys = []; + $rows = map($paginator->getCurrentPageResults(), function (Visit $visit) use (&$extraKeys) { + $extraFields = $this->mapExtraFields($visit); + $extraKeys = array_keys($extraFields); + + $rowData = [ + ...$visit->jsonSerialize(), + 'country' => $visit->getVisitLocation()?->getCountryName() ?? 'Unknown', + 'city' => $visit->getVisitLocation()?->getCityName() ?? 'Unknown', + ...$extraFields, + ]; + + return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]); + }); + $extra = map($extraKeys, camelCaseToHumanFriendly(...)); + + return [ + $rows, + ['Referer', 'Date', 'User agent', 'Country', 'City', ...$extra], + ]; + } + + abstract protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator; + + /** + * @return array + */ + abstract protected function mapExtraFields(Visit $visit): array; +} diff --git a/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php new file mode 100644 index 00000000..76c35990 --- /dev/null +++ b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php @@ -0,0 +1,46 @@ +setName(self::NAME) + ->setDescription('Returns the list of non-orphan visits.'); + } + + protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator + { + return $this->visitsHelper->nonOrphanVisits(new VisitsParams($dateRange)); + } + + /** + * @return array + */ + protected function mapExtraFields(Visit $visit): array + { + $shortUrl = $visit->getShortUrl(); + 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 new file mode 100644 index 00000000..ec675a69 --- /dev/null +++ b/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php @@ -0,0 +1,36 @@ +setName(self::NAME) + ->setDescription('Returns the list of orphan visits.'); + } + + protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator + { + return $this->visitsHelper->orphanVisits(new VisitsParams($dateRange)); + } + + /** + * @return array + */ + protected function mapExtraFields(Visit $visit): array + { + return ['type' => $visit->type()->value]; + } +} diff --git a/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php b/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php new file mode 100644 index 00000000..f94a2000 --- /dev/null +++ b/module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php @@ -0,0 +1,71 @@ +visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); + $this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class); + + $this->commandTester = $this->testerForCommand( + new GetDomainVisitsCommand($this->visitsHelper->reveal(), $this->stringifier->reveal()), + ); + } + + /** @test */ + public function outputIsProperlyGenerated(): void + { + $shortUrl = ShortUrl::createEmpty(); + $visit = Visit::forValidShortUrl($shortUrl, new Visitor('bar', 'foo', '', ''))->locate( + VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')), + ); + $domain = 'doma.in'; + $getVisits = $this->visitsHelper->visitsForDomain($domain, Argument::any())->willReturn( + new Paginator(new ArrayAdapter([$visit])), + ); + $stringify = $this->stringifier->stringify($shortUrl)->willReturn('the_short_url'); + + $this->commandTester->execute(['domain' => $domain]); + $output = $this->commandTester->getDisplay(); + + self::assertEquals( + <<getDate()->toAtomString()} | bar | Spain | Madrid | the_short_url | + +---------+---------------------------+------------+---------+--------+---------------+ + + OUTPUT, + $output, + ); + $getVisits->shouldHaveBeenCalledOnce(); + $stringify->shouldHaveBeenCalledOnce(); + } +} diff --git a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php similarity index 74% rename from module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php rename to module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php index 7a884c89..076eb9b2 100644 --- a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php @@ -9,7 +9,7 @@ use Pagerfanta\Adapter\ArrayAdapter; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; -use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand; +use Shlinkio\Shlink\CLI\Command\ShortUrl\GetShortUrlVisitsCommand; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Entity\ShortUrl; @@ -23,9 +23,10 @@ use Shlinkio\Shlink\IpGeolocation\Model\Location; use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait; use Symfony\Component\Console\Tester\CommandTester; +use function Shlinkio\Shlink\Common\buildDateRange; use function sprintf; -class GetVisitsCommandTest extends TestCase +class GetShortUrlVisitsCommandTest extends TestCase { use CliTestUtilsTrait; @@ -35,7 +36,7 @@ class GetVisitsCommandTest extends TestCase public function setUp(): void { $this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); - $command = new GetVisitsCommand($this->visitsHelper->reveal()); + $command = new GetShortUrlVisitsCommand($this->visitsHelper->reveal()); $this->commandTester = $this->testerForCommand($command); } @@ -61,7 +62,7 @@ class GetVisitsCommandTest extends TestCase $endDate = '2016-02-01'; $this->visitsHelper->visitsForShortUrl( ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), - new VisitsParams(DateRange::withStartAndEndDate(Chronos::parse($startDate), Chronos::parse($endDate))), + new VisitsParams(buildDateRange(Chronos::parse($startDate), Chronos::parse($endDate))), ) ->willReturn(new Paginator(new ArrayAdapter([]))) ->shouldBeCalledOnce(); @@ -99,22 +100,30 @@ class GetVisitsCommandTest extends TestCase /** @test */ public function outputIsProperlyGenerated(): void { + $visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('bar', 'foo', '', ''))->locate( + VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')), + ); $shortCode = 'abc123'; $this->visitsHelper->visitsForShortUrl( ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), Argument::any(), )->willReturn( - new Paginator(new ArrayAdapter([ - Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('bar', 'foo', '', ''))->locate( - VisitLocation::fromGeolocation(new Location('', 'Spain', '', '', 0, 0, '')), - ), - ])), + new Paginator(new ArrayAdapter([$visit])), )->shouldBeCalledOnce(); $this->commandTester->execute(['shortCode' => $shortCode]); $output = $this->commandTester->getDisplay(); - self::assertStringContainsString('foo', $output); - self::assertStringContainsString('Spain', $output); - self::assertStringContainsString('bar', $output); + + self::assertEquals( + <<getDate()->toAtomString()} | bar | Spain | Madrid | + +---------+---------------------------+------------+---------+--------+ + + OUTPUT, + $output, + ); } } diff --git a/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php b/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php new file mode 100644 index 00000000..95036a7f --- /dev/null +++ b/module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php @@ -0,0 +1,71 @@ +visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); + $this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class); + + $this->commandTester = $this->testerForCommand( + new GetTagVisitsCommand($this->visitsHelper->reveal(), $this->stringifier->reveal()), + ); + } + + /** @test */ + public function outputIsProperlyGenerated(): void + { + $shortUrl = ShortUrl::createEmpty(); + $visit = Visit::forValidShortUrl($shortUrl, new Visitor('bar', 'foo', '', ''))->locate( + VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')), + ); + $tag = 'abc123'; + $getVisits = $this->visitsHelper->visitsForTag($tag, Argument::any())->willReturn( + new Paginator(new ArrayAdapter([$visit])), + ); + $stringify = $this->stringifier->stringify($shortUrl)->willReturn('the_short_url'); + + $this->commandTester->execute(['tag' => $tag]); + $output = $this->commandTester->getDisplay(); + + self::assertEquals( + <<getDate()->toAtomString()} | bar | Spain | Madrid | the_short_url | + +---------+---------------------------+------------+---------+--------+---------------+ + + OUTPUT, + $output, + ); + $getVisits->shouldHaveBeenCalledOnce(); + $stringify->shouldHaveBeenCalledOnce(); + } +} diff --git a/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php b/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php new file mode 100644 index 00000000..d6888bf5 --- /dev/null +++ b/module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php @@ -0,0 +1,70 @@ +visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); + $this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class); + + $this->commandTester = $this->testerForCommand( + new GetNonOrphanVisitsCommand($this->visitsHelper->reveal(), $this->stringifier->reveal()), + ); + } + + /** @test */ + public function outputIsProperlyGenerated(): void + { + $shortUrl = ShortUrl::createEmpty(); + $visit = Visit::forValidShortUrl($shortUrl, new Visitor('bar', 'foo', '', ''))->locate( + VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')), + ); + $getVisits = $this->visitsHelper->nonOrphanVisits(Argument::any())->willReturn( + new Paginator(new ArrayAdapter([$visit])), + ); + $stringify = $this->stringifier->stringify($shortUrl)->willReturn('the_short_url'); + + $this->commandTester->execute([]); + $output = $this->commandTester->getDisplay(); + + self::assertEquals( + <<getDate()->toAtomString()} | bar | Spain | Madrid | the_short_url | + +---------+---------------------------+------------+---------+--------+---------------+ + + OUTPUT, + $output, + ); + $getVisits->shouldHaveBeenCalledOnce(); + $stringify->shouldHaveBeenCalledOnce(); + } +} diff --git a/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php b/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php new file mode 100644 index 00000000..c8c10aad --- /dev/null +++ b/module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php @@ -0,0 +1,60 @@ +visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); + $this->commandTester = $this->testerForCommand(new GetOrphanVisitsCommand($this->visitsHelper->reveal())); + } + + /** @test */ + public function outputIsProperlyGenerated(): void + { + $visit = Visit::forBasePath(new Visitor('bar', 'foo', '', ''))->locate( + VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')), + ); + $getVisits = $this->visitsHelper->orphanVisits(Argument::any())->willReturn( + new Paginator(new ArrayAdapter([$visit])), + ); + + $this->commandTester->execute([]); + $output = $this->commandTester->getDisplay(); + + self::assertEquals( + <<getDate()->toAtomString()} | bar | Spain | Madrid | base_url | + +---------+---------------------------+------------+---------+--------+----------+ + + OUTPUT, + $output, + ); + $getVisits->shouldHaveBeenCalledOnce(); + } +} diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index db9a11b9..c5186e41 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -8,6 +8,7 @@ use Cake\Chronos\Chronos; use DateTimeInterface; use Doctrine\ORM\Mapping\Builder\FieldBuilder; use Jaybizzle\CrawlerDetect\CrawlerDetect; +use Laminas\Filter\Word\CamelCaseToSeparator; use Laminas\InputFilter\InputFilter; use PUGX\Shortid\Factory as ShortIdFactory; use Shlinkio\Shlink\Common\Util\DateRange; @@ -19,6 +20,7 @@ use function print_r; use function Shlinkio\Shlink\Common\buildDateRange; use function sprintf; use function str_repeat; +use function ucfirst; function generateRandomShortCode(int $length): string { @@ -115,3 +117,13 @@ function fieldWithUtf8Charset(FieldBuilder $field, array $emConfig, string $coll default => $field, }; } + +function camelCaseToHumanFriendly(string $value): string +{ + static $filter; + if ($filter === null) { + $filter = new CamelCaseToSeparator(' '); + } + + return ucfirst($filter->filter($value)); +} diff --git a/module/Core/src/Entity/Visit.php b/module/Core/src/Entity/Visit.php index 23a518ca..9bff9db9 100644 --- a/module/Core/src/Entity/Visit.php +++ b/module/Core/src/Entity/Visit.php @@ -10,7 +10,6 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Core\Model\Visitor; -use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface; use Shlinkio\Shlink\Core\Visit\Model\VisitType; use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisit; @@ -119,7 +118,7 @@ class Visit extends AbstractEntity implements JsonSerializable return $this->shortUrl; } - public function getVisitLocation(): ?VisitLocationInterface + public function getVisitLocation(): ?VisitLocation { return $this->visitLocation; } diff --git a/module/Core/src/Entity/VisitLocation.php b/module/Core/src/Entity/VisitLocation.php index 594126a7..5ab781de 100644 --- a/module/Core/src/Entity/VisitLocation.php +++ b/module/Core/src/Entity/VisitLocation.php @@ -5,11 +5,10 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Entity; use Shlinkio\Shlink\Common\Entity\AbstractEntity; -use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface; use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisitLocation; use Shlinkio\Shlink\IpGeolocation\Model\Location; -class VisitLocation extends AbstractEntity implements VisitLocationInterface +class VisitLocation extends AbstractEntity { private string $countryCode; private string $countryName; diff --git a/module/Core/src/Visit/Model/UnknownVisitLocation.php b/module/Core/src/Visit/Model/UnknownVisitLocation.php deleted file mode 100644 index b8926bd5..00000000 --- a/module/Core/src/Visit/Model/UnknownVisitLocation.php +++ /dev/null @@ -1,41 +0,0 @@ - 'Unknown', - 'countryName' => 'Unknown', - 'regionName' => 'Unknown', - 'cityName' => 'Unknown', - 'latitude' => 0.0, - 'longitude' => 0.0, - 'timezone' => 'Unknown', - ]; - } -} diff --git a/module/Core/src/Visit/Model/VisitLocationInterface.php b/module/Core/src/Visit/Model/VisitLocationInterface.php deleted file mode 100644 index 9a296a28..00000000 --- a/module/Core/src/Visit/Model/VisitLocationInterface.php +++ /dev/null @@ -1,18 +0,0 @@ -