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\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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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