Allow filtering by date in VisitIterationRepository

This commit is contained in:
Alejandro Celaya 2024-04-12 18:29:55 +02:00
parent 13ee71f351
commit ce0f61b66d
6 changed files with 54 additions and 16 deletions

View file

@ -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'],

View file

@ -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,
) {
}

View file

@ -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<Visit>
@ -42,9 +47,18 @@ class VisitLocationRepository extends EntitySpecificationRepository implements V
/**
* @return iterable<Visit>
*/
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);
}

View file

@ -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<Visit>
*/
public function findAllVisits(int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable;
public function findAllVisits(?DateRange $dateRange = null, int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable;
}

View file

@ -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

View file

@ -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);
}