From ce0f61b66d67a2e1e202d6c592f3f72e69d04644 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 12 Apr 2024 18:29:55 +0200 Subject: [PATCH] Allow filtering by date in VisitIterationRepository --- module/Core/config/dependencies.config.php | 4 +-- .../src/Visit/Geolocation/VisitLocator.php | 4 +-- ...itory.php => VisitIterationRepository.php} | 18 ++++++++-- ... => VisitIterationRepositoryInterface.php} | 5 +-- ...t.php => VisitIterationRepositoryTest.php} | 33 ++++++++++++++++--- .../Visit/Geolocation/VisitLocatorTest.php | 6 ++-- 6 files changed, 54 insertions(+), 16 deletions(-) rename module/Core/src/Visit/Repository/{VisitLocationRepository.php => VisitIterationRepository.php} (72%) rename module/Core/src/Visit/Repository/{VisitLocationRepositoryInterface.php => VisitIterationRepositoryInterface.php} (71%) rename module/Core/test-db/Visit/Repository/{VisitLocationRepositoryTest.php => VisitIterationRepositoryTest.php} (56%) diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index efdd7d33..8a333824 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -73,7 +73,7 @@ return [ Visit\Geolocation\VisitLocator::class => ConfigAbstractFactory::class, Visit\Geolocation\VisitToLocationHelper::class => ConfigAbstractFactory::class, Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class, - Visit\Repository\VisitLocationRepository::class => [ + Visit\Repository\VisitIterationRepository::class => [ EntityRepositoryFactory::class, Visit\Entity\Visit::class, ], @@ -146,7 +146,7 @@ return [ ShortUrl\Repository\ShortUrlListRepository::class, Options\UrlShortenerOptions::class, ], - Visit\Geolocation\VisitLocator::class => ['em', Visit\Repository\VisitLocationRepository::class], + Visit\Geolocation\VisitLocator::class => ['em', Visit\Repository\VisitIterationRepository::class], Visit\Geolocation\VisitToLocationHelper::class => [IpLocationResolverInterface::class], Visit\VisitsStatsHelper::class => ['em'], Tag\TagService::class => ['em'], diff --git a/module/Core/src/Visit/Geolocation/VisitLocator.php b/module/Core/src/Visit/Geolocation/VisitLocator.php index 4b3b8e22..63cb6137 100644 --- a/module/Core/src/Visit/Geolocation/VisitLocator.php +++ b/module/Core/src/Visit/Geolocation/VisitLocator.php @@ -8,14 +8,14 @@ use Doctrine\ORM\EntityManagerInterface; use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation; -use Shlinkio\Shlink\Core\Visit\Repository\VisitLocationRepositoryInterface; +use Shlinkio\Shlink\Core\Visit\Repository\VisitIterationRepositoryInterface; use Shlinkio\Shlink\IpGeolocation\Model\Location; class VisitLocator implements VisitLocatorInterface { public function __construct( private readonly EntityManagerInterface $em, - private readonly VisitLocationRepositoryInterface $repo, + private readonly VisitIterationRepositoryInterface $repo, ) { } diff --git a/module/Core/src/Visit/Repository/VisitLocationRepository.php b/module/Core/src/Visit/Repository/VisitIterationRepository.php similarity index 72% rename from module/Core/src/Visit/Repository/VisitLocationRepository.php rename to module/Core/src/Visit/Repository/VisitIterationRepository.php index 6db1a4f8..0431788c 100644 --- a/module/Core/src/Visit/Repository/VisitLocationRepository.php +++ b/module/Core/src/Visit/Repository/VisitIterationRepository.php @@ -6,9 +6,14 @@ namespace Shlinkio\Shlink\Core\Visit\Repository; use Doctrine\ORM\QueryBuilder; use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; +use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType; +use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Visit\Entity\Visit; -class VisitLocationRepository extends EntitySpecificationRepository implements VisitLocationRepositoryInterface +/** + * Allows iterating large amounts of visits in a memory-efficient way, to use in batch processes + */ +class VisitIterationRepository extends EntitySpecificationRepository implements VisitIterationRepositoryInterface { /** * @return iterable @@ -42,9 +47,18 @@ class VisitLocationRepository extends EntitySpecificationRepository implements V /** * @return iterable */ - public function findAllVisits(int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable + public function findAllVisits(?DateRange $dateRange = null, int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable { $qb = $this->createQueryBuilder('v'); + if ($dateRange?->startDate !== null) { + $qb->andWhere($qb->expr()->gte('v.date', ':since')); + $qb->setParameter('since', $dateRange->startDate, ChronosDateTimeType::CHRONOS_DATETIME); + } + if ($dateRange?->endDate !== null) { + $qb->andWhere($qb->expr()->lte('v.date', ':until')); + $qb->setParameter('until', $dateRange->endDate, ChronosDateTimeType::CHRONOS_DATETIME); + } + return $this->visitsIterableForQuery($qb, $blockSize); } diff --git a/module/Core/src/Visit/Repository/VisitLocationRepositoryInterface.php b/module/Core/src/Visit/Repository/VisitIterationRepositoryInterface.php similarity index 71% rename from module/Core/src/Visit/Repository/VisitLocationRepositoryInterface.php rename to module/Core/src/Visit/Repository/VisitIterationRepositoryInterface.php index 083d61f2..d4ffb864 100644 --- a/module/Core/src/Visit/Repository/VisitLocationRepositoryInterface.php +++ b/module/Core/src/Visit/Repository/VisitIterationRepositoryInterface.php @@ -4,9 +4,10 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Visit\Repository; +use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Visit\Entity\Visit; -interface VisitLocationRepositoryInterface +interface VisitIterationRepositoryInterface { public const DEFAULT_BLOCK_SIZE = 10000; @@ -23,5 +24,5 @@ interface VisitLocationRepositoryInterface /** * @return iterable */ - public function findAllVisits(int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable; + public function findAllVisits(?DateRange $dateRange = null, int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable; } diff --git a/module/Core/test-db/Visit/Repository/VisitLocationRepositoryTest.php b/module/Core/test-db/Visit/Repository/VisitIterationRepositoryTest.php similarity index 56% rename from module/Core/test-db/Visit/Repository/VisitLocationRepositoryTest.php rename to module/Core/test-db/Visit/Repository/VisitIterationRepositoryTest.php index c5aadf1f..7a683e3c 100644 --- a/module/Core/test-db/Visit/Repository/VisitLocationRepositoryTest.php +++ b/module/Core/test-db/Visit/Repository/VisitIterationRepositoryTest.php @@ -4,27 +4,29 @@ declare(strict_types=1); namespace ShlinkioDbTest\Shlink\Core\Visit\Repository; +use Cake\Chronos\Chronos; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation; use Shlinkio\Shlink\Core\Visit\Model\Visitor; -use Shlinkio\Shlink\Core\Visit\Repository\VisitLocationRepository; +use Shlinkio\Shlink\Core\Visit\Repository\VisitIterationRepository; use Shlinkio\Shlink\IpGeolocation\Model\Location; use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase; use function array_map; use function range; -class VisitLocationRepositoryTest extends DatabaseTestCase +class VisitIterationRepositoryTest extends DatabaseTestCase { - private VisitLocationRepository $repo; + private VisitIterationRepository $repo; protected function setUp(): void { $em = $this->getEntityManager(); - $this->repo = new VisitLocationRepository($em, $em->getClassMetadata(Visit::class)); + $this->repo = new VisitIterationRepository($em, $em->getClassMetadata(Visit::class)); } #[Test, DataProvider('provideBlockSize')] @@ -33,7 +35,9 @@ class VisitLocationRepositoryTest extends DatabaseTestCase $shortUrl = ShortUrl::createFake(); $this->getEntityManager()->persist($shortUrl); + $unmodifiedDate = Chronos::now(); for ($i = 0; $i < 6; $i++) { + Chronos::setTestNow($unmodifiedDate->subDays($i)); // Enforce a different day for every visit $visit = Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()); if ($i >= 2) { @@ -44,15 +48,34 @@ class VisitLocationRepositoryTest extends DatabaseTestCase $this->getEntityManager()->persist($visit); } + Chronos::setTestNow(); $this->getEntityManager()->flush(); $withEmptyLocation = $this->repo->findVisitsWithEmptyLocation($blockSize); $unlocated = $this->repo->findUnlocatedVisits($blockSize); - $all = $this->repo->findAllVisits($blockSize); + $all = $this->repo->findAllVisits(blockSize: $blockSize); + $lastThreeDays = $this->repo->findAllVisits( + dateRange: DateRange::since(Chronos::now()->subDays(2)), + blockSize: $blockSize, + ); + $firstTwoDays = $this->repo->findAllVisits( + dateRange: DateRange::until(Chronos::now()->subDays(4)), + blockSize: $blockSize, + ); + $daysInBetween = $this->repo->findAllVisits( + dateRange: DateRange::between( + startDate: Chronos::now()->subDays(5), + endDate: Chronos::now()->subDays(2), + ), + blockSize: $blockSize, + ); self::assertCount(2, [...$unlocated]); self::assertCount(4, [...$withEmptyLocation]); self::assertCount(6, [...$all]); + self::assertCount(3, [...$lastThreeDays]); + self::assertCount(2, [...$firstTwoDays]); + self::assertCount(4, [...$daysInBetween]); } public static function provideBlockSize(): iterable diff --git a/module/Core/test/Visit/Geolocation/VisitLocatorTest.php b/module/Core/test/Visit/Geolocation/VisitLocatorTest.php index 1d3af228..f1d86f63 100644 --- a/module/Core/test/Visit/Geolocation/VisitLocatorTest.php +++ b/module/Core/test/Visit/Geolocation/VisitLocatorTest.php @@ -17,7 +17,7 @@ use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation; use Shlinkio\Shlink\Core\Visit\Geolocation\VisitGeolocationHelperInterface; use Shlinkio\Shlink\Core\Visit\Geolocation\VisitLocator; use Shlinkio\Shlink\Core\Visit\Model\Visitor; -use Shlinkio\Shlink\Core\Visit\Repository\VisitLocationRepositoryInterface; +use Shlinkio\Shlink\Core\Visit\Repository\VisitIterationRepositoryInterface; use Shlinkio\Shlink\IpGeolocation\Model\Location; use function array_map; @@ -30,12 +30,12 @@ class VisitLocatorTest extends TestCase { private VisitLocator $visitService; private MockObject & EntityManager $em; - private MockObject & VisitLocationRepositoryInterface $repo; + private MockObject & VisitIterationRepositoryInterface $repo; protected function setUp(): void { $this->em = $this->createMock(EntityManager::class); - $this->repo = $this->createMock(VisitLocationRepositoryInterface::class); + $this->repo = $this->createMock(VisitIterationRepositoryInterface::class); $this->visitService = new VisitLocator($this->em, $this->repo); }