Added new models to pass to repositories when counting visits of any kind

This commit is contained in:
Alejandro Celaya 2021-05-22 20:16:32 +02:00
parent 9fa32b5b6b
commit 6327ed814a
14 changed files with 168 additions and 57 deletions

View File

@ -16,12 +16,18 @@ final class VisitsParams
private ?DateRange $dateRange;
private int $page;
private int $itemsPerPage;
private bool $excludeBots;
public function __construct(?DateRange $dateRange = null, int $page = self::FIRST_PAGE, ?int $itemsPerPage = null)
{
public function __construct(
?DateRange $dateRange = null,
int $page = self::FIRST_PAGE,
?int $itemsPerPage = null,
bool $excludeBots = false
) {
$this->dateRange = $dateRange ?? new DateRange();
$this->page = $page;
$this->itemsPerPage = $this->determineItemsPerPage($itemsPerPage);
$this->excludeBots = $excludeBots;
}
private function determineItemsPerPage(?int $itemsPerPage): int
@ -39,6 +45,7 @@ final class VisitsParams
parseDateRangeFromQuery($query, 'startDate', 'endDate'),
(int) ($query['page'] ?? 1),
isset($query['itemsPerPage']) ? (int) $query['itemsPerPage'] : null,
isset($query['excludeBots']),
);
}
@ -56,4 +63,9 @@ final class VisitsParams
{
return $this->itemsPerPage;
}
public function excludeBots(): bool
{
return $this->excludeBots;
}
}

View File

@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Paginator\Adapter;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
{
@ -20,7 +21,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
protected function doCount(): int
{
return $this->repo->countOrphanVisits($this->params->getDateRange());
return $this->repo->countOrphanVisits(new VisitsCountFiltering($this->params->getDateRange()));
}
public function getSlice($offset, $length): iterable // phpcs:ignore

View File

@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\Paginator\Adapter;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
@ -43,8 +44,11 @@ class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
{
return $this->visitRepository->countVisitsByTag(
$this->tag,
new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->resolveSpec(),
),
);
}

View File

@ -8,6 +8,7 @@ use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
class VisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
{
@ -45,8 +46,11 @@ class VisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
return $this->visitRepository->countVisitsByShortCode(
$this->identifier->shortCode(),
$this->identifier->domain(),
new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->spec,
),
);
}
}

View File

@ -12,6 +12,7 @@ use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Spec\CountOfOrphanVisits;
use Shlinkio\Shlink\Core\Visit\Spec\CountOfShortUrlVisits;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@ -95,13 +96,9 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
return $this->resolveVisitsWithNativeQuery($qb, $limit, $offset);
}
public function countVisitsByShortCode(
string $shortCode,
?string $domain = null,
?DateRange $dateRange = null,
?Specification $spec = null
): int {
$qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange, $spec);
public function countVisitsByShortCode(string $shortCode, ?string $domain, VisitsCountFiltering $filtering): int
{
$qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $filtering->dateRange(), $filtering->spec());
$qb->select('COUNT(v.id)');
return (int) $qb->getQuery()->getSingleScalarResult();
@ -141,9 +138,9 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
return $this->resolveVisitsWithNativeQuery($qb, $limit, $offset);
}
public function countVisitsByTag(string $tag, ?DateRange $dateRange = null, ?Specification $spec = null): int
public function countVisitsByTag(string $tag, VisitsCountFiltering $filtering): int
{
$qb = $this->createVisitsByTagQueryBuilder($tag, $dateRange, $spec);
$qb = $this->createVisitsByTagQueryBuilder($tag, $filtering->dateRange(), $filtering->spec());
$qb->select('COUNT(v.id)');
return (int) $qb->getQuery()->getSingleScalarResult();
@ -181,9 +178,9 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
return $this->resolveVisitsWithNativeQuery($qb, $limit, $offset);
}
public function countOrphanVisits(?DateRange $dateRange = null): int
public function countOrphanVisits(VisitsCountFiltering $filtering): int
{
return (int) $this->matchSingleScalarResult(new CountOfOrphanVisits($dateRange));
return (int) $this->matchSingleScalarResult(new CountOfOrphanVisits($filtering->dateRange()));
}
public function countVisits(?ApiKey $apiKey = null): int

