From 37088b1a4b016e00d35c6473313c7d6f58f18791 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 31 Oct 2025 08:53:31 +0100 Subject: [PATCH] Allow filtering orphan visits by domain via DEFAULT keyword --- module/Core/config/dependencies.config.php | 2 +- .../Adapter/OrphanVisitsPaginatorAdapter.php | 4 ++++ .../Visit/Persistence/OrphanVisitsCountFiltering.php | 1 + .../Visit/Persistence/OrphanVisitsListFiltering.php | 3 ++- module/Core/src/Visit/Repository/VisitRepository.php | 5 ++--- module/Core/src/Visit/VisitsStatsHelper.php | 8 ++++++-- .../test-db/Visit/Repository/VisitRepositoryTest.php | 12 +++++++++--- .../Adapter/OrphanVisitsPaginatorAdapterTest.php | 8 +++++++- module/Core/test/Visit/VisitsStatsHelperTest.php | 3 ++- 9 files changed, 34 insertions(+), 12 deletions(-) diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 0ad943e9..5bb534c2 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -168,7 +168,7 @@ return [ ], Visit\Geolocation\VisitLocator::class => ['em', Visit\Repository\VisitIterationRepository::class], Visit\Geolocation\VisitToLocationHelper::class => [IpLocationResolverInterface::class], - Visit\VisitsStatsHelper::class => ['em'], + Visit\VisitsStatsHelper::class => ['em', Config\Options\UrlShortenerOptions::class], Tag\TagService::class => ['em', Tag\Repository\TagRepository::class], ShortUrl\DeleteShortUrlService::class => [ 'em', diff --git a/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php b/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php index fbb14c42..ad175a79 100644 --- a/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php +++ b/module/Core/src/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Visit\Paginator\Adapter; +use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\Paginator\Adapter\AbstractCacheableCountPaginatorAdapter; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams; @@ -19,6 +20,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte private readonly VisitRepositoryInterface $repo, private readonly OrphanVisitsParams $params, private readonly ApiKey|null $apiKey, + private readonly UrlShortenerOptions $options, ) { } @@ -30,6 +32,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte apiKey: $this->apiKey, domain: $this->params->domain, type: $this->params->type, + defaultDomain: $this->options->defaultDomain, )); } @@ -41,6 +44,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte apiKey: $this->apiKey, domain: $this->params->domain, type: $this->params->type, + defaultDomain: $this->options->defaultDomain, limit: $length, offset: $offset, )); diff --git a/module/Core/src/Visit/Persistence/OrphanVisitsCountFiltering.php b/module/Core/src/Visit/Persistence/OrphanVisitsCountFiltering.php index 981c26d4..84f7af57 100644 --- a/module/Core/src/Visit/Persistence/OrphanVisitsCountFiltering.php +++ b/module/Core/src/Visit/Persistence/OrphanVisitsCountFiltering.php @@ -16,6 +16,7 @@ class OrphanVisitsCountFiltering extends WithDomainVisitsCountFiltering ApiKey|null $apiKey = null, string|null $domain = null, public readonly OrphanVisitType|null $type = null, + public readonly string $defaultDomain = '', ) { parent::__construct($dateRange, $excludeBots, $apiKey, $domain); } diff --git a/module/Core/src/Visit/Persistence/OrphanVisitsListFiltering.php b/module/Core/src/Visit/Persistence/OrphanVisitsListFiltering.php index a60a0690..23c3d6cb 100644 --- a/module/Core/src/Visit/Persistence/OrphanVisitsListFiltering.php +++ b/module/Core/src/Visit/Persistence/OrphanVisitsListFiltering.php @@ -16,9 +16,10 @@ final class OrphanVisitsListFiltering extends OrphanVisitsCountFiltering ApiKey|null $apiKey = null, string|null $domain = null, OrphanVisitType|null $type = null, + string $defaultDomain = '', public readonly int|null $limit = null, public readonly int|null $offset = null, ) { - parent::__construct($dateRange, $excludeBots, $apiKey, $domain, $type); + parent::__construct($dateRange, $excludeBots, $apiKey, $domain, $type, $defaultDomain); } } diff --git a/module/Core/src/Visit/Repository/VisitRepository.php b/module/Core/src/Visit/Repository/VisitRepository.php index 8b87954e..78ff74b3 100644 --- a/module/Core/src/Visit/Repository/VisitRepository.php +++ b/module/Core/src/Visit/Repository/VisitRepository.php @@ -188,9 +188,8 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo } $domain = $filtering->domain; - if ($domain === Domain::DEFAULT_AUTHORITY) { - // TODO - } elseif ($domain !== null) { + $domain = $domain === Domain::DEFAULT_AUTHORITY ? $filtering->defaultDomain : $domain; + if ($domain !== null) { $qb->andWhere($qb->expr()->like('v.visitedUrl', $conn->quote('%' . $domain . '%'))); } diff --git a/module/Core/src/Visit/VisitsStatsHelper.php b/module/Core/src/Visit/VisitsStatsHelper.php index 65e710c7..b9721f3c 100644 --- a/module/Core/src/Visit/VisitsStatsHelper.php +++ b/module/Core/src/Visit/VisitsStatsHelper.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\Visit; use Doctrine\ORM\EntityManagerInterface; use Pagerfanta\Adapter\AdapterInterface; use Shlinkio\Shlink\Common\Paginator\Paginator; +use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\Domain\Entity\Domain; use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository; use Shlinkio\Shlink\Core\Exception\DomainNotFoundException; @@ -38,7 +39,7 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey; readonly class VisitsStatsHelper implements VisitsStatsHelperInterface { - public function __construct(private EntityManagerInterface $em) + public function __construct(private EntityManagerInterface $em, private UrlShortenerOptions $options) { } @@ -128,7 +129,10 @@ readonly class VisitsStatsHelper implements VisitsStatsHelperInterface /** @var VisitRepository $repo */ $repo = $this->em->getRepository(Visit::class); - return $this->createPaginator(new OrphanVisitsPaginatorAdapter($repo, $params, $apiKey), $params); + return $this->createPaginator( + new OrphanVisitsPaginatorAdapter($repo, $params, $apiKey, $this->options), + $params, + ); } public function nonOrphanVisits(WithDomainVisitsParams $params, ApiKey|null $apiKey = null): Paginator diff --git a/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php b/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php index 1b360005..56b6175f 100644 --- a/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php +++ b/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php @@ -405,7 +405,7 @@ class VisitRepositoryTest extends DatabaseTestCase Chronos::parse(sprintf('2020-01-0%s', $i + 1)), )); $this->getEntityManager()->persist($this->setDateOnVisit( - fn () => Visit::forInvalidShortUrl(Visitor::empty()), + fn () => Visit::forInvalidShortUrl(Visitor::fromParams(visitedUrl: 'https://s.test/bar')), Chronos::parse(sprintf('2020-01-0%s', $i + 1)), )); $this->getEntityManager()->persist($this->setDateOnVisit( @@ -449,6 +449,10 @@ class VisitRepositoryTest extends DatabaseTestCase limit: 4, ))); self::assertCount(6, $this->repo->findOrphanVisits(new OrphanVisitsListFiltering(domain: 'example.com'))); + self::assertCount(6, $this->repo->findOrphanVisits(new OrphanVisitsListFiltering( + domain: Domain::DEFAULT_AUTHORITY, + defaultDomain: 's.test', + ))); } #[Test] @@ -464,7 +468,7 @@ class VisitRepositoryTest extends DatabaseTestCase Chronos::parse(sprintf('2020-01-0%s', $i + 1)), )); $this->getEntityManager()->persist($this->setDateOnVisit( - fn () => Visit::forInvalidShortUrl(Visitor::empty()), + fn () => Visit::forInvalidShortUrl(Visitor::fromParams(visitedUrl: 'https://s.test/foo/bar')), Chronos::parse(sprintf('2020-01-0%s', $i + 1)), )); $this->getEntityManager()->persist($this->setDateOnVisit( @@ -497,8 +501,10 @@ class VisitRepositoryTest extends DatabaseTestCase self::assertEquals(6, $this->repo->countOrphanVisits(new OrphanVisitsCountFiltering( type: OrphanVisitType::REGULAR_404, ))); + self::assertEquals(6, $this->repo->countOrphanVisits(new OrphanVisitsCountFiltering(domain: 'example.com'))); self::assertEquals(6, $this->repo->countOrphanVisits(new OrphanVisitsCountFiltering( - domain: 'example.com', + domain: Domain::DEFAULT_AUTHORITY, + defaultDomain: 's.test', ))); } diff --git a/module/Core/test/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapterTest.php b/module/Core/test/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapterTest.php index b62fa0c6..b3e800a7 100644 --- a/module/Core/test/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapterTest.php +++ b/module/Core/test/Visit/Paginator/Adapter/OrphanVisitsPaginatorAdapterTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams; use Shlinkio\Shlink\Core\Visit\Model\Visitor; @@ -30,7 +31,12 @@ class OrphanVisitsPaginatorAdapterTest extends TestCase $this->params = new OrphanVisitsParams(); $this->apiKey = ApiKey::create(); - $this->adapter = new OrphanVisitsPaginatorAdapter($this->repo, $this->params, $this->apiKey); + $this->adapter = new OrphanVisitsPaginatorAdapter( + $this->repo, + $this->params, + $this->apiKey, + new UrlShortenerOptions(), + ); } #[Test] diff --git a/module/Core/test/Visit/VisitsStatsHelperTest.php b/module/Core/test/Visit/VisitsStatsHelperTest.php index 8d75f523..68aa4310 100644 --- a/module/Core/test/Visit/VisitsStatsHelperTest.php +++ b/module/Core/test/Visit/VisitsStatsHelperTest.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\Domain\Entity\Domain; use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository; use Shlinkio\Shlink\Core\Exception\DomainNotFoundException; @@ -54,7 +55,7 @@ class VisitsStatsHelperTest extends TestCase protected function setUp(): void { $this->em = $this->createMock(EntityManagerInterface::class); - $this->helper = new VisitsStatsHelper($this->em); + $this->helper = new VisitsStatsHelper($this->em, new UrlShortenerOptions()); } #[Test, DataProvider('provideCounts')]