From 353ac0fc0c50c92de1e535bad735f2473253b8a4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 23 May 2022 21:19:59 +0200 Subject: [PATCH] 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)); +}