From 72e56d271d6babdfee5620a85b68ba549cc46576 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 22 May 2022 19:22:29 +0200 Subject: [PATCH 1/8] Created tags visits command, with abstract class wrapping common logic for visits lists commands --- module/CLI/config/cli.config.php | 1 + module/CLI/config/dependencies.config.php | 2 + .../src/Command/ShortUrl/GetVisitsCommand.php | 50 ++-------------- .../CLI/src/Command/Tag/RenameTagCommand.php | 2 +- .../CLI/src/Command/Tag/TagVisitsCommand.php | 31 ++++++++++ .../Visit/AbstractVisitsListCommand.php | 57 +++++++++++++++++++ .../Command/ShortUrl/GetVisitsCommandTest.php | 3 +- module/Core/src/Entity/Visit.php | 3 +- module/Core/src/Entity/VisitLocation.php | 3 +- .../src/Visit/Model/UnknownVisitLocation.php | 41 ------------- .../Visit/Model/VisitLocationInterface.php | 18 ------ 11 files changed, 102 insertions(+), 109 deletions(-) create mode 100644 module/CLI/src/Command/Tag/TagVisitsCommand.php create mode 100644 module/CLI/src/Command/Visit/AbstractVisitsListCommand.php delete mode 100644 module/Core/src/Visit/Model/UnknownVisitLocation.php delete mode 100644 module/Core/src/Visit/Model/VisitLocationInterface.php diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 2b5b5afd..5e24703a 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -24,6 +24,7 @@ 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\TagVisitsCommand::NAME => Command\Tag\TagVisitsCommand::class, Command\Domain\ListDomainsCommand::NAME => Command\Domain\ListDomainsCommand::class, Command\Domain\DomainRedirectsCommand::NAME => Command\Domain\DomainRedirectsCommand::class, diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 137bdd7a..b394cb8f 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -55,6 +55,7 @@ return [ Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class, Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class, Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class, + Command\Tag\TagVisitsCommand::class => ConfigAbstractFactory::class, Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class, Command\Db\MigrateDatabaseCommand::class => ConfigAbstractFactory::class, @@ -102,6 +103,7 @@ return [ Command\Tag\ListTagsCommand::class => [TagService::class], Command\Tag\RenameTagCommand::class => [TagService::class], Command\Tag\DeleteTagsCommand::class => [TagService::class], + Command\Tag\TagVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\Domain\ListDomainsCommand::class => [DomainService::class], Command\Domain\DomainRedirectsCommand::class => [DomainService::class], diff --git a/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php b/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php index bb2f0229..7bcbd367 100644 --- a/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php +++ b/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php @@ -4,34 +4,21 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\ShortUrl; -use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand; -use Shlinkio\Shlink\CLI\Util\ExitCodes; -use Shlinkio\Shlink\CLI\Util\ShlinkTable; -use Shlinkio\Shlink\Core\Entity\Visit; +use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand; +use Shlinkio\Shlink\Common\Paginator\Paginator; +use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\VisitsParams; -use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation; -use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use function Functional\map; -use function Functional\select_keys; -use function Shlinkio\Shlink\Common\buildDateRange; -use function sprintf; - -class GetVisitsCommand extends AbstractWithDateRangeCommand +class GetVisitsCommand extends AbstractVisitsListCommand { public const NAME = 'short-url:visits'; - public function __construct(private VisitsStatsHelperInterface $visitsHelper) - { - parent::__construct(); - } - protected function doConfigure(): void { $this @@ -41,16 +28,6 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand ->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'); @@ -65,24 +42,9 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand } } - protected function execute(InputInterface $input, OutputInterface $output): ?int + protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator { $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; + return $this->visitsHelper->visitsForShortUrl($identifier, new VisitsParams($dateRange)); } } 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/Tag/TagVisitsCommand.php b/module/CLI/src/Command/Tag/TagVisitsCommand.php new file mode 100644 index 00000000..a50cb0da --- /dev/null +++ b/module/CLI/src/Command/Tag/TagVisitsCommand.php @@ -0,0 +1,31 @@ +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)); + } +} diff --git a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php new file mode 100644 index 00000000..3ed69c7f --- /dev/null +++ b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php @@ -0,0 +1,57 @@ +getStartDateOption($input, $output); + $endDate = $this->getEndDateOption($input, $output); + $paginator = $this->getVisitsPaginator($input, buildDateRange($startDate, $endDate)); + + $rows = map($paginator->getCurrentPageResults(), function (Visit $visit) { + $rowData = $visit->jsonSerialize(); + $rowData['country'] = $visit->getVisitLocation()?->getCountryName() ?? 'Unknown'; + $rowData['city'] = $visit->getVisitLocation()?->getCityName() ?? 'Unknown'; + return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city']); + }); + ShlinkTable::default($output)->render(['Referer', 'Date', 'User agent', 'Country', 'City'], $rows); + + return ExitCodes::EXIT_SUCCESS; + } + + abstract protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator; +} diff --git a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php index 7a884c89..daea7b7f 100644 --- a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php @@ -106,7 +106,7 @@ class GetVisitsCommandTest extends TestCase )->willReturn( new Paginator(new ArrayAdapter([ Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('bar', 'foo', '', ''))->locate( - VisitLocation::fromGeolocation(new Location('', 'Spain', '', '', 0, 0, '')), + VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')), ), ])), )->shouldBeCalledOnce(); @@ -115,6 +115,7 @@ class GetVisitsCommandTest extends TestCase $output = $this->commandTester->getDisplay(); self::assertStringContainsString('foo', $output); self::assertStringContainsString('Spain', $output); + self::assertStringContainsString('Madrid', $output); self::assertStringContainsString('bar', $output); } } 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 @@ - Date: Sun, 22 May 2022 19:34:08 +0200 Subject: [PATCH 2/8] Created visits commands for orphan, non-orphan and domain --- module/CLI/config/cli.config.php | 3 ++ module/CLI/config/dependencies.config.php | 6 ++++ .../Command/Domain/GetDomainVisitsCommand.php | 31 +++++++++++++++++++ .../Visit/GetNonOrphanVisitsCommand.php | 27 ++++++++++++++++ .../Command/Visit/GetOrphanVisitsCommand.php | 27 ++++++++++++++++ 5 files changed, 94 insertions(+) create mode 100644 module/CLI/src/Command/Domain/GetDomainVisitsCommand.php create mode 100644 module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php create mode 100644 module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 5e24703a..cd7302c0 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -16,6 +16,8 @@ return [ 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, @@ -28,6 +30,7 @@ return [ 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 b394cb8f..ea60d7fe 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -47,6 +47,8 @@ return [ 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, @@ -62,6 +64,7 @@ return [ Command\Domain\ListDomainsCommand::class => ConfigAbstractFactory::class, Command\Domain\DomainRedirectsCommand::class => ConfigAbstractFactory::class, + Command\Domain\GetDomainVisitsCommand::class => ConfigAbstractFactory::class, ], ], @@ -95,6 +98,8 @@ return [ IpLocationResolverInterface::class, LockFactory::class, ], + Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class], + Command\Visit\GetNonOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class], Command\Api\DisableKeyCommand::class => [ApiKeyService::class], @@ -107,6 +112,7 @@ return [ Command\Domain\ListDomainsCommand::class => [DomainService::class], Command\Domain\DomainRedirectsCommand::class => [DomainService::class], + Command\Domain\GetDomainVisitsCommand::class => [Visit\VisitsStatsHelper::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..2157416f --- /dev/null +++ b/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php @@ -0,0 +1,31 @@ +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)); + } +} diff --git a/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php new file mode 100644 index 00000000..1c99619c --- /dev/null +++ b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php @@ -0,0 +1,27 @@ +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)); + } +} diff --git a/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php b/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php new file mode 100644 index 00000000..561fe8ff --- /dev/null +++ b/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php @@ -0,0 +1,27 @@ +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)); + } +} From 00002b1e248f7d398703b2c1da0a07c857bfb839 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 23 May 2022 20:47:37 +0200 Subject: [PATCH 3/8] Renamed some visits commands --- module/CLI/config/cli.config.php | 4 ++-- module/CLI/config/dependencies.config.php | 8 ++++---- ...{GetVisitsCommand.php => GetShortUrlVisitsCommand.php} | 2 +- .../Tag/{TagVisitsCommand.php => GetTagVisitsCommand.php} | 2 +- module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename module/CLI/src/Command/ShortUrl/{GetVisitsCommand.php => GetShortUrlVisitsCommand.php} (96%) rename module/CLI/src/Command/Tag/{TagVisitsCommand.php => GetTagVisitsCommand.php} (94%) diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index cd7302c0..7629d855 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -11,7 +11,7 @@ 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, @@ -26,7 +26,7 @@ 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\TagVisitsCommand::NAME => Command\Tag\TagVisitsCommand::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, diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index ea60d7fe..8bebedaf 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -42,7 +42,7 @@ 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, @@ -57,7 +57,7 @@ return [ Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class, Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class, Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class, - Command\Tag\TagVisitsCommand::class => ConfigAbstractFactory::class, + Command\Tag\GetTagVisitsCommand::class => ConfigAbstractFactory::class, Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class, Command\Db\MigrateDatabaseCommand::class => ConfigAbstractFactory::class, @@ -89,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], @@ -108,7 +108,7 @@ return [ Command\Tag\ListTagsCommand::class => [TagService::class], Command\Tag\RenameTagCommand::class => [TagService::class], Command\Tag\DeleteTagsCommand::class => [TagService::class], - Command\Tag\TagVisitsCommand::class => [Visit\VisitsStatsHelper::class], + Command\Tag\GetTagVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\Domain\ListDomainsCommand::class => [DomainService::class], Command\Domain\DomainRedirectsCommand::class => [DomainService::class], diff --git a/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php b/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php similarity index 96% rename from module/CLI/src/Command/ShortUrl/GetVisitsCommand.php rename to module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php index 7bcbd367..2683cfd1 100644 --- a/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php +++ b/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php @@ -15,7 +15,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -class GetVisitsCommand extends AbstractVisitsListCommand +class GetShortUrlVisitsCommand extends AbstractVisitsListCommand { public const NAME = 'short-url:visits'; diff --git a/module/CLI/src/Command/Tag/TagVisitsCommand.php b/module/CLI/src/Command/Tag/GetTagVisitsCommand.php similarity index 94% rename from module/CLI/src/Command/Tag/TagVisitsCommand.php rename to module/CLI/src/Command/Tag/GetTagVisitsCommand.php index a50cb0da..2d23d0f4 100644 --- a/module/CLI/src/Command/Tag/TagVisitsCommand.php +++ b/module/CLI/src/Command/Tag/GetTagVisitsCommand.php @@ -11,7 +11,7 @@ use Shlinkio\Shlink\Core\Model\VisitsParams; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -class TagVisitsCommand extends AbstractVisitsListCommand +class GetTagVisitsCommand extends AbstractVisitsListCommand { public const NAME = 'tag:visits'; diff --git a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php index daea7b7f..93770d55 100644 --- a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.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; @@ -35,7 +35,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); } From 353ac0fc0c50c92de1e535bad735f2473253b8a4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 23 May 2022 21:19:59 +0200 Subject: [PATCH 4/8] Added logic to resolve extra columns on visits commands --- module/CLI/config/dependencies.config.php | 6 +-- .../Command/Domain/GetDomainVisitsCommand.php | 19 +++++++++ .../ShortUrl/GetShortUrlVisitsCommand.php | 9 +++++ .../src/Command/Tag/GetTagVisitsCommand.php | 19 +++++++++ .../Visit/AbstractVisitsListCommand.php | 40 +++++++++++++++---- .../Visit/GetNonOrphanVisitsCommand.php | 19 +++++++++ .../Command/Visit/GetOrphanVisitsCommand.php | 9 +++++ module/Core/functions/functions.php | 12 ++++++ 8 files changed, 123 insertions(+), 10 deletions(-) diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 8bebedaf..933affd0 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -99,7 +99,7 @@ return [ LockFactory::class, ], Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class], - Command\Visit\GetNonOrphanVisitsCommand::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], @@ -108,11 +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], + 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], + 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 index 2157416f..00c811c1 100644 --- a/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php +++ b/module/CLI/src/Command/Domain/GetDomainVisitsCommand.php @@ -7,7 +7,10 @@ namespace Shlinkio\Shlink\CLI\Command\Domain; use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Model\VisitsParams; +use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; +use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -15,6 +18,13 @@ class GetDomainVisitsCommand extends AbstractVisitsListCommand { public const NAME = 'domain:visits'; + public function __construct( + VisitsStatsHelperInterface $visitsHelper, + private readonly ShortUrlStringifierInterface $shortUrlStringifier, + ) { + parent::__construct($visitsHelper); + } + protected function doConfigure(): void { $this @@ -28,4 +38,13 @@ class GetDomainVisitsCommand extends AbstractVisitsListCommand $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 index 2683cfd1..49c390f8 100644 --- a/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php +++ b/module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl; use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\VisitsParams; use Symfony\Component\Console\Input\InputArgument; @@ -47,4 +48,12 @@ class GetShortUrlVisitsCommand extends AbstractVisitsListCommand $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/Tag/GetTagVisitsCommand.php b/module/CLI/src/Command/Tag/GetTagVisitsCommand.php index 2d23d0f4..ac0157bc 100644 --- a/module/CLI/src/Command/Tag/GetTagVisitsCommand.php +++ b/module/CLI/src/Command/Tag/GetTagVisitsCommand.php @@ -7,7 +7,10 @@ namespace Shlinkio\Shlink\CLI\Command\Tag; use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Model\VisitsParams; +use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; +use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -15,6 +18,13 @@ class GetTagVisitsCommand extends AbstractVisitsListCommand { public const NAME = 'tag:visits'; + public function __construct( + VisitsStatsHelperInterface $visitsHelper, + private readonly ShortUrlStringifierInterface $shortUrlStringifier, + ) { + parent::__construct($visitsHelper); + } + protected function doConfigure(): void { $this @@ -28,4 +38,13 @@ class GetTagVisitsCommand extends AbstractVisitsListCommand $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/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php index 3ed69c7f..257c7f26 100644 --- a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php +++ b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php @@ -14,9 +14,11 @@ use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function array_keys; use function Functional\map; use function Functional\select_keys; use function Shlinkio\Shlink\Common\buildDateRange; +use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly; use function sprintf; abstract class AbstractVisitsListCommand extends AbstractWithDateRangeCommand @@ -41,17 +43,41 @@ abstract class AbstractVisitsListCommand extends AbstractWithDateRangeCommand $startDate = $this->getStartDateOption($input, $output); $endDate = $this->getEndDateOption($input, $output); $paginator = $this->getVisitsPaginator($input, buildDateRange($startDate, $endDate)); + [$rows, $headers] = $this->resolveRowsAndHeaders($paginator); - $rows = map($paginator->getCurrentPageResults(), function (Visit $visit) { - $rowData = $visit->jsonSerialize(); - $rowData['country'] = $visit->getVisitLocation()?->getCountryName() ?? 'Unknown'; - $rowData['city'] = $visit->getVisitLocation()?->getCityName() ?? 'Unknown'; - return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city']); - }); - ShlinkTable::default($output)->render(['Referer', 'Date', 'User agent', 'Country', 'City'], $rows); + 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 index 1c99619c..76c35990 100644 --- a/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php +++ b/module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php @@ -6,13 +6,23 @@ namespace Shlinkio\Shlink\CLI\Command\Visit; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Model\VisitsParams; +use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; +use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Symfony\Component\Console\Input\InputInterface; class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand { public const NAME = 'visit:non-orphan'; + public function __construct( + VisitsStatsHelperInterface $visitsHelper, + private readonly ShortUrlStringifierInterface $shortUrlStringifier, + ) { + parent::__construct($visitsHelper); + } + protected function doConfigure(): void { $this @@ -24,4 +34,13 @@ class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand { 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 index 561fe8ff..ec675a69 100644 --- a/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php +++ b/module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php @@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Model\VisitsParams; use Symfony\Component\Console\Input\InputInterface; @@ -24,4 +25,12 @@ class GetOrphanVisitsCommand extends AbstractVisitsListCommand { return $this->visitsHelper->orphanVisits(new VisitsParams($dateRange)); } + + /** + * @return array + */ + protected function mapExtraFields(Visit $visit): array + { + return ['type' => $visit->type()->value]; + } } 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)); +} From fba7b362454d4ecb40dc4b578de51bb2fd9ddda9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 24 May 2022 17:43:13 +0200 Subject: [PATCH 5/8] Improved GetShortUrlVisitsCommand test --- ...t.php => GetShortUrlVisitsCommandTest.php} | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) rename module/CLI/test/Command/ShortUrl/{GetVisitsCommandTest.php => GetShortUrlVisitsCommandTest.php} (77%) diff --git a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php similarity index 77% rename from module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php rename to module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php index 93770d55..d5709da6 100644 --- a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php @@ -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; @@ -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,23 +100,32 @@ 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', '', 'Madrid', 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('Madrid', $output); - self::assertStringContainsString('bar', $output); + + echo $output; + + self::assertEquals( + <<getDate()->toAtomString()} | bar | Spain | Madrid | + +---------+---------------------------+------------+---------+--------+ + + OUTPUT, + $output, + ); } } From 5201ea4516dbacc84187c3be41d21ae283e50801 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 24 May 2022 17:54:44 +0200 Subject: [PATCH 6/8] Created tests for short-url-visits commands --- .../Domain/GetDomainVisitsCommandTest.php | 71 +++++++++++++++++++ .../ShortUrl/GetShortUrlVisitsCommandTest.php | 2 - .../Command/Tag/GetTagVisitsCommandTest.php | 71 +++++++++++++++++++ .../Visit/GetNonOrphanVisitsCommandTest.php | 70 ++++++++++++++++++ 4 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 module/CLI/test/Command/Domain/GetDomainVisitsCommandTest.php create mode 100644 module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php create mode 100644 module/CLI/test/Command/Visit/GetNonOrphanVisitsCommandTest.php 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/GetShortUrlVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php index d5709da6..076eb9b2 100644 --- a/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php @@ -114,8 +114,6 @@ class GetShortUrlVisitsCommandTest extends TestCase $this->commandTester->execute(['shortCode' => $shortCode]); $output = $this->commandTester->getDisplay(); - echo $output; - self::assertEquals( <<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(); + } +} From 4146835f6f24d39bf670eb1cec1aef8933ee44e5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 24 May 2022 17:59:06 +0200 Subject: [PATCH 7/8] Created GetOrhanVisitsCommand test --- .../Visit/GetOrphanVisitsCommandTest.php | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php 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(); + } +} From fe4237b2b1d7ba5dd177bb0986de39c7f6a1758a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 24 May 2022 18:00:17 +0200 Subject: [PATCH 8/8] Updated changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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*