diff --git a/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php b/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php index bd82cd9d..08327a98 100644 --- a/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php +++ b/module/Core/src/ShortUrl/Transformer/ShortUrlDataTransformer.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Transformer; use Shlinkio\Shlink\Common\Rest\DataTransformerInterface; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; +use Shlinkio\Shlink\Core\Visit\Model\VisitsSummary; use function Functional\invoke; use function Functional\invoke_if; @@ -33,7 +34,10 @@ class ShortUrlDataTransformer implements DataTransformerInterface 'title' => $shortUrl->title(), 'crawlable' => $shortUrl->crawlable(), 'forwardQuery' => $shortUrl->forwardQuery(), - 'visitsSummary' => $this->buildVisitsSummary($shortUrl), + 'visitsSummary' => VisitsSummary::fromTotalAndNonBots( + $shortUrl->getVisitsCount(), + $shortUrl->nonBotVisitsCount(), + ), // Deprecated 'visitsCount' => $shortUrl->getVisitsCount(), @@ -52,16 +56,4 @@ class ShortUrlDataTransformer implements DataTransformerInterface 'maxVisits' => $maxVisits, ]; } - - private function buildVisitsSummary(ShortUrl $shortUrl): array - { - $totalVisits = $shortUrl->getVisitsCount(); - $nonBotVisits = $shortUrl->nonBotVisitsCount(); - - return [ - 'total' => $totalVisits, - 'nonBots' => $nonBotVisits, - 'bots' => $totalVisits - $nonBotVisits, - ]; - } } diff --git a/module/Core/src/Visit/Model/VisitsStats.php b/module/Core/src/Visit/Model/VisitsStats.php index 475a25b5..adac34eb 100644 --- a/module/Core/src/Visit/Model/VisitsStats.php +++ b/module/Core/src/Visit/Model/VisitsStats.php @@ -8,15 +8,34 @@ use JsonSerializable; final class VisitsStats implements JsonSerializable { - public function __construct(private int $visitsCount, private int $orphanVisitsCount) - { + private readonly VisitsSummary $nonOrphanVisitsSummary; + private readonly VisitsSummary $orphanVisitsSummary; + + public function __construct( + int $nonOrphanVisitsTotal, + int $orphanVisitsTotal, + ?int $nonOrphanVisitsNonBots = null, + ?int $orphanVisitsNonBots = null, + ) { + $this->nonOrphanVisitsSummary = VisitsSummary::fromTotalAndNonBots( + $nonOrphanVisitsTotal, + $nonOrphanVisitsNonBots ?? $nonOrphanVisitsTotal, + ); + $this->orphanVisitsSummary = VisitsSummary::fromTotalAndNonBots( + $orphanVisitsTotal, + $orphanVisitsNonBots ?? $orphanVisitsTotal, + ); } public function jsonSerialize(): array { return [ - 'visitsCount' => $this->visitsCount, - 'orphanVisitsCount' => $this->orphanVisitsCount, + 'nonOrphanVisits' => $this->nonOrphanVisitsSummary, + 'orphanVisits' => $this->orphanVisitsSummary, + + // Deprecated + 'visitsCount' => $this->nonOrphanVisitsSummary->total, + 'orphanVisitsCount' => $this->orphanVisitsSummary->total, ]; } } diff --git a/module/Core/src/Visit/Model/VisitsSummary.php b/module/Core/src/Visit/Model/VisitsSummary.php new file mode 100644 index 00000000..654170cb --- /dev/null +++ b/module/Core/src/Visit/Model/VisitsSummary.php @@ -0,0 +1,28 @@ + $this->total, + 'nonBots' => $this->nonBots, + 'bots' => $this->total - $this->nonBots, + ]; + } +} diff --git a/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php b/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php index c181665e..4e6e4daf 100644 --- a/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php +++ b/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php @@ -12,26 +12,25 @@ use Shlinkio\Shlink\Core\Visit\Repository\VisitRepositoryInterface; class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter { - public function __construct(private VisitRepositoryInterface $repo, private VisitsParams $params) + public function __construct(private readonly VisitRepositoryInterface $repo, private readonly VisitsParams $params) { } protected function doCount(): int { return $this->repo->countOrphanVisits(new VisitsCountFiltering( - $this->params->dateRange, - $this->params->excludeBots, + dateRange: $this->params->dateRange, + excludeBots: $this->params->excludeBots, )); } public function getSlice(int $offset, int $length): iterable { return $this->repo->findOrphanVisits(new VisitsListFiltering( - $this->params->dateRange, - $this->params->excludeBots, - null, - $length, - $offset, + dateRange: $this->params->dateRange, + excludeBots: $this->params->excludeBots, + limit: $length, + offset: $offset, )); } } diff --git a/module/Core/src/Visit/Persistence/VisitsCountFiltering.php b/module/Core/src/Visit/Persistence/VisitsCountFiltering.php index f839a945..c445200e 100644 --- a/module/Core/src/Visit/Persistence/VisitsCountFiltering.php +++ b/module/Core/src/Visit/Persistence/VisitsCountFiltering.php @@ -18,6 +18,6 @@ class VisitsCountFiltering public static function withApiKey(?ApiKey $apiKey): self { - return new self(null, false, $apiKey); + return new self(apiKey: $apiKey); } } diff --git a/module/Core/src/Visit/VisitsStatsHelper.php b/module/Core/src/Visit/VisitsStatsHelper.php index dcba7030..2f28f0dd 100644 --- a/module/Core/src/Visit/VisitsStatsHelper.php +++ b/module/Core/src/Visit/VisitsStatsHelper.php @@ -32,7 +32,7 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey; class VisitsStatsHelper implements VisitsStatsHelperInterface { - public function __construct(private EntityManagerInterface $em) + public function __construct(private readonly EntityManagerInterface $em) { } @@ -42,13 +42,17 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface $visitsRepo = $this->em->getRepository(Visit::class); return new VisitsStats( - $visitsRepo->countNonOrphanVisits(VisitsCountFiltering::withApiKey($apiKey)), - $visitsRepo->countOrphanVisits(new VisitsCountFiltering()), + nonOrphanVisitsTotal: $visitsRepo->countNonOrphanVisits(VisitsCountFiltering::withApiKey($apiKey)), + orphanVisitsTotal: $visitsRepo->countOrphanVisits(new VisitsCountFiltering()), + nonOrphanVisitsNonBots: $visitsRepo->countNonOrphanVisits( + new VisitsCountFiltering(excludeBots: true, apiKey: $apiKey), + ), + orphanVisitsNonBots: $visitsRepo->countOrphanVisits(new VisitsCountFiltering(excludeBots: true)), ); } /** - * @return Visit[]|Paginator + * @return Paginator * @throws ShortUrlNotFoundException */ public function visitsForShortUrl( diff --git a/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php b/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php index c7a4ecd0..cda8fe98 100644 --- a/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php +++ b/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php @@ -14,6 +14,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Model\Visitor; +use Shlinkio\Shlink\Core\Visit\Model\VisitsSummary; use Shlinkio\Shlink\Core\Visit\Model\VisitType; use Shlinkio\Shlink\Core\Visit\Transformer\OrphanVisitDataTransformer; @@ -63,11 +64,7 @@ class PublishingUpdatesGeneratorTest extends TestCase 'title' => $title, 'crawlable' => false, 'forwardQuery' => true, - 'visitsSummary' => [ - 'total' => 0, - 'nonBots' => 0, - 'bots' => 0, - ], + 'visitsSummary' => VisitsSummary::fromTotalAndNonBots(0, 0), ], 'visit' => [ 'referer' => '', @@ -144,11 +141,7 @@ class PublishingUpdatesGeneratorTest extends TestCase 'title' => $shortUrl->title(), 'crawlable' => false, 'forwardQuery' => true, - 'visitsSummary' => [ - 'total' => 0, - 'nonBots' => 0, - 'bots' => 0, - ], + 'visitsSummary' => VisitsSummary::fromTotalAndNonBots(0, 0), ]], $update->payload); } } diff --git a/module/Core/test/Visit/VisitsStatsHelperTest.php b/module/Core/test/Visit/VisitsStatsHelperTest.php index 8afd56db..1774ba6a 100644 --- a/module/Core/test/Visit/VisitsStatsHelperTest.php +++ b/module/Core/test/Visit/VisitsStatsHelperTest.php @@ -53,11 +53,13 @@ class VisitsStatsHelperTest extends TestCase public function returnsExpectedVisitsStats(int $expectedCount): void { $repo = $this->createMock(VisitRepository::class); - $repo->expects($this->once())->method('countNonOrphanVisits')->with(new VisitsCountFiltering())->willReturn( - $expectedCount * 3, - ); - $repo->expects($this->once())->method('countOrphanVisits')->with( - $this->isInstanceOf(VisitsCountFiltering::class), + $repo->expects($this->exactly(2))->method('countNonOrphanVisits')->withConsecutive( + [new VisitsCountFiltering()], + [new VisitsCountFiltering(excludeBots: true)], + )->willReturn($expectedCount * 3); + $repo->expects($this->exactly(2))->method('countOrphanVisits')->withConsecutive( + [$this->isInstanceOf(VisitsCountFiltering::class)], + [$this->isInstanceOf(VisitsCountFiltering::class)], )->willReturn($expectedCount); $this->em->expects($this->once())->method('getRepository')->with(Visit::class)->willReturn($repo);