View File

@ -9,6 +9,7 @@ use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepositoryInterfa
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
interface VisitRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
@ -42,12 +43,7 @@ interface VisitRepositoryInterface extends ObjectRepository, EntitySpecification
?Specification $spec = null
): array;
public function countVisitsByShortCode(
string $shortCode,
?string $domain = null,
?DateRange $dateRange = null,
?Specification $spec = null
): int;
public function countVisitsByShortCode(string $shortCode, ?string $domain, VisitsCountFiltering $filtering): int;
/**
* @return Visit[]
@ -60,14 +56,14 @@ interface VisitRepositoryInterface extends ObjectRepository, EntitySpecification
?Specification $spec = null
): array;
public function countVisitsByTag(string $tag, ?DateRange $dateRange = null, ?Specification $spec = null): int;
public function countVisitsByTag(string $tag, VisitsCountFiltering $filtering): int;
/**
* @return Visit[]
*/
public function findOrphanVisits(?DateRange $dateRange = null, ?int $limit = null, ?int $offset = null): array;
public function countOrphanVisits(?DateRange $dateRange = null): int;
public function countOrphanVisits(VisitsCountFiltering $filtering): int;
public function countVisits(?ApiKey $apiKey = null): int;
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Persistence;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange;
class VisitsCountFiltering
{
private ?DateRange $dateRange;
private bool $excludeBots;
private ?Specification $spec;
public function __construct(?DateRange $dateRange = null, bool $excludeBots = false, ?Specification $spec = null)
{
$this->dateRange = $dateRange;
$this->excludeBots = $excludeBots;
$this->spec = $spec;
}
public function dateRange(): ?DateRange
{
return $this->dateRange;
}
public function excludeBots(): bool
{
return $this->excludeBots;
}
public function spec(): ?Specification
{
return $this->spec;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Persistence;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange;
final class VisitsListFiltering extends VisitsCountFiltering
{
private ?int $limit;
private ?int $offset;
public function __construct(
?DateRange $dateRange = null,
bool $excludeBots = false,
?Specification $spec = null,
?int $limit = null,
?int $offset = null
) {
parent::__construct($dateRange, $excludeBots, $spec);
$this->limit = $limit;
$this->offset = $offset;
}
public function limit(): ?int
{
return $this->limit;
}
public function offset(): ?int
{
return $this->offset;
}
}

View File

@ -22,6 +22,7 @@ use Shlinkio\Shlink\Core\Repository\TagRepository;
use Shlinkio\Shlink\Core\Repository\VisitRepository;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Model\VisitsStats;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsStatsHelper implements VisitsStatsHelperInterface
@ -38,7 +39,10 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
/** @var VisitRepository $visitsRepo */
$visitsRepo = $this->em->getRepository(Visit::class);
return new VisitsStats($visitsRepo->countVisits($apiKey), $visitsRepo->countOrphanVisits());
return new VisitsStats(
$visitsRepo->countVisits($apiKey),
$visitsRepo->countOrphanVisits(new VisitsCountFiltering()),
);
}
/**

View File

@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Repository\VisitRepository;
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
@ -110,18 +111,17 @@ class VisitRepositoryTest extends DatabaseTestCase
{
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
self::assertEquals(0, $this->repo->countVisitsByShortCode('invalid'));
self::assertEquals(6, $this->repo->countVisitsByShortCode($shortCode));
self::assertEquals(3, $this->repo->countVisitsByShortCode($shortCode, $domain));
self::assertEquals(2, $this->repo->countVisitsByShortCode($shortCode, null, new DateRange(
Chronos::parse('2016-01-02'),
Chronos::parse('2016-01-03'),
self::assertEquals(0, $this->repo->countVisitsByShortCode('invalid', null, new VisitsCountFiltering()));
self::assertEquals(6, $this->repo->countVisitsByShortCode($shortCode, null, new VisitsCountFiltering()));
self::assertEquals(3, $this->repo->countVisitsByShortCode($shortCode, $domain, new VisitsCountFiltering()));
self::assertEquals(2, $this->repo->countVisitsByShortCode($shortCode, null, new VisitsCountFiltering(
DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
)));
self::assertEquals(4, $this->repo->countVisitsByShortCode($shortCode, null, new DateRange(
Chronos::parse('2016-01-03'),
self::assertEquals(4, $this->repo->countVisitsByShortCode($shortCode, null, new VisitsCountFiltering(
DateRange::withStartDate(Chronos::parse('2016-01-03')),
)));
self::assertEquals(1, $this->repo->countVisitsByShortCode($shortCode, $domain, new DateRange(
Chronos::parse('2016-01-03'),
self::assertEquals(1, $this->repo->countVisitsByShortCode($shortCode, $domain, new VisitsCountFiltering(
DateRange::withStartDate(Chronos::parse('2016-01-03')),
)));
}
@ -160,13 +160,14 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->createShortUrlsAndVisits(false, [$foo]);
$this->getEntityManager()->flush();
self::assertEquals(0, $this->repo->countVisitsByTag('invalid'));
self::assertEquals(12, $this->repo->countVisitsByTag($foo));
self::assertEquals(4, $this->repo->countVisitsByTag($foo, new DateRange(
Chronos::parse('2016-01-02'),
Chronos::parse('2016-01-03'),
self::assertEquals(0, $this->repo->countVisitsByTag('invalid', new VisitsCountFiltering()));
self::assertEquals(12, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering()));
self::assertEquals(4, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering(
DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
)));
self::assertEquals(8, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering(
DateRange::withStartDate(Chronos::parse('2016-01-03')),
)));
self::assertEquals(8, $this->repo->countVisitsByTag($foo, new DateRange(Chronos::parse('2016-01-03'))));
}
/** @test */
@ -213,7 +214,7 @@ class VisitRepositoryTest extends DatabaseTestCase
self::assertEquals(4, $this->repo->countVisits($apiKey1));
self::assertEquals(5 + 7, $this->repo->countVisits($apiKey2));
self::assertEquals(4 + 7, $this->repo->countVisits($domainApiKey));
self::assertEquals(3, $this->repo->countOrphanVisits());
self::assertEquals(3, $this->repo->countOrphanVisits(new VisitsCountFiltering()));
}
/** @test */
@ -276,13 +277,17 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->flush();
self::assertEquals(18, $this->repo->countOrphanVisits());
self::assertEquals(18, $this->repo->countOrphanVisits(DateRange::emptyInstance()));
self::assertEquals(9, $this->repo->countOrphanVisits(DateRange::withStartDate(Chronos::parse('2020-01-04'))));
self::assertEquals(6, $this->repo->countOrphanVisits(
DateRange::withStartAndEndDate(Chronos::parse('2020-01-02'), Chronos::parse('2020-01-03')),
self::assertEquals(18, $this->repo->countOrphanVisits(new VisitsCountFiltering()));
self::assertEquals(18, $this->repo->countOrphanVisits(new VisitsCountFiltering(DateRange::emptyInstance())));
self::assertEquals(9, $this->repo->countOrphanVisits(
new VisitsCountFiltering(DateRange::withStartDate(Chronos::parse('2020-01-04'))),
));
self::assertEquals(6, $this->repo->countOrphanVisits(new VisitsCountFiltering(
DateRange::withStartAndEndDate(Chronos::parse('2020-01-02'), Chronos::parse('2020-01-03')),
)));
self::assertEquals(3, $this->repo->countOrphanVisits(
new VisitsCountFiltering(DateRange::withEndDate(Chronos::parse('2020-01-01'))),
));
self::assertEquals(3, $this->repo->countOrphanVisits(DateRange::withEndDate(Chronos::parse('2020-01-01'))));
}
private function createShortUrlsAndVisits(bool $withDomain = true, array $tags = []): array

View File

@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\OrphanVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
class OrphanVisitsPaginatorAdapterTest extends TestCase
{
@ -32,7 +33,9 @@ class OrphanVisitsPaginatorAdapterTest extends TestCase
public function countDelegatesToRepository(): void
{
$expectedCount = 5;
$repoCount = $this->repo->countOrphanVisits($this->params->getDateRange())->willReturn($expectedCount);
$repoCount = $this->repo->countOrphanVisits(
new VisitsCountFiltering($this->params->getDateRange()),
)->willReturn($expectedCount);
$result = $this->adapter->getNbResults();

View File

@ -11,6 +11,7 @@ use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsForTagPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsForTagPaginatorAdapterTest extends TestCase
@ -46,7 +47,10 @@ class VisitsForTagPaginatorAdapterTest extends TestCase
$count = 3;
$apiKey = ApiKey::create();
$adapter = $this->createAdapter($apiKey);
$countVisits = $this->repo->countVisitsByTag('foo', new DateRange(), $apiKey->spec())->willReturn(3);
$countVisits = $this->repo->countVisitsByTag(
'foo',
new VisitsCountFiltering(DateRange::emptyInstance(), false, $apiKey->spec()),
)->willReturn(3);
for ($i = 0; $i < $count; $i++) {
$adapter->getNbResults();

View File

@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsPaginatorAdapterTest extends TestCase
@ -49,7 +50,11 @@ class VisitsPaginatorAdapterTest extends TestCase
$count = 3;
$apiKey = ApiKey::create();
$adapter = $this->createAdapter($apiKey);
$countVisits = $this->repo->countVisitsByShortCode('', null, new DateRange(), $apiKey->spec())->willReturn(3);
$countVisits = $this->repo->countVisitsByShortCode(
'',
null,
new VisitsCountFiltering(new DateRange(), false, $apiKey->spec()),
)->willReturn(3);
for ($i = 0; $i < $count; $i++) {
$adapter->getNbResults();

View File

@ -23,6 +23,7 @@ use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
use Shlinkio\Shlink\Core\Repository\TagRepository;
use Shlinkio\Shlink\Core\Repository\VisitRepository;
use Shlinkio\Shlink\Core\Visit\Model\VisitsStats;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelper;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use ShlinkioTest\Shlink\Core\Util\ApiKeyHelpersTrait;
@ -53,7 +54,9 @@ class VisitsStatsHelperTest extends TestCase
{
$repo = $this->prophesize(VisitRepository::class);
$count = $repo->countVisits(null)->willReturn($expectedCount * 3);
$countOrphan = $repo->countOrphanVisits()->willReturn($expectedCount);
$countOrphan = $repo->countOrphanVisits(Argument::type(VisitsCountFiltering::class))->willReturn(
$expectedCount,
);
$getRepo = $this->em->getRepository(Visit::class)->willReturn($repo->reveal());
$stats = $this->helper->getVisitsStats();
@ -86,7 +89,7 @@ class VisitsStatsHelperTest extends TestCase
$repo2->findVisitsByShortCode($shortCode, null, Argument::type(DateRange::class), 1, 0, $spec)->willReturn(
$list,
);
$repo2->countVisitsByShortCode($shortCode, null, Argument::type(DateRange::class), $spec)->willReturn(1);
$repo2->countVisitsByShortCode($shortCode, null, Argument::type(VisitsCountFiltering::class))->willReturn(1);
$this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce();
$paginator = $this->helper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), new VisitsParams(), $apiKey);
@ -140,7 +143,7 @@ class VisitsStatsHelperTest extends TestCase
$list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance()));
$repo2 = $this->prophesize(VisitRepository::class);
$repo2->findVisitsByTag($tag, Argument::type(DateRange::class), 1, 0, $spec)->willReturn($list);
$repo2->countVisitsByTag($tag, Argument::type(DateRange::class), $spec)->willReturn(1);
$repo2->countVisitsByTag($tag, Argument::type(VisitsCountFiltering::class))->willReturn(1);
$this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce();
$paginator = $this->helper->visitsForTag($tag, new VisitsParams(), $apiKey);
@ -155,7 +158,7 @@ class VisitsStatsHelperTest extends TestCase
{
$list = map(range(0, 3), fn () => Visit::forBasePath(Visitor::emptyInstance()));
$repo = $this->prophesize(VisitRepository::class);
$countVisits = $repo->countOrphanVisits(Argument::type(DateRange::class))->willReturn(count($list));
$countVisits = $repo->countOrphanVisits(Argument::type(VisitsCountFiltering::class))->willReturn(count($list));
$listVisits = $repo->findOrphanVisits(Argument::type(DateRange::class), Argument::cetera())->willReturn($list);
$getRepo = $this->em->getRepository(Visit::class)->willReturn($repo->reveal());