From 55e2780f50619255114e5859a50ff823aa1d9eaa Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Mar 2024 10:33:31 +0200 Subject: [PATCH] Load non-orphan visits overview via short url visits counts --- CHANGELOG.md | 2 +- ....Core.Visit.Entity.ShortUrlVisitsCount.php | 3 +- .../ShortUrlVisitsCountRepository.php | 31 +++++++++++++++++++ ...ShortUrlVisitsCountRepositoryInterface.php | 12 +++++++ .../src/Visit/Spec/CountOfNonOrphanVisits.php | 2 +- module/Core/src/Visit/VisitsStatsHelper.php | 12 ++++--- .../Core/test/Visit/VisitsStatsHelperTest.php | 16 +++++++--- 7 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 module/Core/src/Visit/Repository/ShortUrlVisitsCountRepository.php create mode 100644 module/Core/src/Visit/Repository/ShortUrlVisitsCountRepositoryInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b5f5cff..0dfd04c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * *Nothing* ### Fixed -* *Nothing* +* Fix error when importing short URLs and visits from a Shlink 4.x instance ## [4.0.3] - 2024-03-15 diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Visit.Entity.ShortUrlVisitsCount.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Visit.Entity.ShortUrlVisitsCount.php index d4a8546b..07977a50 100644 --- a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Visit.Entity.ShortUrlVisitsCount.php +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Visit.Entity.ShortUrlVisitsCount.php @@ -11,7 +11,8 @@ use Doctrine\ORM\Mapping\ClassMetadata; return static function (ClassMetadata $metadata, array $emConfig): void { $builder = new ClassMetadataBuilder($metadata); - $builder->setTable(determineTableName('short_url_visits_counts', $emConfig)); + $builder->setTable(determineTableName('short_url_visits_counts', $emConfig)) + ->setCustomRepositoryClass(Visit\Repository\ShortUrlVisitsCountRepository::class); $builder->createField('id', Types::BIGINT) ->columnName('id') diff --git a/module/Core/src/Visit/Repository/ShortUrlVisitsCountRepository.php b/module/Core/src/Visit/Repository/ShortUrlVisitsCountRepository.php new file mode 100644 index 00000000..f07aab33 --- /dev/null +++ b/module/Core/src/Visit/Repository/ShortUrlVisitsCountRepository.php @@ -0,0 +1,31 @@ +getEntityManager()->createQueryBuilder(); + $qb->select('COALESCE(SUM(vc.count), 0)') + ->from(ShortUrlVisitsCount::class, 'vc') + ->join('vc.shortUrl', 's'); + + + if ($filtering->excludeBots) { + $qb->andWhere($qb->expr()->eq('vc.potentialBot', ':potentialBot')) + ->setParameter('potentialBot', false); + } + + $this->applySpecification($qb, $filtering->apiKey?->spec(), 's'); + + return (int) $qb->getQuery()->getSingleScalarResult(); + } +} diff --git a/module/Core/src/Visit/Repository/ShortUrlVisitsCountRepositoryInterface.php b/module/Core/src/Visit/Repository/ShortUrlVisitsCountRepositoryInterface.php new file mode 100644 index 00000000..fd966039 --- /dev/null +++ b/module/Core/src/Visit/Repository/ShortUrlVisitsCountRepositoryInterface.php @@ -0,0 +1,12 @@ +em->getRepository(Visit::class); + /** @var ShortUrlVisitsCountRepository $visitsCountRepo */ + $visitsCountRepo = $this->em->getRepository(ShortUrlVisitsCount::class); return new VisitsStats( - nonOrphanVisitsTotal: $visitsRepo->countNonOrphanVisits(new VisitsCountFiltering(apiKey: $apiKey)), + nonOrphanVisitsTotal: $visitsCountRepo->countNonOrphanVisits(new VisitsCountFiltering(apiKey: $apiKey)), orphanVisitsTotal: $visitsRepo->countOrphanVisits(new OrphanVisitsCountFiltering(apiKey: $apiKey)), - nonOrphanVisitsNonBots: $visitsRepo->countNonOrphanVisits( + nonOrphanVisitsNonBots: $visitsCountRepo->countNonOrphanVisits( new VisitsCountFiltering(excludeBots: true, apiKey: $apiKey), ), orphanVisitsNonBots: $visitsRepo->countOrphanVisits( diff --git a/module/Core/test/Visit/VisitsStatsHelperTest.php b/module/Core/test/Visit/VisitsStatsHelperTest.php index f6bb5464..e109852a 100644 --- a/module/Core/test/Visit/VisitsStatsHelperTest.php +++ b/module/Core/test/Visit/VisitsStatsHelperTest.php @@ -22,6 +22,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository; use Shlinkio\Shlink\Core\Tag\Entity\Tag; use Shlinkio\Shlink\Core\Tag\Repository\TagRepository; +use Shlinkio\Shlink\Core\Visit\Entity\ShortUrlVisitsCount; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams; use Shlinkio\Shlink\Core\Visit\Model\Visitor; @@ -31,6 +32,7 @@ use Shlinkio\Shlink\Core\Visit\Persistence\OrphanVisitsCountFiltering; use Shlinkio\Shlink\Core\Visit\Persistence\OrphanVisitsListFiltering; use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering; use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering; +use Shlinkio\Shlink\Core\Visit\Repository\ShortUrlVisitsCountRepository; use Shlinkio\Shlink\Core\Visit\Repository\VisitRepository; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelper; use Shlinkio\Shlink\Rest\Entity\ApiKey; @@ -54,9 +56,9 @@ class VisitsStatsHelperTest extends TestCase #[Test, DataProvider('provideCounts')] public function returnsExpectedVisitsStats(int $expectedCount, ?ApiKey $apiKey): void { - $repo = $this->createMock(VisitRepository::class); $callCount = 0; - $repo->expects($this->exactly(2))->method('countNonOrphanVisits')->willReturnCallback( + $visitsCountRepo = $this->createMock(ShortUrlVisitsCountRepository::class); + $visitsCountRepo->expects($this->exactly(2))->method('countNonOrphanVisits')->willReturnCallback( function (VisitsCountFiltering $options) use ($expectedCount, $apiKey, &$callCount) { Assert::assertEquals($callCount !== 0, $options->excludeBots); Assert::assertEquals($apiKey, $options->apiKey); @@ -65,10 +67,16 @@ class VisitsStatsHelperTest extends TestCase return $expectedCount * 3; }, ); - $repo->expects($this->exactly(2))->method('countOrphanVisits')->with( + + $visitsRepo = $this->createMock(VisitRepository::class); + $visitsRepo->expects($this->exactly(2))->method('countOrphanVisits')->with( $this->isInstanceOf(VisitsCountFiltering::class), )->willReturn($expectedCount); - $this->em->expects($this->once())->method('getRepository')->with(Visit::class)->willReturn($repo); + + $this->em->expects($this->exactly(2))->method('getRepository')->willReturnMap([ + [Visit::class, $visitsRepo], + [ShortUrlVisitsCount::class, $visitsCountRepo], + ]); $stats = $this->helper->getVisitsStats($apiKey);