Applied API role specs to tag visits

This commit is contained in:
Alejandro Celaya
2021-01-04 11:14:28 +01:00
parent 4a1e7b761a
commit 8aa6bdb934
17 changed files with 214 additions and 37 deletions

View File

@@ -4,20 +4,28 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
{
private VisitRepositoryInterface $visitRepository;
private string $tag;
private VisitsParams $params;
private ?ApiKey $apiKey;
public function __construct(VisitRepositoryInterface $visitRepository, string $tag, VisitsParams $params)
{
public function __construct(
VisitRepositoryInterface $visitRepository,
string $tag,
VisitsParams $params,
?ApiKey $apiKey
) {
$this->visitRepository = $visitRepository;
$this->params = $params;
$this->tag = $tag;
$this->apiKey = $apiKey;
}
public function getItems($offset, $itemCountPerPage): array // phpcs:ignore
@@ -27,11 +35,21 @@ class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
$this->params->getDateRange(),
$itemCountPerPage,
$offset,
$this->resolveSpec(),
);
}
protected function doCount(): int
{
return $this->visitRepository->countVisitsByTag($this->tag, $this->params->getDateRange());
return $this->visitRepository->countVisitsByTag(
$this->tag,
$this->params->getDateRange(),
$this->resolveSpec(),
);
}
private function resolveSpec(): ?Specification
{
return $this->apiKey !== null ? $this->apiKey->spec(true) : null;
}
}

View File

@@ -5,9 +5,13 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Repository;
use Happyr\DoctrineSpecification\EntitySpecificationRepository;
use Happyr\DoctrineSpecification\Spec;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
use Shlinkio\Shlink\Core\Tag\Spec\CountTagsWithName;
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function Functional\map;
@@ -47,4 +51,14 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
fn (array $row) => new TagInfo($row['tag'], (int) $row['shortUrlsCount'], (int) $row['visitsCount']),
);
}
public function tagExists(string $tag, ?ApiKey $apiKey = null): bool
{
$result = (int) $this->matchSingleScalarResult(Spec::andX(
new CountTagsWithName($tag),
new WithApiKeySpecsEnsuringJoin($apiKey),
));
return $result > 0;
}
}

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ObjectRepository;
use Happyr\DoctrineSpecification\EntitySpecificationRepositoryInterface;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
interface TagRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
{
@@ -17,4 +18,6 @@ interface TagRepositoryInterface extends ObjectRepository, EntitySpecificationRe
* @return TagInfo[]
*/
public function findTagsWithInfo(?Specification $spec = null): array;
public function tagExists(string $tag, ?ApiKey $apiKey = null): bool;
}

View File

@@ -131,32 +131,36 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
string $tag,
?DateRange $dateRange = null,
?int $limit = null,
?int $offset = null
?int $offset = null,
?Specification $spec = null
): array {
$qb = $this->createVisitsByTagQueryBuilder($tag, $dateRange);
$qb = $this->createVisitsByTagQueryBuilder($tag, $dateRange, $spec);
return $this->resolveVisitsWithNativeQuery($qb, $limit, $offset);
}
public function countVisitsByTag(string $tag, ?DateRange $dateRange = null): int
public function countVisitsByTag(string $tag, ?DateRange $dateRange = null, ?Specification $spec = null): int
{
$qb = $this->createVisitsByTagQueryBuilder($tag, $dateRange);
$qb = $this->createVisitsByTagQueryBuilder($tag, $dateRange, $spec);
$qb->select('COUNT(v.id)');
return (int) $qb->getQuery()->getSingleScalarResult();
}
private function createVisitsByTagQueryBuilder(string $tag, ?DateRange $dateRange = null): QueryBuilder
{
// Parameters in this query need to be part of the query itself, as we need to use it a sub-query later
private function createVisitsByTagQueryBuilder(
string $tag,
?DateRange $dateRange,
?Specification $spec
): 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 strictly provided by the caller, it's reasonably safe
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Visit::class, 'v')
->join('v.shortUrl', 's')
->join('s.tags', 't')
->where($qb->expr()->eq('t.name', '\'' . $tag . '\''));
->where($qb->expr()->eq('t.name', '\'' . $tag . '\'')); // This needs to be concatenated, not bound
// Apply date range filtering
$this->applyDatesInline($qb, $dateRange);
$this->applySpecification($qb, $spec, 'v');
return $qb;
}

