mirror of
https://github.com/shlinkio/shlink.git
synced 2025-12-10 09:33:48 -06:00
Merge pull request #2503 from acelaya-forks/domain-visits-filter
Allow tags, orphan and non-orphan visits lists to be filtered by domain
This commit is contained in:
commit
628fb9ebb5
@ -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",
|
||||
|
||||
@ -64,6 +64,10 @@
|
||||
"type": "string",
|
||||
"enum": ["true"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json",
|
||||
"description": "Return visits for short URLs that belong to this domain. Use **DEFAULT** keyword to return visits from default domain."
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
|
||||
@ -55,6 +55,10 @@
|
||||
"type": "string",
|
||||
"enum": ["true"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json",
|
||||
"description": "Return visits for short URLs that belong to this domain. Use **DEFAULT** keyword to return visits from default domain."
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
|
||||
@ -65,6 +65,10 @@
|
||||
"type": "string",
|
||||
"enum": ["invalid_short_url", "base_url", "regular_404"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json",
|
||||
"description": "Return only visits for this domain. Use **DEFAULT** keyword to return visits from default domain."
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
|
||||
@ -5,24 +5,34 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
||||
use Shlinkio\Shlink\CLI\Input\DomainOption;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
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;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class GetTagVisitsCommand extends AbstractVisitsListCommand
|
||||
{
|
||||
public const string NAME = 'tag:visits';
|
||||
|
||||
private readonly DomainOption $domainOption;
|
||||
|
||||
public function __construct(
|
||||
VisitsStatsHelperInterface $visitsHelper,
|
||||
private readonly ShortUrlStringifierInterface $shortUrlStringifier,
|
||||
) {
|
||||
parent::__construct($visitsHelper);
|
||||
$this->domainOption = new DomainOption($this, sprintf(
|
||||
'Return visits that belong to this domain only. Use %s keyword for visits in default domain',
|
||||
Domain::DEFAULT_AUTHORITY,
|
||||
));
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@ -39,7 +49,10 @@ 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: $dateRange,
|
||||
domain: $this->domainOption->get($input),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4,23 +4,33 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Input\DomainOption;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
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;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand
|
||||
{
|
||||
public const string NAME = 'visit:non-orphan';
|
||||
|
||||
private readonly DomainOption $domainOption;
|
||||
|
||||
public function __construct(
|
||||
VisitsStatsHelperInterface $visitsHelper,
|
||||
private readonly ShortUrlStringifierInterface $shortUrlStringifier,
|
||||
) {
|
||||
parent::__construct($visitsHelper);
|
||||
$this->domainOption = new DomainOption($this, sprintf(
|
||||
'Return visits that belong to this domain only. Use %s keyword for visits in default domain',
|
||||
Domain::DEFAULT_AUTHORITY,
|
||||
));
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@ -35,7 +45,10 @@ 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: $dateRange,
|
||||
domain: $this->domainOption->get($input),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4,11 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Input\DomainOption;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitType;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
@ -19,6 +22,17 @@ class GetOrphanVisitsCommand extends AbstractVisitsListCommand
|
||||
{
|
||||
public const string NAME = 'visit:orphan';
|
||||
|
||||
private readonly DomainOption $domainOption;
|
||||
|
||||
public function __construct(VisitsStatsHelperInterface $visitsHelper)
|
||||
{
|
||||
parent::__construct($visitsHelper);
|
||||
$this->domainOption = new DomainOption($this, sprintf(
|
||||
'Return visits that belong to this domain only. Use %s keyword for visits in default domain',
|
||||
Domain::DEFAULT_AUTHORITY,
|
||||
));
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
@ -37,7 +51,11 @@ class GetOrphanVisitsCommand extends AbstractVisitsListCommand
|
||||
{
|
||||
$rawType = $input->getOption('type');
|
||||
$type = $rawType !== null ? OrphanVisitType::from($rawType) : null;
|
||||
return $this->visitsHelper->orphanVisits(new OrphanVisitsParams(dateRange: $dateRange, type: $type));
|
||||
return $this->visitsHelper->orphanVisits(new OrphanVisitsParams(
|
||||
dateRange: $dateRange,
|
||||
domain: $this->domainOption->get($input),
|
||||
type: $type,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
29
module/CLI/src/Input/DomainOption.php
Normal file
29
module/CLI/src/Input/DomainOption.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
final readonly class DomainOption
|
||||
{
|
||||
private const string NAME = 'domain';
|
||||
|
||||
public function __construct(Command $command, string $description)
|
||||
{
|
||||
$command->addOption(
|
||||
name: self::NAME,
|
||||
shortcut: 'd',
|
||||
mode: InputOption::VALUE_REQUIRED,
|
||||
description: $description,
|
||||
);
|
||||
}
|
||||
|
||||
public function get(InputInterface $input): string|null
|
||||
{
|
||||
return $input->getOption(self::NAME);
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -159,13 +159,11 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
|
||||
$qb->andWhere($qb->expr()->notIn('s.id', $subQb->getDQL()));
|
||||
}
|
||||
|
||||
if ($filtering->domain !== null) {
|
||||
if ($filtering->domain === Domain::DEFAULT_AUTHORITY) {
|
||||
$qb->andWhere($qb->expr()->isNull('s.domain'));
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->eq('d.authority', ':domain'))
|
||||
->setParameter('domain', $filtering->domain);
|
||||
}
|
||||
if ($filtering->domain === Domain::DEFAULT_AUTHORITY) {
|
||||
$qb->andWhere($qb->expr()->isNull('s.domain'));
|
||||
} elseif ($filtering->domain !== null) {
|
||||
$qb->andWhere($qb->expr()->eq('d.authority', ':domain'))
|
||||
->setParameter('domain', $filtering->domain);
|
||||
}
|
||||
|
||||
if ($filtering->excludeMaxVisitsReached) {
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
31
module/Core/src/Visit/Model/WithDomainVisitsParams.php
Normal file
31
module/Core/src/Visit/Model/WithDomainVisitsParams.php
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
));
|
||||
|
||||
@ -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,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -28,7 +30,9 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
|
||||
dateRange: $this->params->dateRange,
|
||||
excludeBots: $this->params->excludeBots,
|
||||
apiKey: $this->apiKey,
|
||||
domain: $this->params->domain,
|
||||
type: $this->params->type,
|
||||
defaultDomain: $this->options->defaultDomain,
|
||||
));
|
||||
}
|
||||
|
||||
@ -38,7 +42,9 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
|
||||
dateRange: $this->params->dateRange,
|
||||
excludeBots: $this->params->excludeBots,
|
||||
apiKey: $this->apiKey,
|
||||
domain: $this->params->domain,
|
||||
type: $this->params->type,
|
||||
defaultDomain: $this->options->defaultDomain,
|
||||
limit: $length,
|
||||
offset: $offset,
|
||||
));
|
||||
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,14 +8,16 @@ 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,
|
||||
public readonly string $defaultDomain = '',
|
||||
) {
|
||||
parent::__construct($dateRange, $excludeBots, $apiKey);
|
||||
parent::__construct($dateRange, $excludeBots, $apiKey, $domain);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,10 +14,12 @@ final class OrphanVisitsListFiltering extends OrphanVisitsCountFiltering
|
||||
DateRange|null $dateRange = null,
|
||||
bool $excludeBots = false,
|
||||
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, $type);
|
||||
parent::__construct($dateRange, $excludeBots, $apiKey, $domain, $type, $defaultDomain);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -18,8 +18,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\Spec\CountOfNonOrphanVisits;
|
||||
use Shlinkio\Shlink\Core\Visit\Spec\CountOfOrphanVisits;
|
||||
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsCountFiltering;
|
||||
use Shlinkio\Shlink\Core\Visit\Persistence\WithDomainVisitsListFiltering;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
@ -69,13 +69,13 @@ 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);
|
||||
}
|
||||
|
||||
public function countVisitsByTag(string $tag, VisitsCountFiltering $filtering): int
|
||||
public function countVisitsByTag(string $tag, WithDomainVisitsCountFiltering $filtering): int
|
||||
{
|
||||
$qb = $this->createVisitsByTagQueryBuilder($tag, $filtering);
|
||||
$qb->select('COUNT(v.id)');
|
||||
@ -83,19 +83,29 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function createVisitsByTagQueryBuilder(string $tag, VisitsCountFiltering $filtering): QueryBuilder
|
||||
private function createVisitsByTagQueryBuilder(string $tag, WithDomainVisitsCountFiltering $filtering): QueryBuilder
|
||||
{
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
|
||||
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later.
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->from(Visit::class, 'v')
|
||||
->join('v.shortUrl', 's')
|
||||
->join('s.tags', 't')
|
||||
->where($qb->expr()->eq('t.name', $this->getEntityManager()->getConnection()->quote($tag)));
|
||||
->where($qb->expr()->eq('t.name', $conn->quote($tag)));
|
||||
|
||||
if ($filtering->excludeBots) {
|
||||
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
|
||||
}
|
||||
|
||||
$domain = $filtering->domain;
|
||||
if ($domain === Domain::DEFAULT_AUTHORITY) {
|
||||
$qb->andWhere($qb->expr()->isNull('s.domain'));
|
||||
} elseif ($domain !== null) {
|
||||
$qb->join('s.domain', 'd')
|
||||
->andWhere($qb->expr()->eq('d.authority', $conn->quote($domain)));
|
||||
}
|
||||
|
||||
$this->applyDatesInline($qb, $filtering->dateRange);
|
||||
$this->applySpecification($qb, $filtering->apiKey?->inlinedSpec(), 'v');
|
||||
|
||||
@ -149,15 +159,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);
|
||||
}
|
||||
|
||||
@ -167,36 +169,78 @@ 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;
|
||||
$domain = $domain === Domain::DEFAULT_AUTHORITY ? $filtering->defaultDomain : $domain;
|
||||
if ($domain !== null) {
|
||||
$qb->andWhere($qb->expr()->like('v.visitedUrl', $conn->quote('%' . $domain . '%')));
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Visit[]
|
||||
*/
|
||||
public function findNonOrphanVisits(VisitsListFiltering $filtering): array
|
||||
public function findNonOrphanVisits(WithDomainVisitsListFiltering $filtering): array
|
||||
{
|
||||
$qb = $this->createNonOrphanVisitsQueryBuilder($filtering);
|
||||
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
|
||||
}
|
||||
|
||||
public function countNonOrphanVisits(WithDomainVisitsCountFiltering $filtering): int
|
||||
{
|
||||
$qb = $this->createNonOrphanVisitsQueryBuilder($filtering);
|
||||
$qb->select('COUNT(v.id)');
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function createNonOrphanVisitsQueryBuilder(WithDomainVisitsCountFiltering $filtering): QueryBuilder
|
||||
{
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
$qb = $this->createAllVisitsQueryBuilder($filtering);
|
||||
$qb->andWhere($qb->expr()->isNotNull('v.shortUrl'));
|
||||
|
||||
$apiKey = $filtering->apiKey;
|
||||
if (ApiKey::isShortUrlRestricted($apiKey)) {
|
||||
$domain = $filtering->domain;
|
||||
if (ApiKey::isShortUrlRestricted($apiKey) || $domain !== null) {
|
||||
$qb->join('v.shortUrl', 's');
|
||||
}
|
||||
|
||||
if ($domain === Domain::DEFAULT_AUTHORITY) {
|
||||
$qb->andWhere($qb->expr()->isNull('s.domain'));
|
||||
} elseif ($domain !== null) {
|
||||
$qb->join('s.domain', 'd')
|
||||
->andWhere($qb->expr()->eq('d.authority', $conn->quote($domain)));
|
||||
}
|
||||
|
||||
$this->applySpecification($qb, $apiKey?->inlinedSpec(), 'v');
|
||||
|
||||
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function countNonOrphanVisits(VisitsCountFiltering $filtering): int
|
||||
private function createAllVisitsQueryBuilder(VisitsCountFiltering $filtering): QueryBuilder
|
||||
{
|
||||
return (int) $this->matchSingleScalarResult(new CountOfNonOrphanVisits($filtering));
|
||||
}
|
||||
|
||||
private function createAllVisitsQueryBuilder(VisitsListFiltering|OrphanVisitsListFiltering $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
|
||||
// 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();
|
||||
$qb->from(Visit::class, 'v');
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -1,39 +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\VisitsCountFiltering;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
|
||||
|
||||
class CountOfNonOrphanVisits extends BaseSpecification
|
||||
{
|
||||
public function __construct(private readonly VisitsCountFiltering $filtering)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function getSpec(): Specification
|
||||
{
|
||||
$conditions = [
|
||||
Spec::isNotNull('shortUrl'),
|
||||
new InDateRange($this->filtering->dateRange),
|
||||
];
|
||||
|
||||
if ($this->filtering->excludeBots) {
|
||||
$conditions[] = Spec::eq('potentialBot', false);
|
||||
}
|
||||
|
||||
$apiKey = $this->filtering->apiKey;
|
||||
if ($apiKey !== null) {
|
||||
$conditions[] = new WithApiKeySpecsEnsuringJoin($apiKey, 'shortUrl');
|
||||
}
|
||||
|
||||
return Spec::countOf(Spec::andX(...$conditions));
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -23,6 +24,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;
|
||||
@ -37,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)
|
||||
{
|
||||
}
|
||||
|
||||
@ -88,7 +90,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);
|
||||
@ -127,10 +129,13 @@ 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(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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -22,6 +22,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;
|
||||
use Shlinkio\Shlink\Core\Visit\Repository\OrphanVisitsCountRepository;
|
||||
use Shlinkio\Shlink\Core\Visit\Repository\ShortUrlVisitsCountRepository;
|
||||
use Shlinkio\Shlink\Core\Visit\Repository\VisitRepository;
|
||||
@ -187,13 +189,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')),
|
||||
)));
|
||||
}
|
||||
@ -203,21 +205,40 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
{
|
||||
$foo = 'foo';
|
||||
|
||||
$this->createShortUrlsAndVisits(false, [$foo]);
|
||||
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||
ShortUrlInputFilter::LONG_URL => 'https://longUrl',
|
||||
ShortUrlInputFilter::TAGS => [$foo],
|
||||
ShortUrlInputFilter::DOMAIN => 'foo.com',
|
||||
]), $this->relationResolver);
|
||||
$this->getEntityManager()->persist($shortUrl1);
|
||||
$this->createVisitsForShortUrl($shortUrl1, 6);
|
||||
|
||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||
ShortUrlInputFilter::LONG_URL => 'https://longUrl',
|
||||
ShortUrlInputFilter::TAGS => [$foo],
|
||||
]), $this->relationResolver);
|
||||
$this->getEntityManager()->persist($shortUrl2);
|
||||
$this->createVisitsForShortUrl($shortUrl2, 6);
|
||||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$this->createShortUrlsAndVisits(false, [$foo]);
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertEquals(0, $this->repo->countVisitsByTag('invalid', new VisitsCountFiltering()));
|
||||
self::assertEquals(12, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering()));
|
||||
self::assertEquals(8, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering(null, true)));
|
||||
self::assertEquals(4, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering(
|
||||
self::assertEquals(0, $this->repo->countVisitsByTag('invalid', new WithDomainVisitsCountFiltering()));
|
||||
self::assertEquals(12, $this->repo->countVisitsByTag($foo, new WithDomainVisitsCountFiltering()));
|
||||
self::assertEquals(8, $this->repo->countVisitsByTag($foo, new WithDomainVisitsCountFiltering(
|
||||
excludeBots: true,
|
||||
)));
|
||||
self::assertEquals(4, $this->repo->countVisitsByTag($foo, new WithDomainVisitsCountFiltering(
|
||||
DateRange::between(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
|
||||
)));
|
||||
self::assertEquals(8, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering(
|
||||
self::assertEquals(8, $this->repo->countVisitsByTag($foo, new WithDomainVisitsCountFiltering(
|
||||
DateRange::since(Chronos::parse('2016-01-03')),
|
||||
)));
|
||||
self::assertEquals(6, $this->repo->countVisitsByTag($foo, new WithDomainVisitsCountFiltering(
|
||||
domain: 'foo.com',
|
||||
)));
|
||||
self::assertEquals(6, $this->repo->countVisitsByTag($foo, new WithDomainVisitsCountFiltering(
|
||||
domain: Domain::DEFAULT_AUTHORITY,
|
||||
)));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@ -318,13 +339,17 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertEquals(4 + 5 + 7, $this->repo->countNonOrphanVisits(new VisitsCountFiltering()));
|
||||
self::assertEquals(4 + 5 + 7, $this->repo->countNonOrphanVisits(new WithDomainVisitsCountFiltering()));
|
||||
self::assertEquals(4 + 5 + 7, $this->countRepo->countNonOrphanVisits(new VisitsCountFiltering()));
|
||||
self::assertEquals(4, $this->repo->countNonOrphanVisits(new VisitsCountFiltering(apiKey: $apiKey1)));
|
||||
self::assertEquals(4, $this->repo->countNonOrphanVisits(new WithDomainVisitsCountFiltering(apiKey: $apiKey1)));
|
||||
self::assertEquals(4, $this->countRepo->countNonOrphanVisits(new VisitsCountFiltering(apiKey: $apiKey1)));
|
||||
self::assertEquals(5 + 7, $this->repo->countNonOrphanVisits(new VisitsCountFiltering(apiKey: $apiKey2)));
|
||||
self::assertEquals(5 + 7, $this->repo->countNonOrphanVisits(new WithDomainVisitsCountFiltering(
|
||||
apiKey: $apiKey2,
|
||||
)));
|
||||
self::assertEquals(5 + 7, $this->countRepo->countNonOrphanVisits(new VisitsCountFiltering(apiKey: $apiKey2)));
|
||||
self::assertEquals(4 + 7, $this->repo->countNonOrphanVisits(new VisitsCountFiltering(apiKey: $domainApiKey)));
|
||||
self::assertEquals(4 + 7, $this->repo->countNonOrphanVisits(new WithDomainVisitsCountFiltering(
|
||||
apiKey: $domainApiKey,
|
||||
)));
|
||||
self::assertEquals(4 + 7, $this->countRepo->countNonOrphanVisits(new VisitsCountFiltering(
|
||||
apiKey: $domainApiKey,
|
||||
)));
|
||||
@ -334,21 +359,27 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
self::assertEquals(0, $this->orphanCountRepo->countOrphanVisits(new OrphanVisitsCountFiltering(
|
||||
apiKey: $noOrphanVisitsApiKey,
|
||||
)));
|
||||
self::assertEquals(4, $this->repo->countNonOrphanVisits(new VisitsCountFiltering(DateRange::since(
|
||||
self::assertEquals(4, $this->repo->countNonOrphanVisits(new WithDomainVisitsCountFiltering(DateRange::since(
|
||||
Chronos::parse('2016-01-05')->startOfDay(),
|
||||
))));
|
||||
self::assertEquals(2, $this->repo->countNonOrphanVisits(new VisitsCountFiltering(DateRange::since(
|
||||
self::assertEquals(2, $this->repo->countNonOrphanVisits(new WithDomainVisitsCountFiltering(DateRange::since(
|
||||
Chronos::parse('2016-01-03')->startOfDay(),
|
||||
), false, $apiKey1)));
|
||||
self::assertEquals(1, $this->repo->countNonOrphanVisits(new VisitsCountFiltering(DateRange::since(
|
||||
self::assertEquals(1, $this->repo->countNonOrphanVisits(new WithDomainVisitsCountFiltering(DateRange::since(
|
||||
Chronos::parse('2016-01-07')->startOfDay(),
|
||||
), false, $apiKey2)));
|
||||
self::assertEquals(3 + 5, $this->repo->countNonOrphanVisits(
|
||||
new VisitsCountFiltering(excludeBots: true, apiKey: $apiKey2),
|
||||
new WithDomainVisitsCountFiltering(excludeBots: true, apiKey: $apiKey2),
|
||||
));
|
||||
self::assertEquals(3 + 5, $this->countRepo->countNonOrphanVisits(
|
||||
new VisitsCountFiltering(excludeBots: true, apiKey: $apiKey2),
|
||||
));
|
||||
self::assertEquals(4 + 7, $this->repo->countNonOrphanVisits(
|
||||
new WithDomainVisitsCountFiltering(domain: $domain->authority),
|
||||
));
|
||||
self::assertEquals(5, $this->repo->countNonOrphanVisits(
|
||||
new WithDomainVisitsCountFiltering(domain: Domain::DEFAULT_AUTHORITY),
|
||||
));
|
||||
self::assertEquals(4, $this->repo->countOrphanVisits(new OrphanVisitsCountFiltering()));
|
||||
self::assertEquals(4, $this->orphanCountRepo->countOrphanVisits(new OrphanVisitsCountFiltering()));
|
||||
self::assertEquals(3, $this->repo->countOrphanVisits(new OrphanVisitsCountFiltering(excludeBots: true)));
|
||||
@ -374,11 +405,11 @@ 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(
|
||||
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)),
|
||||
));
|
||||
|
||||
@ -417,6 +448,11 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
type: OrphanVisitType::BASE_URL,
|
||||
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]
|
||||
@ -432,11 +468,11 @@ 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(
|
||||
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)),
|
||||
));
|
||||
}
|
||||
@ -465,6 +501,11 @@ 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: Domain::DEFAULT_AUTHORITY,
|
||||
defaultDomain: 's.test',
|
||||
)));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@ -479,31 +520,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]
|
||||
@ -526,6 +574,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
|
||||
/**
|
||||
* @return array{string, string, ShortUrl}
|
||||
* @fixme This method does too many things and is not intuitive. It should be removed or simplified
|
||||
*/
|
||||
private function createShortUrlsAndVisits(
|
||||
bool|string $withDomain = true,
|
||||
@ -558,6 +607,10 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
return [$shortCode, $domain, $shortUrl];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $amount - How many visits in total. Defaults to 6
|
||||
* @param int $botsAmount - How many of the visits should be bots. Defaults to 2
|
||||
*/
|
||||
private function createVisitsForShortUrl(ShortUrl $shortUrl, int $amount = 6, int $botsAmount = 2): void
|
||||
{
|
||||
for ($i = 0; $i < $amount; $i++) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -29,10 +30,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;
|
||||
@ -52,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')]
|
||||
@ -147,7 +150,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 +173,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 +268,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()));
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -31,5 +31,7 @@ class NonOrphanVisitsTest extends ApiTestCase
|
||||
yield 'bots excluded' => [['excludeBots' => 'true'], 6, 6];
|
||||
yield 'bots excluded and pagination' => [['excludeBots' => 'true', 'page' => 1, 'itemsPerPage' => 4], 6, 4];
|
||||
yield 'date filter' => [['startDate' => Chronos::now()->addDays(1)->toAtomString()], 0, 0];
|
||||
yield 'domain filter' => [['domain' => 'example.com'], 0, 0];
|
||||
yield 'default domain filter' => [['domain' => 'DEFAULT'], 7, 7];
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ class OrphanVisitsTest extends ApiTestCase
|
||||
'userAgent' => 'cf-facebook',
|
||||
'visitLocation' => null,
|
||||
'potentialBot' => true,
|
||||
'visitedUrl' => 'foo.com',
|
||||
'visitedUrl' => 'https://example.com/short',
|
||||
'type' => 'invalid_short_url',
|
||||
'redirectUrl' => null,
|
||||
];
|
||||
@ -29,7 +29,7 @@ class OrphanVisitsTest extends ApiTestCase
|
||||
'userAgent' => 'shlink-tests-agent',
|
||||
'visitLocation' => null,
|
||||
'potentialBot' => false,
|
||||
'visitedUrl' => '',
|
||||
'visitedUrl' => 'https://s.test/bar',
|
||||
'type' => 'regular_404',
|
||||
'redirectUrl' => null,
|
||||
];
|
||||
@ -39,7 +39,7 @@ class OrphanVisitsTest extends ApiTestCase
|
||||
'userAgent' => 'shlink-tests-agent',
|
||||
'visitLocation' => null,
|
||||
'potentialBot' => false,
|
||||
'visitedUrl' => '',
|
||||
'visitedUrl' => 'https://s.test/foo',
|
||||
'type' => 'base_url',
|
||||
'redirectUrl' => null,
|
||||
];
|
||||
@ -80,6 +80,14 @@ class OrphanVisitsTest extends ApiTestCase
|
||||
1,
|
||||
[self::INVALID_SHORT_URL],
|
||||
];
|
||||
yield 'example domain only' => [['domain' => 'example.com'], 1, 1, [self::INVALID_SHORT_URL]];
|
||||
yield 'default domain only' => [['domain' => 's.test'], 2, 2, [self::REGULAR_NOT_FOUND, self::BASE_URL]];
|
||||
yield 'default domain only with DEFAULT keyword' => [
|
||||
['domain' => 'DEFAULT'],
|
||||
2,
|
||||
2,
|
||||
[self::REGULAR_NOT_FOUND, self::BASE_URL],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@ -17,11 +17,11 @@ class TagVisitsTest extends ApiTestCase
|
||||
public function expectedVisitsAreReturned(
|
||||
string $apiKey,
|
||||
string $tag,
|
||||
bool $excludeBots,
|
||||
array $query,
|
||||
int $expectedVisitsAmount,
|
||||
): void {
|
||||
$resp = $this->callApiWithKey(self::METHOD_GET, sprintf('/tags/%s/visits', $tag), [
|
||||
RequestOptions::QUERY => $excludeBots ? ['excludeBots' => true] : [],
|
||||
RequestOptions::QUERY => $query,
|
||||
], $apiKey);
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
@ -33,16 +33,18 @@ class TagVisitsTest extends ApiTestCase
|
||||
|
||||
public static function provideTags(): iterable
|
||||
{
|
||||
yield 'foo with admin API key' => ['valid_api_key', 'foo', false, 5];
|
||||
yield 'foo with admin API key and no bots' => ['valid_api_key', 'foo', true, 4];
|
||||
yield 'bar with admin API key' => ['valid_api_key', 'bar', false, 2];
|
||||
yield 'bar with admin API key and no bots' => ['valid_api_key', 'bar', true, 1];
|
||||
yield 'baz with admin API key' => ['valid_api_key', 'baz', false, 0];
|
||||
yield 'foo with author API key' => ['author_api_key', 'foo', false, 5];
|
||||
yield 'foo with author API key and no bots' => ['author_api_key', 'foo', true, 4];
|
||||
yield 'bar with author API key' => ['author_api_key', 'bar', false, 2];
|
||||
yield 'bar with author API key and no bots' => ['author_api_key', 'bar', true, 1];
|
||||
yield 'foo with domain API key' => ['domain_api_key', 'foo', false, 0];
|
||||
yield 'foo with admin API key' => ['valid_api_key', 'foo', [], 5];
|
||||
yield 'foo with admin API key and no bots' => ['valid_api_key', 'foo', ['excludeBots' => true], 4];
|
||||
yield 'bar with admin API key' => ['valid_api_key', 'bar', [], 2];
|
||||
yield 'bar with admin API key and no bots' => ['valid_api_key', 'bar', ['excludeBots' => true], 1];
|
||||
yield 'baz with admin API key' => ['valid_api_key', 'baz', [], 0];
|
||||
yield 'foo with author API key' => ['author_api_key', 'foo', [], 5];
|
||||
yield 'foo with author API key and no bots' => ['author_api_key', 'foo', ['excludeBots' => true], 4];
|
||||
yield 'bar with author API key' => ['author_api_key', 'bar', [], 2];
|
||||
yield 'bar with author API key and no bots' => ['author_api_key', 'bar', ['excludeBots' => true], 1];
|
||||
yield 'foo with domain API key' => ['domain_api_key', 'foo', [], 0];
|
||||
yield 'foo with specific domain' => ['valid_api_key', 'foo', ['domain' => 'example.com'], 0];
|
||||
yield 'foo with default domain' => ['valid_api_key', 'foo', ['domain' => 'DEFAULT'], 5];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideApiKeysAndTags')]
|
||||
|
||||
@ -58,20 +58,32 @@ class VisitsFixture extends AbstractFixture implements DependentFixtureInterface
|
||||
Visitor::fromParams('shlink-tests-agent', 'https://app.shlink.io', ''),
|
||||
));
|
||||
|
||||
// Orphan visits (s.test is the default domain in tests env)
|
||||
$manager->persist($this->setVisitDate(
|
||||
fn () => Visit::forBasePath(Visitor::fromParams('shlink-tests-agent', 'https://s.test', '1.2.3.4')),
|
||||
fn () => Visit::forBasePath(Visitor::fromParams(
|
||||
'shlink-tests-agent',
|
||||
'https://s.test',
|
||||
'1.2.3.4',
|
||||
visitedUrl: 'https://s.test/foo',
|
||||
)),
|
||||
'2020-01-01',
|
||||
));
|
||||
$manager->persist($this->setVisitDate(
|
||||
fn () => Visit::forRegularNotFound(
|
||||
Visitor::fromParams('shlink-tests-agent', 'https://s.test/foo/bar', '1.2.3.4'),
|
||||
),
|
||||
fn () => Visit::forRegularNotFound(Visitor::fromParams(
|
||||
'shlink-tests-agent',
|
||||
'https://s.test/foo/bar',
|
||||
'1.2.3.4',
|
||||
visitedUrl: 'https://s.test/bar',
|
||||
)),
|
||||
'2020-02-01',
|
||||
));
|
||||
$manager->persist($this->setVisitDate(
|
||||
fn () => Visit::forInvalidShortUrl(
|
||||
Visitor::fromParams('cf-facebook', 'https://s.test/foo', '1.2.3.4', 'foo.com'),
|
||||
),
|
||||
fn () => Visit::forInvalidShortUrl(Visitor::fromParams(
|
||||
'cf-facebook',
|
||||
'https://s.test/foo',
|
||||
'1.2.3.4',
|
||||
visitedUrl: 'https://example.com/short',
|
||||
)),
|
||||
'2020-03-01',
|
||||
));
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user