Allow tags, orphan and non-orphan visits to be provided a domain filter param

This commit is contained in:
Alejandro Celaya
2025-10-28 10:55:06 +01:00
parent 10dab5be20
commit 14a7e3bb05
26 changed files with 189 additions and 98 deletions

View File

@@ -9,7 +9,7 @@ use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -39,7 +39,7 @@ class GetTagVisitsCommand extends AbstractVisitsListCommand
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
{
$tag = $input->getArgument('tag');
return $this->visitsHelper->visitsForTag($tag, new VisitsParams($dateRange));
return $this->visitsHelper->visitsForTag($tag, new WithDomainVisitsParams($dateRange));
}
/**

View File

@@ -8,7 +8,7 @@ use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Symfony\Component\Console\Input\InputInterface;
@@ -35,7 +35,7 @@ class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand
*/
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
{
return $this->visitsHelper->nonOrphanVisits(new VisitsParams($dateRange));
return $this->visitsHelper->nonOrphanVisits(new WithDomainVisitsParams($dateRange));
}
/**

View File

@@ -8,7 +8,7 @@ use Shlinkio\Shlink\Common\Paginator\Paginator;
abstract class AbstractInfinitePaginableListParams
{
private const FIRST_PAGE = 1;
private const int FIRST_PAGE = 1;
public readonly int $page;
public readonly int $itemsPerPage;

View File

@@ -9,20 +9,22 @@ use ValueError;
use function Shlinkio\Shlink\Core\enumToString;
use function sprintf;
final class OrphanVisitsParams extends VisitsParams
final class OrphanVisitsParams extends WithDomainVisitsParams
{
public function __construct(
DateRange|null $dateRange = null,
int|null $page = null,
int|null $itemsPerPage = null,
bool $excludeBots = false,
string|null $domain = null,
public readonly OrphanVisitType|null $type = null,
) {
parent::__construct($dateRange, $page, $itemsPerPage, $excludeBots);
parent::__construct($dateRange, $page, $itemsPerPage, $excludeBots, $domain);
}
public static function fromVisitsParamsAndRawData(VisitsParams $visitsParams, array $query): self
public static function fromRawData(array $query): self
{
$visitsParams = WithDomainVisitsParams::fromRawData($query);
$type = $query['type'] ?? null;
return new self(
@@ -30,6 +32,7 @@ final class OrphanVisitsParams extends VisitsParams
page: $visitsParams->page,
itemsPerPage: $visitsParams->itemsPerPage,
excludeBots: $visitsParams->excludeBots,
domain: $visitsParams->domain,
type: $type !== null ? self::parseType($type) : null,
);
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Shlinkio\Shlink\Core\Visit\Model;
use Shlinkio\Shlink\Common\Util\DateRange;
class WithDomainVisitsParams extends VisitsParams
{
public function __construct(
DateRange|null $dateRange = null,
int|null $page = null,
int|null $itemsPerPage = null,
bool $excludeBots = false,
public readonly string|null $domain = null,
) {
parent::__construct($dateRange, $page, $itemsPerPage, $excludeBots);
}
public static function fromRawData(array $query): self
{
$visitsParams = VisitsParams::fromRawData($query);
return new self(
dateRange: $visitsParams->dateRange,
page: $visitsParams->page,
itemsPerPage: $visitsParams->itemsPerPage,
excludeBots: $visitsParams->excludeBots,
domain: $query['domain'] ?? null,
);
}
}

View File

@@ -6,9 +6,9 @@ namespace Shlinkio\Shlink\Core\Visit\Paginator\Adapter;
use Shlinkio\Shlink\Core\Paginator\Adapter\AbstractCacheableCountPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -17,26 +17,28 @@ class NonOrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAda
{
public function __construct(
private readonly VisitRepositoryInterface $repo,
private readonly VisitsParams $params,
private readonly WithDomainVisitsParams $params,
private readonly ApiKey|null $apiKey,
) {
}
protected function doCount(): int
{
return $this->repo->countNonOrphanVisits(new VisitsCountFiltering(
return $this->repo->countNonOrphanVisits(new WithDomainVisitsCountFiltering(
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$this->params->domain,
));
}
public function getSlice(int $offset, int $length): iterable
{
return $this->repo->findNonOrphanVisits(new VisitsListFiltering(
return $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$this->params->domain,
$length,
$offset,
));

View File

@@ -28,6 +28,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
dateRange: $this->params->dateRange,
excludeBots: $this->params->excludeBots,
apiKey: $this->apiKey,
domain: $this->params->domain,
type: $this->params->type,
));
}
@@ -38,6 +39,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
dateRange: $this->params->dateRange,
excludeBots: $this->params->excludeBots,
apiKey: $this->apiKey,
domain: $this->params->domain,
type: $this->params->type,
limit: $length,
offset: $offset,

View File

@@ -6,9 +6,9 @@ namespace Shlinkio\Shlink\Core\Visit\Paginator\Adapter;
use Shlinkio\Shlink\Core\Paginator\Adapter\AbstractCacheableCountPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -18,7 +18,7 @@ class TagVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
public function __construct(
private readonly VisitRepositoryInterface $visitRepository,
private readonly string $tag,
private readonly VisitsParams $params,
private readonly WithDomainVisitsParams $params,
private readonly ApiKey|null $apiKey,
) {
}
@@ -27,10 +27,11 @@ class TagVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
{
return $this->visitRepository->findVisitsByTag(
$this->tag,
new VisitsListFiltering(
new WithDomainVisitsListFiltering(
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$this->params->domain,
$length,
$offset,
),
@@ -41,10 +42,11 @@ class TagVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
{
return $this->visitRepository->countVisitsByTag(
$this->tag,
new VisitsCountFiltering(
new WithDomainVisitsCountFiltering(
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$this->params->domain,
),
);
}

View File

@@ -8,14 +8,15 @@ use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitType;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class OrphanVisitsCountFiltering extends VisitsCountFiltering
class OrphanVisitsCountFiltering extends WithDomainVisitsCountFiltering
{
public function __construct(
DateRange|null $dateRange = null,
bool $excludeBots = false,
ApiKey|null $apiKey = null,
string|null $domain = null,
public readonly OrphanVisitType|null $type = null,
) {
parent::__construct($dateRange, $excludeBots, $apiKey);
parent::__construct($dateRange, $excludeBots, $apiKey, $domain);
}
}

View File

@@ -14,10 +14,11 @@ final class OrphanVisitsListFiltering extends OrphanVisitsCountFiltering
DateRange|null $dateRange = null,
bool $excludeBots = false,
ApiKey|null $apiKey = null,
string|null $domain = null,
OrphanVisitType|null $type = null,
public readonly int|null $limit = null,
public readonly int|null $offset = null,
) {
parent::__construct($dateRange, $excludeBots, $apiKey, $type);
parent::__construct($dateRange, $excludeBots, $apiKey, $domain, $type);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Persistence;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class WithDomainVisitsCountFiltering extends VisitsCountFiltering
{
public function __construct(
DateRange|null $dateRange = null,
bool $excludeBots = false,
ApiKey|null $apiKey = null,
public readonly string|null $domain = null,
) {
parent::__construct($dateRange, $excludeBots, $apiKey);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Persistence;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
final class WithDomainVisitsListFiltering extends WithDomainVisitsCountFiltering
{
public function __construct(
DateRange|null $dateRange = null,
bool $excludeBots = false,
ApiKey|null $apiKey = null,
string|null $domain = null,
public readonly int|null $limit = null,
public readonly int|null $offset = null,
) {
parent::__construct($dateRange, $excludeBots, $apiKey, $domain);
}
}

View File

@@ -18,6 +18,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\Persistence\WithDomainVisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Spec\CountOfNonOrphanVisits;
use Shlinkio\Shlink\Core\Visit\Spec\CountOfOrphanVisits;
use Shlinkio\Shlink\Rest\ApiKey\Role;
@@ -69,7 +70,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
return $qb;
}
public function findVisitsByTag(string $tag, VisitsListFiltering $filtering): array
public function findVisitsByTag(string $tag, WithDomainVisitsListFiltering $filtering): array
{
$qb = $this->createVisitsByTagQueryBuilder($tag, $filtering);
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
@@ -173,7 +174,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
/**
* @return Visit[]
*/
public function findNonOrphanVisits(VisitsListFiltering $filtering): array
public function findNonOrphanVisits(WithDomainVisitsListFiltering $filtering): array
{
$qb = $this->createAllVisitsQueryBuilder($filtering);
$qb->andWhere($qb->expr()->isNotNull('v.shortUrl'));
@@ -193,8 +194,9 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
return (int) $this->matchSingleScalarResult(new CountOfNonOrphanVisits($filtering));
}
private function createAllVisitsQueryBuilder(VisitsListFiltering|OrphanVisitsListFiltering $filtering): QueryBuilder
{
private function createAllVisitsQueryBuilder(
VisitsListFiltering|OrphanVisitsListFiltering|WithDomainVisitsListFiltering $filtering,
): QueryBuilder {
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later
// Since they are not provided by the caller, it's reasonably safe
$qb = $this->getEntityManager()->createQueryBuilder();

View File

@@ -12,6 +12,8 @@ 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\Persistence\WithDomainVisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsListFiltering;
/**
* @extends ObjectRepository<Visit>
@@ -28,9 +30,9 @@ interface VisitRepositoryInterface extends ObjectRepository, EntitySpecification
/**
* @return Visit[]
*/
public function findVisitsByTag(string $tag, VisitsListFiltering $filtering): array;
public function findVisitsByTag(string $tag, WithDomainVisitsListFiltering $filtering): array;
public function countVisitsByTag(string $tag, VisitsCountFiltering $filtering): int;
public function countVisitsByTag(string $tag, WithDomainVisitsCountFiltering $filtering): int;
/**
* @return Visit[]
@@ -49,9 +51,9 @@ interface VisitRepositoryInterface extends ObjectRepository, EntitySpecification
/**
* @return Visit[]
*/
public function findNonOrphanVisits(VisitsListFiltering $filtering): array;
public function findNonOrphanVisits(WithDomainVisitsListFiltering $filtering): array;
public function countNonOrphanVisits(VisitsCountFiltering $filtering): int;
public function countNonOrphanVisits(WithDomainVisitsCountFiltering $filtering): int;
public function findMostRecentOrphanVisit(): Visit|null;
}

View File

@@ -23,6 +23,7 @@ use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\VisitsStats;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\DomainVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\NonOrphanVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\OrphanVisitsPaginatorAdapter;
@@ -88,7 +89,7 @@ readonly class VisitsStatsHelper implements VisitsStatsHelperInterface
/**
* @inheritDoc
*/
public function visitsForTag(string $tag, VisitsParams $params, ApiKey|null $apiKey = null): Paginator
public function visitsForTag(string $tag, WithDomainVisitsParams $params, ApiKey|null $apiKey = null): Paginator
{
/** @var TagRepository $tagRepo */
$tagRepo = $this->em->getRepository(Tag::class);
@@ -130,7 +131,7 @@ readonly class VisitsStatsHelper implements VisitsStatsHelperInterface
return $this->createPaginator(new OrphanVisitsPaginatorAdapter($repo, $params, $apiKey), $params);
}
public function nonOrphanVisits(VisitsParams $params, ApiKey|null $apiKey = null): Paginator
public function nonOrphanVisits(WithDomainVisitsParams $params, ApiKey|null $apiKey = null): Paginator
{
/** @var VisitRepository $repo */
$repo = $this->em->getRepository(Visit::class);

View File

@@ -13,6 +13,7 @@ use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\VisitsStats;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
interface VisitsStatsHelperInterface
@@ -33,7 +34,7 @@ interface VisitsStatsHelperInterface
* @return Paginator<Visit>
* @throws TagNotFoundException
*/
public function visitsForTag(string $tag, VisitsParams $params, ApiKey|null $apiKey = null): Paginator;
public function visitsForTag(string $tag, WithDomainVisitsParams $params, ApiKey|null $apiKey = null): Paginator;
/**
* @return Paginator<Visit>
@@ -49,5 +50,5 @@ interface VisitsStatsHelperInterface
/**
* @return Paginator<Visit>
*/
public function nonOrphanVisits(VisitsParams $params, ApiKey|null $apiKey = null): Paginator;
public function nonOrphanVisits(WithDomainVisitsParams $params, ApiKey|null $apiKey = null): Paginator;
}

View File

@@ -22,6 +22,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\Persistence\WithDomainVisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Repository\OrphanVisitsCountRepository;
use Shlinkio\Shlink\Core\Visit\Repository\ShortUrlVisitsCountRepository;
use Shlinkio\Shlink\Core\Visit\Repository\VisitRepository;
@@ -187,13 +188,13 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->createShortUrlsAndVisits(false, [$foo]);
$this->getEntityManager()->flush();
self::assertCount(0, $this->repo->findVisitsByTag('invalid', new VisitsListFiltering()));
self::assertCount(18, $this->repo->findVisitsByTag($foo, new VisitsListFiltering()));
self::assertCount(12, $this->repo->findVisitsByTag($foo, new VisitsListFiltering(null, true)));
self::assertCount(6, $this->repo->findVisitsByTag($foo, new VisitsListFiltering(
self::assertCount(0, $this->repo->findVisitsByTag('invalid', new WithDomainVisitsListFiltering()));
self::assertCount(18, $this->repo->findVisitsByTag($foo, new WithDomainVisitsListFiltering()));
self::assertCount(12, $this->repo->findVisitsByTag($foo, new WithDomainVisitsListFiltering(null, true)));
self::assertCount(6, $this->repo->findVisitsByTag($foo, new WithDomainVisitsListFiltering(
DateRange::between(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
)));
self::assertCount(12, $this->repo->findVisitsByTag($foo, new VisitsListFiltering(
self::assertCount(12, $this->repo->findVisitsByTag($foo, new WithDomainVisitsListFiltering(
DateRange::since(Chronos::parse('2016-01-03')),
)));
}
@@ -479,31 +480,38 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->flush();
self::assertCount(21, $this->repo->findNonOrphanVisits(new VisitsListFiltering()));
self::assertCount(21, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::allTime())));
self::assertCount(4, $this->repo->findNonOrphanVisits(new VisitsListFiltering(apiKey: $authoredApiKey)));
self::assertCount(7, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::since(
self::assertCount(21, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering()));
self::assertCount(21, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(
DateRange::allTime(),
)));
self::assertCount(4, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(
apiKey: $authoredApiKey,
)));
self::assertCount(7, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(DateRange::since(
Chronos::parse('2016-01-05')->endOfDay(),
))));
self::assertCount(12, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::until(
self::assertCount(12, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(DateRange::until(
Chronos::parse('2016-01-04')->endOfDay(),
))));
self::assertCount(6, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::between(
self::assertCount(6, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(DateRange::between(
Chronos::parse('2016-01-03')->startOfDay(),
Chronos::parse('2016-01-04')->endOfDay(),
))));
self::assertCount(13, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::between(
self::assertCount(13, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(DateRange::between(
Chronos::parse('2016-01-03')->startOfDay(),
Chronos::parse('2016-01-08')->endOfDay(),
))));
self::assertCount(3, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::between(
self::assertCount(3, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(DateRange::between(
Chronos::parse('2016-01-03')->startOfDay(),
Chronos::parse('2016-01-08')->endOfDay(),
), limit: 10, offset: 10)));
self::assertCount(15, $this->repo->findNonOrphanVisits(new VisitsListFiltering(excludeBots: true)));
self::assertCount(10, $this->repo->findNonOrphanVisits(new VisitsListFiltering(limit: 10)));
self::assertCount(1, $this->repo->findNonOrphanVisits(new VisitsListFiltering(limit: 10, offset: 20)));
self::assertCount(5, $this->repo->findNonOrphanVisits(new VisitsListFiltering(limit: 5, offset: 5)));
self::assertCount(15, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(excludeBots: true)));
self::assertCount(10, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(limit: 10)));
self::assertCount(1, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(
limit: 10,
offset: 20,
)));
self::assertCount(5, $this->repo->findNonOrphanVisits(new WithDomainVisitsListFiltering(limit: 5, offset: 5)));
}
#[Test]

View File

@@ -10,10 +10,10 @@ use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\NonOrphanVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -21,13 +21,13 @@ class NonOrphanVisitsPaginatorAdapterTest extends TestCase
{
private NonOrphanVisitsPaginatorAdapter $adapter;
private MockObject & VisitRepositoryInterface $repo;
private VisitsParams $params;
private WithDomainVisitsParams $params;
private ApiKey $apiKey;
protected function setUp(): void
{
$this->repo = $this->createMock(VisitRepositoryInterface::class);
$this->params = VisitsParams::fromRawData([]);
$this->params = WithDomainVisitsParams::fromRawData([]);
$this->apiKey = ApiKey::create();
$this->adapter = new NonOrphanVisitsPaginatorAdapter($this->repo, $this->params, $this->apiKey);
@@ -38,7 +38,7 @@ class NonOrphanVisitsPaginatorAdapterTest extends TestCase
{
$expectedCount = 5;
$this->repo->expects($this->once())->method('countNonOrphanVisits')->with(
new VisitsCountFiltering($this->params->dateRange, $this->params->excludeBots, $this->apiKey),
new WithDomainVisitsCountFiltering($this->params->dateRange, $this->params->excludeBots, $this->apiKey),
)->willReturn($expectedCount);
$result = $this->adapter->getNbResults();
@@ -55,12 +55,12 @@ class NonOrphanVisitsPaginatorAdapterTest extends TestCase
{
$visitor = Visitor::empty();
$list = [Visit::forRegularNotFound($visitor), Visit::forInvalidShortUrl($visitor)];
$this->repo->expects($this->once())->method('findNonOrphanVisits')->with(new VisitsListFiltering(
$this->repo->expects($this->once())->method('findNonOrphanVisits')->with(new WithDomainVisitsListFiltering(
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$limit,
$offset,
limit: $limit,
offset: $offset,
))->willReturn($list);
$result = $this->adapter->getSlice($offset, $limit);

View File

@@ -8,10 +8,10 @@ use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\TagVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -33,7 +33,7 @@ class VisitsForTagPaginatorAdapterTest extends TestCase
$adapter = $this->createAdapter(null);
$this->repo->expects($this->exactly($count))->method('findVisitsByTag')->with(
'foo',
new VisitsListFiltering(DateRange::allTime(), false, null, $limit, $offset),
new WithDomainVisitsListFiltering(DateRange::allTime(), limit: $limit, offset: $offset),
)->willReturn([]);
for ($i = 0; $i < $count; $i++) {
@@ -49,7 +49,7 @@ class VisitsForTagPaginatorAdapterTest extends TestCase
$adapter = $this->createAdapter($apiKey);
$this->repo->expects($this->once())->method('countVisitsByTag')->with(
'foo',
new VisitsCountFiltering(DateRange::allTime(), false, $apiKey),
new WithDomainVisitsCountFiltering(DateRange::allTime(), apiKey: $apiKey),
)->willReturn(3);
for ($i = 0; $i < $count; $i++) {
@@ -59,6 +59,6 @@ class VisitsForTagPaginatorAdapterTest extends TestCase
private function createAdapter(ApiKey|null $apiKey): TagVisitsPaginatorAdapter
{
return new TagVisitsPaginatorAdapter($this->repo, 'foo', VisitsParams::fromRawData([]), $apiKey);
return new TagVisitsPaginatorAdapter($this->repo, 'foo', WithDomainVisitsParams::fromRawData([]), $apiKey);
}
}

View File

@@ -29,10 +29,12 @@ use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\VisitsStats;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
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\Persistence\WithDomainVisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Repository\OrphanVisitsCountRepository;
use Shlinkio\Shlink\Core\Visit\Repository\ShortUrlVisitsCountRepository;
use Shlinkio\Shlink\Core\Visit\Repository\VisitRepository;
@@ -147,7 +149,7 @@ class VisitsStatsHelperTest extends TestCase
$this->expectException(TagNotFoundException::class);
$this->helper->visitsForTag($tag, new VisitsParams(), $apiKey);
$this->helper->visitsForTag($tag, new WithDomainVisitsParams(), $apiKey);
}
#[Test, DataProviderExternal(ApiKeyDataProviders::class, 'adminApiKeysProvider')]
@@ -170,7 +172,7 @@ class VisitsStatsHelperTest extends TestCase
[Visit::class, $repo2],
]);
$paginator = $this->helper->visitsForTag($tag, new VisitsParams(), $apiKey);
$paginator = $this->helper->visitsForTag($tag, new WithDomainVisitsParams(), $apiKey);
self::assertEquals($list, ArrayUtils::iteratorToArray($paginator->getCurrentPageResults()));
}
@@ -265,14 +267,14 @@ class VisitsStatsHelperTest extends TestCase
);
$repo = $this->createMock(VisitRepository::class);
$repo->expects($this->once())->method('countNonOrphanVisits')->with(
$this->isInstanceOf(VisitsCountFiltering::class),
$this->isInstanceOf(WithDOmainVisitsCountFiltering::class),
)->willReturn(count($list));
$repo->expects($this->once())->method('findNonOrphanVisits')->with(
$this->isInstanceOf(VisitsListFiltering::class),
$this->isInstanceOf(WithDOmainVisitsListFiltering::class),
)->willReturn($list);
$this->em->expects($this->once())->method('getRepository')->with(Visit::class)->willReturn($repo);
$paginator = $this->helper->nonOrphanVisits(new VisitsParams());
$paginator = $this->helper->nonOrphanVisits(new WithDomainVisitsParams());
self::assertEquals($list, ArrayUtils::iteratorToArray($paginator->getCurrentPageResults()));
}

View File

@@ -10,7 +10,6 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtils;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -26,9 +25,8 @@ abstract class AbstractListVisitsAction extends AbstractRestAction
public function handle(ServerRequestInterface $request): ResponseInterface
{
$params = VisitsParams::fromRawData($request->getQueryParams());
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
$visits = $this->getVisitsPaginator($request, $params, $apiKey);
$visits = $this->getVisitsPaginator($request, $apiKey);
return new JsonResponse(['visits' => PagerfantaUtils::serializePaginator($visits)]);
}
@@ -36,9 +34,5 @@ abstract class AbstractListVisitsAction extends AbstractRestAction
/**
* @return Pagerfanta<Visit>
*/
abstract protected function getVisitsPaginator(
ServerRequestInterface $request,
VisitsParams $params,
ApiKey $apiKey,
): Pagerfanta;
abstract protected function getVisitsPaginator(ServerRequestInterface $request, ApiKey $apiKey): Pagerfanta;
}

View File

@@ -23,8 +23,9 @@ class DomainVisitsAction extends AbstractListVisitsAction
parent::__construct($visitsHelper);
}
protected function getVisitsPaginator(Request $request, VisitsParams $params, ApiKey $apiKey): Pagerfanta
protected function getVisitsPaginator(Request $request, ApiKey $apiKey): Pagerfanta
{
$params = VisitsParams::fromRawData($request->getQueryParams());
$domain = $this->resolveDomainParam($request);
return $this->visitsHelper->visitsForDomain($domain, $params, $apiKey);
}

View File

@@ -6,18 +6,16 @@ namespace Shlinkio\Shlink\Rest\Action\Visit;
use Pagerfanta\Pagerfanta;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class NonOrphanVisitsAction extends AbstractListVisitsAction
{
protected const string ROUTE_PATH = '/visits/non-orphan';
protected function getVisitsPaginator(
ServerRequestInterface $request,
VisitsParams $params,
ApiKey $apiKey,
): Pagerfanta {
protected function getVisitsPaginator(ServerRequestInterface $request, ApiKey $apiKey): Pagerfanta
{
$params = WithDomainVisitsParams::fromRawData($request->getQueryParams());
return $this->visitsHelper->nonOrphanVisits($params, $apiKey);
}
}

View File

@@ -7,19 +7,15 @@ namespace Shlinkio\Shlink\Rest\Action\Visit;
use Pagerfanta\Pagerfanta;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class OrphanVisitsAction extends AbstractListVisitsAction
{
protected const string ROUTE_PATH = '/visits/orphan';
protected function getVisitsPaginator(
ServerRequestInterface $request,
VisitsParams $params,
ApiKey $apiKey,
): Pagerfanta {
$orphanParams = OrphanVisitsParams::fromVisitsParamsAndRawData($params, $request->getQueryParams());
protected function getVisitsPaginator(ServerRequestInterface $request, ApiKey $apiKey): Pagerfanta
{
$orphanParams = OrphanVisitsParams::fromRawData($request->getQueryParams());
return $this->visitsHelper->orphanVisits($orphanParams, $apiKey);
}
}

View File

@@ -14,8 +14,9 @@ class ShortUrlVisitsAction extends AbstractListVisitsAction
{
protected const string ROUTE_PATH = '/short-urls/{shortCode}/visits';
protected function getVisitsPaginator(Request $request, VisitsParams $params, ApiKey $apiKey): Pagerfanta
protected function getVisitsPaginator(Request $request, ApiKey $apiKey): Pagerfanta
{
$params = VisitsParams::fromRawData($request->getQueryParams());
$identifier = ShortUrlIdentifier::fromApiRequest($request);
return $this->visitsHelper->visitsForShortUrl($identifier, $params, $apiKey);
}

View File

@@ -6,15 +6,16 @@ namespace Shlinkio\Shlink\Rest\Action\Visit;
use Pagerfanta\Pagerfanta;
use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class TagVisitsAction extends AbstractListVisitsAction
{
protected const string ROUTE_PATH = '/tags/{tag}/visits';
protected function getVisitsPaginator(Request $request, VisitsParams $params, ApiKey $apiKey): Pagerfanta
protected function getVisitsPaginator(Request $request, ApiKey $apiKey): Pagerfanta
{
$params = WithDomainVisitsParams::fromRawData($request->getQueryParams());
$tag = $request->getAttribute('tag', '');
return $this->visitsHelper->visitsForTag($tag, $params, $apiKey);
}