View File

@@ -55,8 +55,9 @@ interface VisitRepositoryInterface extends ObjectRepository, EntitySpecification
string $tag,
?DateRange $dateRange = null,
?int $limit = null,
?int $offset = null
?int $offset = null,
?Specification $spec = null
): array;
public function countVisitsByTag(string $tag, ?DateRange $dateRange = null): int;
public function countVisitsByTag(string $tag, ?DateRange $dateRange = null, ?Specification $spec = null): int;
}

View File

@@ -76,18 +76,17 @@ class VisitsTracker implements VisitsTrackerInterface
* @return Visit[]|Paginator
* @throws TagNotFoundException
*/
public function visitsForTag(string $tag, VisitsParams $params): Paginator
public function visitsForTag(string $tag, VisitsParams $params, ?ApiKey $apiKey = null): Paginator
{
/** @var TagRepository $tagRepo */
$tagRepo = $this->em->getRepository(Tag::class);
$count = $tagRepo->count(['name' => $tag]);
if ($count === 0) {
if (! $tagRepo->tagExists($tag, $apiKey)) {
throw TagNotFoundException::fromTag($tag);
}
/** @var VisitRepositoryInterface $repo */
$repo = $this->em->getRepository(Visit::class);
$paginator = new Paginator(new VisitsForTagPaginatorAdapter($repo, $tag, $params));
$paginator = new Paginator(new VisitsForTagPaginatorAdapter($repo, $tag, $params, $apiKey));
$paginator->setItemCountPerPage($params->getItemsPerPage())
->setCurrentPageNumber($params->getPage());

View File

@@ -28,5 +28,5 @@ interface VisitsTrackerInterface
* @return Visit[]|Paginator
* @throws TagNotFoundException
*/
public function visitsForTag(string $tag, VisitsParams $params): Paginator;
public function visitsForTag(string $tag, VisitsParams $params, ?ApiKey $apiKey = null): Paginator;
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Spec;
use Doctrine\ORM\QueryBuilder;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class BelongsToApiKeyInlined implements Specification
{
private ApiKey $apiKey;
public function __construct(ApiKey $apiKey)
{
$this->apiKey = $apiKey;
}
public function getFilter(QueryBuilder $qb, string $dqlAlias): string
{
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later
return (string) $qb->expr()->eq('s.authorApiKey', '\'' . $this->apiKey->getId() . '\'');
}
public function modify(QueryBuilder $qb, string $dqlAlias): void
{
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Spec;
use Doctrine\ORM\QueryBuilder;
use Happyr\DoctrineSpecification\Specification\Specification;
class BelongsToDomainInlined implements Specification
{
private int $domainId;
public function __construct(int $domainId)
{
$this->domainId = $domainId;
}
public function getFilter(QueryBuilder $qb, string $dqlAlias): string
{
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later
return (string) $qb->expr()->eq('s.domain', '\'' . $this->domainId . '\'');
}
public function modify(QueryBuilder $qb, string $dqlAlias): void
{
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Tag\Spec;
use Happyr\DoctrineSpecification\BaseSpecification;
use Happyr\DoctrineSpecification\Spec;
use Happyr\DoctrineSpecification\Specification\Specification;
class CountTagsWithName extends BaseSpecification
{
private string $tagName;
public function __construct(string $tagName)
{
parent::__construct();
$this->tagName = $tagName;
}
protected function getSpec(): Specification
{
return Spec::countOf(
Spec::andX(
Spec::select('id'),
Spec::eq('name', $this->tagName),
),
);
}
}