mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-10 01:03:13 +08:00
Added amount of bots, non-bots and total visits to the list of tags with stats
This commit is contained in:
30
module/Core/src/Tag/Model/OrderableField.php
Normal file
30
module/Core/src/Tag/Model/OrderableField.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Tag\Model;
|
||||
|
||||
use function Shlinkio\Shlink\Core\camelCaseToSnakeCase;
|
||||
|
||||
enum OrderableField: string
|
||||
{
|
||||
case TAG = 'tag';
|
||||
// case SHORT_URLS = 'shortUrls';
|
||||
// case VISITS = 'visits';
|
||||
// case NON_BOT_VISITS = 'nonBotVisits';
|
||||
|
||||
/** @deprecated Use VISITS instead */
|
||||
case VISITS_COUNT = 'visitsCount';
|
||||
/** @deprecated Use SHORT_URLS instead */
|
||||
case SHORT_URLS_COUNT = 'shortUrlsCount';
|
||||
|
||||
public static function isAggregateField(string $field): bool
|
||||
{
|
||||
return $field === self::SHORT_URLS_COUNT->value || $field === self::VISITS_COUNT->value;
|
||||
}
|
||||
|
||||
public static function toSnakeCaseValidField(?string $field): string
|
||||
{
|
||||
return camelCaseToSnakeCase($field === self::SHORT_URLS_COUNT->value ? $field : self::VISITS_COUNT->value);
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,29 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Tag\Model;
|
||||
|
||||
use JsonSerializable;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitsSummary;
|
||||
|
||||
final class TagInfo implements JsonSerializable
|
||||
{
|
||||
public readonly VisitsSummary $visitsSummary;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $tag,
|
||||
public readonly int $shortUrlsCount,
|
||||
public readonly int $visitsCount,
|
||||
int $visitsCount,
|
||||
?int $nonBotVisitsCount = null,
|
||||
) {
|
||||
$this->visitsSummary = VisitsSummary::fromTotalAndNonBots($visitsCount, $nonBotVisitsCount ?? $visitsCount);
|
||||
}
|
||||
|
||||
public static function fromRawData(array $data): self
|
||||
{
|
||||
return new self($data['tag'], (int) $data['shortUrlsCount'], (int) $data['visitsCount']);
|
||||
return new self(
|
||||
$data['tag'],
|
||||
(int) $data['shortUrlsCount'],
|
||||
(int) $data['visitsCount'],
|
||||
isset($data['nonBotVisitsCount']) ? (int) $data['nonBotVisitsCount'] : null,
|
||||
);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
@@ -25,7 +35,10 @@ final class TagInfo implements JsonSerializable
|
||||
return [
|
||||
'tag' => $this->tag,
|
||||
'shortUrlsCount' => $this->shortUrlsCount,
|
||||
'visitsCount' => $this->visitsCount,
|
||||
'visitsSummary' => $this->visitsSummary,
|
||||
|
||||
// Deprecated
|
||||
'visitsCount' => $this->visitsSummary->total,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||
use Happyr\DoctrineSpecification\Spec;
|
||||
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\OrderableField;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagsListFiltering;
|
||||
use Shlinkio\Shlink\Core\Tag\Spec\CountTagsWithName;
|
||||
@@ -16,7 +17,6 @@ use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithInlinedApiKeySpecsEnsuringJoin;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Functional\map;
|
||||
|
||||
use const PHP_INT_MAX;
|
||||
@@ -43,7 +43,7 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
||||
{
|
||||
$orderField = $filtering?->orderBy?->field;
|
||||
$orderDir = $filtering?->orderBy?->direction;
|
||||
$orderMainQuery = contains(['shortUrlsCount', 'visitsCount'], $orderField);
|
||||
$orderMainQuery = $orderField !== null && OrderableField::isAggregateField($orderField);
|
||||
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
$subQb = $this->createQueryBuilder('t');
|
||||
@@ -72,12 +72,17 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
||||
't.id_0 AS id',
|
||||
't.name_1 AS name',
|
||||
'COUNT(DISTINCT s.id) AS short_urls_count',
|
||||
'COUNT(DISTINCT v.id) AS visits_count',
|
||||
'COUNT(DISTINCT v.id) AS visits_count', // Native queries require snake_case for cross-db compatibility
|
||||
'COUNT(DISTINCT v2.id) AS non_bot_visits_count',
|
||||
)
|
||||
->from('(' . $subQb->getQuery()->getSQL() . ')', 't') // @phpstan-ignore-line
|
||||
->leftJoin('t', 'short_urls_in_tags', 'st', $nativeQb->expr()->eq('t.id_0', 'st.tag_id'))
|
||||
->leftJoin('st', 'short_urls', 's', $nativeQb->expr()->eq('s.id', 'st.short_url_id'))
|
||||
->leftJoin('st', 'visits', 'v', $nativeQb->expr()->eq('s.id', 'v.short_url_id'))
|
||||
->leftJoin('st', 'visits', 'v', $nativeQb->expr()->eq('st.short_url_id', 'v.short_url_id'))
|
||||
->leftJoin('st', 'visits', 'v2', $nativeQb->expr()->and( // @phpstan-ignore-line
|
||||
$nativeQb->expr()->eq('st.short_url_id', 'v2.short_url_id'),
|
||||
$nativeQb->expr()->eq('v2.potential_bot', $conn->quote('0')),
|
||||
))
|
||||
->groupBy('t.id_0', 't.name_1');
|
||||
|
||||
// Apply API key role conditions to the native query too, as they will affect the amounts on the aggregates
|
||||
@@ -92,10 +97,7 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
||||
|
||||
if ($orderMainQuery) {
|
||||
$nativeQb
|
||||
->orderBy(
|
||||
$orderField === 'shortUrlsCount' ? 'short_urls_count' : 'visits_count',
|
||||
$orderDir ?? 'ASC',
|
||||
)
|
||||
->orderBy(OrderableField::toSnakeCaseValidField($orderField), $orderDir ?? 'ASC')
|
||||
->setMaxResults($filtering?->limit ?? PHP_INT_MAX)
|
||||
->setFirstResult($filtering?->offset ?? 0);
|
||||
}
|
||||
@@ -107,6 +109,7 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
||||
$rsm->addScalarResult('name', 'tag');
|
||||
$rsm->addScalarResult('short_urls_count', 'shortUrlsCount');
|
||||
$rsm->addScalarResult('visits_count', 'visitsCount');
|
||||
$rsm->addScalarResult('non_bot_visits_count', 'nonBotVisitsCount');
|
||||
|
||||
return map(
|
||||
$this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm)->getResult(),
|
||||
|
||||
@@ -22,7 +22,7 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class TagService implements TagServiceInterface
|
||||
{
|
||||
public function __construct(private ORM\EntityManagerInterface $em)
|
||||
public function __construct(private readonly ORM\EntityManagerInterface $em)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user