Allow filtering orphan visits by domain via DEFAULT keyword

This commit is contained in:
Alejandro Celaya 2025-10-31 08:53:31 +01:00
parent b5f8e8a4cd
commit 37088b1a4b
9 changed files with 34 additions and 12 deletions

View File

@ -168,7 +168,7 @@ return [
], ],
Visit\Geolocation\VisitLocator::class => ['em', Visit\Repository\VisitIterationRepository::class], Visit\Geolocation\VisitLocator::class => ['em', Visit\Repository\VisitIterationRepository::class],
Visit\Geolocation\VisitToLocationHelper::class => [IpLocationResolverInterface::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], Tag\TagService::class => ['em', Tag\Repository\TagRepository::class],
ShortUrl\DeleteShortUrlService::class => [ ShortUrl\DeleteShortUrlService::class => [
'em', 'em',

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Paginator\Adapter; 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\Paginator\Adapter\AbstractCacheableCountPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams; use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams;
@ -19,6 +20,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
private readonly VisitRepositoryInterface $repo, private readonly VisitRepositoryInterface $repo,
private readonly OrphanVisitsParams $params, private readonly OrphanVisitsParams $params,
private readonly ApiKey|null $apiKey, private readonly ApiKey|null $apiKey,
private readonly UrlShortenerOptions $options,
) { ) {
} }
@ -30,6 +32,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
apiKey: $this->apiKey, apiKey: $this->apiKey,
domain: $this->params->domain, domain: $this->params->domain,
type: $this->params->type, type: $this->params->type,
defaultDomain: $this->options->defaultDomain,
)); ));
} }
@ -41,6 +44,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
apiKey: $this->apiKey, apiKey: $this->apiKey,
domain: $this->params->domain, domain: $this->params->domain,
type: $this->params->type, type: $this->params->type,
defaultDomain: $this->options->defaultDomain,
limit: $length, limit: $length,
offset: $offset, offset: $offset,
)); ));

View File

@ -16,6 +16,7 @@ class OrphanVisitsCountFiltering extends WithDomainVisitsCountFiltering
ApiKey|null $apiKey = null, ApiKey|null $apiKey = null,
string|null $domain = null, string|null $domain = null,
public readonly OrphanVisitType|null $type = null, public readonly OrphanVisitType|null $type = null,
public readonly string $defaultDomain = '',
) { ) {
parent::__construct($dateRange, $excludeBots, $apiKey, $domain); parent::__construct($dateRange, $excludeBots, $apiKey, $domain);
} }

View File

@ -16,9 +16,10 @@ final class OrphanVisitsListFiltering extends OrphanVisitsCountFiltering
ApiKey|null $apiKey = null, ApiKey|null $apiKey = null,
string|null $domain = null, string|null $domain = null,
OrphanVisitType|null $type = null, OrphanVisitType|null $type = null,
string $defaultDomain = '',
public readonly int|null $limit = null, public readonly int|null $limit = null,
public readonly int|null $offset = null, public readonly int|null $offset = null,
) { ) {
parent::__construct($dateRange, $excludeBots, $apiKey, $domain, $type); parent::__construct($dateRange, $excludeBots, $apiKey, $domain, $type, $defaultDomain);
} }
} }

View File

@ -188,9 +188,8 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
} }
$domain = $filtering->domain; $domain = $filtering->domain;
if ($domain === Domain::DEFAULT_AUTHORITY) { $domain = $domain === Domain::DEFAULT_AUTHORITY ? $filtering->defaultDomain : $domain;
// TODO if ($domain !== null) {
} elseif ($domain !== null) {
$qb->andWhere($qb->expr()->like('v.visitedUrl', $conn->quote('%' . $domain . '%'))); $qb->andWhere($qb->expr()->like('v.visitedUrl', $conn->quote('%' . $domain . '%')));
} }

View File

@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\Visit;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Pagerfanta\Adapter\AdapterInterface; use Pagerfanta\Adapter\AdapterInterface;
use Shlinkio\Shlink\Common\Paginator\Paginator; 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\Entity\Domain;
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository; use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
use Shlinkio\Shlink\Core\Exception\DomainNotFoundException; use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
@ -38,7 +39,7 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
readonly class VisitsStatsHelper implements VisitsStatsHelperInterface 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 */ /** @var VisitRepository $repo */
$repo = $this->em->getRepository(Visit::class); $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 public function nonOrphanVisits(WithDomainVisitsParams $params, ApiKey|null $apiKey = null): Paginator

View File

@ -405,7 +405,7 @@ class VisitRepositoryTest extends DatabaseTestCase
Chronos::parse(sprintf('2020-01-0%s', $i + 1)), Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
)); ));
$this->getEntityManager()->persist($this->setDateOnVisit( $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)), Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
)); ));
$this->getEntityManager()->persist($this->setDateOnVisit( $this->getEntityManager()->persist($this->setDateOnVisit(
@ -449,6 +449,10 @@ class VisitRepositoryTest extends DatabaseTestCase
limit: 4, limit: 4,
))); )));
self::assertCount(6, $this->repo->findOrphanVisits(new OrphanVisitsListFiltering(domain: 'example.com'))); 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] #[Test]
@ -464,7 +468,7 @@ class VisitRepositoryTest extends DatabaseTestCase
Chronos::parse(sprintf('2020-01-0%s', $i + 1)), Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
)); ));
$this->getEntityManager()->persist($this->setDateOnVisit( $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)), Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
)); ));
$this->getEntityManager()->persist($this->setDateOnVisit( $this->getEntityManager()->persist($this->setDateOnVisit(
@ -497,8 +501,10 @@ class VisitRepositoryTest extends DatabaseTestCase
self::assertEquals(6, $this->repo->countOrphanVisits(new OrphanVisitsCountFiltering( self::assertEquals(6, $this->repo->countOrphanVisits(new OrphanVisitsCountFiltering(
type: OrphanVisitType::REGULAR_404, type: OrphanVisitType::REGULAR_404,
))); )));
self::assertEquals(6, $this->repo->countOrphanVisits(new OrphanVisitsCountFiltering(domain: 'example.com')));
self::assertEquals(6, $this->repo->countOrphanVisits(new OrphanVisitsCountFiltering( self::assertEquals(6, $this->repo->countOrphanVisits(new OrphanVisitsCountFiltering(
domain: 'example.com', domain: Domain::DEFAULT_AUTHORITY,
defaultDomain: 's.test',
))); )));
} }

View File

@ -8,6 +8,7 @@ use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams; use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams;
use Shlinkio\Shlink\Core\Visit\Model\Visitor; use Shlinkio\Shlink\Core\Visit\Model\Visitor;
@ -30,7 +31,12 @@ class OrphanVisitsPaginatorAdapterTest extends TestCase
$this->params = new OrphanVisitsParams(); $this->params = new OrphanVisitsParams();
$this->apiKey = ApiKey::create(); $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] #[Test]

View File

@ -12,6 +12,7 @@ use PHPUnit\Framework\Attributes\DataProviderExternal;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Domain\Entity\Domain; use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository; use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
use Shlinkio\Shlink\Core\Exception\DomainNotFoundException; use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
@ -54,7 +55,7 @@ class VisitsStatsHelperTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$this->em = $this->createMock(EntityManagerInterface::class); $this->em = $this->createMock(EntityManagerInterface::class);
$this->helper = new VisitsStatsHelper($this->em); $this->helper = new VisitsStatsHelper($this->em, new UrlShortenerOptions());
} }
#[Test, DataProvider('provideCounts')] #[Test, DataProvider('provideCounts')]