Allow filtering by domain in VisitRepository::findOrphanVisits

This commit is contained in:
Alejandro Celaya 2025-10-30 09:04:51 +01:00
parent 9dcc51abde
commit 94426c7bf4
4 changed files with 36 additions and 53 deletions

View File

@ -20,9 +20,9 @@
"ext-pdo": "*",
"akrabat/ip-address-middleware": "^2.6",
"cakephp/chronos": "^3.1",
"doctrine/dbal": "^4.2",
"doctrine/migrations": "^3.8",
"doctrine/orm": "^3.3",
"doctrine/dbal": "^4.3",
"doctrine/migrations": "^3.9",
"doctrine/orm": "^3.5",
"donatj/phpuseragentparser": "^1.10",
"endroid/qr-code": "^6.0.5",
"friendsofphp/proxy-manager-lts": "^1.0",

View File

@ -21,7 +21,6 @@ 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\Spec\CountOfNonOrphanVisits;
use Shlinkio\Shlink\Core\Visit\Spec\CountOfOrphanVisits;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@ -161,15 +160,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
return [];
}
$qb = $this->createAllVisitsQueryBuilder($filtering);
$qb->andWhere($qb->expr()->isNull('v.shortUrl'));
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later
if ($filtering->type) {
$conn = $this->getEntityManager()->getConnection();
$qb->andWhere($qb->expr()->eq('v.type', $conn->quote($filtering->type->value)));
}
$qb = $this->createOrphanVisitsQueryBuilder($filtering);
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
@ -179,7 +170,32 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
return 0;
}
return (int) $this->matchSingleScalarResult(new CountOfOrphanVisits($filtering));
$qb = $this->createOrphanVisitsQueryBuilder($filtering);
$qb->select('COUNT(v.id)');
return (int) $qb->getQuery()->getSingleScalarResult();
}
private function createOrphanVisitsQueryBuilder(OrphanVisitsCountFiltering $filtering): QueryBuilder
{
$qb = $this->createAllVisitsQueryBuilder($filtering);
$qb->andWhere($qb->expr()->isNull('v.shortUrl'));
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later
$conn = $this->getEntityManager()->getConnection();
if ($filtering->type) {
$qb->andWhere($qb->expr()->eq('v.type', $conn->quote($filtering->type->value)));
}
$domain = $filtering->domain;
if ($domain === Domain::DEFAULT_AUTHORITY) {
// TODO
} elseif ($domain !== null) {
$qb->andWhere($qb->expr()->like('v.visitedUrl', $conn->quote('%' . $domain . '%')));
}
return $qb;
}
/**

View File

@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Spec;
use Happyr\DoctrineSpecification\Spec;
use Happyr\DoctrineSpecification\Specification\BaseSpecification;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Core\Spec\InDateRange;
use Shlinkio\Shlink\Core\Visit\Persistence\OrphanVisitsCountFiltering;
class CountOfOrphanVisits extends BaseSpecification
{
public function __construct(private readonly OrphanVisitsCountFiltering $filtering)
{
parent::__construct();
}
protected function getSpec(): Specification
{
$conditions = [
Spec::isNull('shortUrl'),
new InDateRange($this->filtering->dateRange),
];
if ($this->filtering->excludeBots) {
$conditions[] = Spec::eq('potentialBot', false);
}
if ($this->filtering->type) {
$conditions[] = Spec::eq('type', $this->filtering->type->value);
}
return Spec::countOf(Spec::andX(...$conditions));
}
}

View File

@ -399,7 +399,7 @@ class VisitRepositoryTest extends DatabaseTestCase
Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
));
$this->getEntityManager()->persist($this->setDateOnVisit(
fn () => Visit::forRegularNotFound(Visitor::empty()),
fn () => Visit::forRegularNotFound(Visitor::fromParams(visitedUrl: 'https://example.com/foo?1=2')),
Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
));
@ -438,6 +438,7 @@ class VisitRepositoryTest extends DatabaseTestCase
type: OrphanVisitType::BASE_URL,
limit: 4,
)));
self::assertCount(6, $this->repo->findOrphanVisits(new OrphanVisitsListFiltering(domain: 'example.com')));
}
#[Test]
@ -457,7 +458,7 @@ class VisitRepositoryTest extends DatabaseTestCase
Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
));
$this->getEntityManager()->persist($this->setDateOnVisit(
fn () => Visit::forRegularNotFound(Visitor::empty()),
fn () => Visit::forRegularNotFound(Visitor::fromParams(visitedUrl: 'https://example.com/foo/bar')),
Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
));
}
@ -486,6 +487,9 @@ 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',
)));
}
#[Test]