mirror of
https://github.com/shlinkio/shlink.git
synced 2025-01-21 22:13:15 -06:00
Created endpoint to get visits for one specific domain
This commit is contained in:
parent
e029d91544
commit
e11bf6ac67
@ -6,7 +6,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
use const Shlinkio\Shlink\MIN_TASK_WORKERS;
|
||||
|
||||
return (static function () {
|
||||
return (static function (): array {
|
||||
$taskWorkers = (int) EnvVars::TASK_WORKER_NUM()->loadFromEnv(16);
|
||||
|
||||
return [
|
||||
|
@ -16,7 +16,7 @@ return (static function (): array {
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'domain' => [ // TODO Refactor this structure to url_shortener.schema and url_shortener.default_domain
|
||||
'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED()->loadFromEnv(true)) ? 'https' : 'http',
|
||||
'hostname' => EnvVars::DEFAULT_DOMAIN()->loadFromEnv(''),
|
||||
],
|
||||
|
@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
|
||||
return (static function () {
|
||||
return (static function (): CliApp {
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/container.php';
|
||||
return $container->get(CliApp::class);
|
||||
|
@ -22,7 +22,7 @@ if (! class_exists(LOCAL_LOCK_FACTORY)) {
|
||||
}
|
||||
|
||||
// Build container
|
||||
return (static function () {
|
||||
return (static function (): ServiceManager {
|
||||
$config = require __DIR__ . '/config.php';
|
||||
$container = new ServiceManager($config['dependencies']);
|
||||
$container->setService('config', $config);
|
||||
|
@ -3,9 +3,10 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
return (static function () {
|
||||
return (static function (): EntityManagerInterface {
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/container.php';
|
||||
return $container->get(EntityManager::class);
|
||||
|
@ -95,7 +95,7 @@
|
||||
"/rest/v{version}/tags/{tag}/visits": {
|
||||
"$ref": "paths/v2_tags_{tag}_visits.json"
|
||||
},
|
||||
"/rest/v{version}/domain/{domain}/visits": {
|
||||
"/rest/v{version}/domains/{domain}/visits": {
|
||||
"$ref": "paths/v2_domains_{domain}_visits.json"
|
||||
},
|
||||
"/rest/v{version}/visits/orphan": {
|
||||
|
@ -154,6 +154,47 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Visit[]
|
||||
*/
|
||||
public function findVisitsByDomain(string $domain, VisitsListFiltering $filtering): array
|
||||
{
|
||||
$qb = $this->createVisitsByDomainQueryBuilder($domain, $filtering);
|
||||
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
|
||||
}
|
||||
|
||||
public function countVisitsByDomain(string $domain, VisitsCountFiltering $filtering): int
|
||||
{
|
||||
$qb = $this->createVisitsByDomainQueryBuilder($domain, $filtering);
|
||||
$qb->select('COUNT(v.id)');
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function createVisitsByDomainQueryBuilder(string $domain, VisitsCountFiltering $filtering): QueryBuilder
|
||||
{
|
||||
// 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');
|
||||
|
||||
if ($domain === 'DEFAULT') {
|
||||
$qb->where($qb->expr()->isNull('s.domain'));
|
||||
} else {
|
||||
$qb->join('s.domain', 'd')
|
||||
->where($qb->expr()->eq('d.authority', $this->getEntityManager()->getConnection()->quote($domain)));
|
||||
}
|
||||
|
||||
if ($filtering->excludeBots()) {
|
||||
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
|
||||
}
|
||||
|
||||
$this->applyDatesInline($qb, $filtering->dateRange());
|
||||
$this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec(), 'v');
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function findOrphanVisits(VisitsListFiltering $filtering): array
|
||||
{
|
||||
$qb = $this->createAllVisitsQueryBuilder($filtering);
|
||||
|
@ -45,6 +45,13 @@ interface VisitRepositoryInterface extends ObjectRepository, EntitySpecification
|
||||
|
||||
public function countVisitsByTag(string $tag, VisitsCountFiltering $filtering): int;
|
||||
|
||||
/**
|
||||
* @return Visit[]
|
||||
*/
|
||||
public function findVisitsByDomain(string $domain, VisitsListFiltering $filtering): array;
|
||||
|
||||
public function countVisitsByDomain(string $domain, VisitsCountFiltering $filtering): int;
|
||||
|
||||
/**
|
||||
* @return Visit[]
|
||||
*/
|
||||
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Visit\Paginator\Adapter;
|
||||
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Paginator\Adapter\AbstractCacheableCountPaginatorAdapter;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
|
||||
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class DomainVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
||||
{
|
||||
public function __construct(
|
||||
private VisitRepositoryInterface $visitRepository,
|
||||
private string $domain,
|
||||
private VisitsParams $params,
|
||||
private ?ApiKey $apiKey,
|
||||
) {
|
||||
}
|
||||
|
||||
protected function doCount(): int
|
||||
{
|
||||
return $this->visitRepository->countVisitsByDomain(
|
||||
$this->domain,
|
||||
new VisitsCountFiltering(
|
||||
$this->params->getDateRange(),
|
||||
$this->params->excludeBots(),
|
||||
$this->apiKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSlice(int $offset, int $length): iterable
|
||||
{
|
||||
return $this->visitRepository->findVisitsByDomain(
|
||||
$this->domain,
|
||||
new VisitsListFiltering(
|
||||
$this->params->getDateRange(),
|
||||
$this->params->excludeBots(),
|
||||
$this->apiKey,
|
||||
$length,
|
||||
$offset,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,9 +7,12 @@ namespace Shlinkio\Shlink\Core\Visit;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Pagerfanta\Adapter\AdapterInterface;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
@ -19,6 +22,7 @@ use Shlinkio\Shlink\Core\Repository\TagRepository;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitsStats;
|
||||
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\DomainVisitsPaginatorAdapter;
|
||||
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\NonOrphanVisitsPaginatorAdapter;
|
||||
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\OrphanVisitsPaginatorAdapter;
|
||||
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\ShortUrlVisitsPaginatorAdapter;
|
||||
@ -85,6 +89,24 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
|
||||
return $this->createPaginator(new TagVisitsPaginatorAdapter($repo, $tag, $params, $apiKey), $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Visit[]|Paginator
|
||||
* @throws DomainNotFoundException
|
||||
*/
|
||||
public function visitsForDomain(string $domain, VisitsParams $params, ?ApiKey $apiKey = null): Paginator
|
||||
{
|
||||
/** @var DomainRepository $domainRepo */
|
||||
$domainRepo = $this->em->getRepository(Domain::class);
|
||||
if ($domain !== 'DEFAULT' && $domainRepo->count(['authority' => $domain]) === 0) {
|
||||
throw DomainNotFoundException::fromAuthority($domain);
|
||||
}
|
||||
|
||||
/** @var VisitRepositoryInterface $repo */
|
||||
$repo = $this->em->getRepository(Visit::class);
|
||||
|
||||
return $this->createPaginator(new DomainVisitsPaginatorAdapter($repo, $domain, $params, $apiKey), $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Visit[]|Paginator
|
||||
*/
|
||||
|
@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Visit;
|
||||
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
@ -33,6 +34,12 @@ interface VisitsStatsHelperInterface
|
||||
*/
|
||||
public function visitsForTag(string $tag, VisitsParams $params, ?ApiKey $apiKey = null): Paginator;
|
||||
|
||||
/**
|
||||
* @return Visit[]|Paginator
|
||||
* @throws DomainNotFoundException
|
||||
*/
|
||||
public function visitsForDomain(string $domain, VisitsParams $params, ?ApiKey $apiKey = null): Paginator;
|
||||
|
||||
/**
|
||||
* @return Visit[]|Paginator
|
||||
*/
|
||||
|
@ -34,6 +34,7 @@ return [
|
||||
Action\ShortUrl\ListShortUrlsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\ShortUrlVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\TagVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\DomainVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\GlobalVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\OrphanVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\NonOrphanVisitsAction::class => ConfigAbstractFactory::class,
|
||||
@ -73,6 +74,10 @@ return [
|
||||
],
|
||||
Action\Visit\ShortUrlVisitsAction::class => [Visit\VisitsStatsHelper::class],
|
||||
Action\Visit\TagVisitsAction::class => [Visit\VisitsStatsHelper::class],
|
||||
Action\Visit\DomainVisitsAction::class => [
|
||||
Visit\VisitsStatsHelper::class,
|
||||
'config.url_shortener.domain.hostname',
|
||||
],
|
||||
Action\Visit\GlobalVisitsAction::class => [Visit\VisitsStatsHelper::class],
|
||||
Action\Visit\OrphanVisitsAction::class => [
|
||||
Visit\VisitsStatsHelper::class,
|
||||
|
@ -6,49 +6,52 @@ namespace Shlinkio\Shlink\Rest;
|
||||
|
||||
use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler;
|
||||
|
||||
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
||||
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||
return (static function (): array {
|
||||
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
||||
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||
|
||||
return [
|
||||
return [
|
||||
|
||||
'routes' => [
|
||||
Action\HealthAction::getRouteDef(),
|
||||
'routes' => [
|
||||
Action\HealthAction::getRouteDef(),
|
||||
|
||||
// Short URLs
|
||||
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
$dropDomainMiddleware,
|
||||
$overrideDomainMiddleware,
|
||||
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class,
|
||||
]),
|
||||
Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
$overrideDomainMiddleware,
|
||||
]),
|
||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||
// Short URLs
|
||||
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
$dropDomainMiddleware,
|
||||
$overrideDomainMiddleware,
|
||||
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class,
|
||||
]),
|
||||
Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
$overrideDomainMiddleware,
|
||||
]),
|
||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||
|
||||
// Visits
|
||||
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\Visit\TagVisitsAction::getRouteDef(),
|
||||
Action\Visit\GlobalVisitsAction::getRouteDef(),
|
||||
Action\Visit\OrphanVisitsAction::getRouteDef(),
|
||||
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
||||
// Visits
|
||||
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\Visit\TagVisitsAction::getRouteDef(),
|
||||
Action\Visit\DomainVisitsAction::getRouteDef(),
|
||||
Action\Visit\GlobalVisitsAction::getRouteDef(),
|
||||
Action\Visit\OrphanVisitsAction::getRouteDef(),
|
||||
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
||||
|
||||
// Tags
|
||||
Action\Tag\ListTagsAction::getRouteDef(),
|
||||
Action\Tag\TagsStatsAction::getRouteDef(),
|
||||
Action\Tag\DeleteTagsAction::getRouteDef(),
|
||||
Action\Tag\UpdateTagAction::getRouteDef(),
|
||||
// Tags
|
||||
Action\Tag\ListTagsAction::getRouteDef(),
|
||||
Action\Tag\TagsStatsAction::getRouteDef(),
|
||||
Action\Tag\DeleteTagsAction::getRouteDef(),
|
||||
Action\Tag\UpdateTagAction::getRouteDef(),
|
||||
|
||||
// Domains
|
||||
Action\Domain\ListDomainsAction::getRouteDef(),
|
||||
Action\Domain\DomainRedirectsAction::getRouteDef(),
|
||||
// Domains
|
||||
Action\Domain\ListDomainsAction::getRouteDef(),
|
||||
Action\Domain\DomainRedirectsAction::getRouteDef(),
|
||||
|
||||
Action\MercureInfoAction::getRouteDef([NotConfiguredMercureErrorHandler::class]),
|
||||
],
|
||||
Action\MercureInfoAction::getRouteDef([NotConfiguredMercureErrorHandler::class]),
|
||||
],
|
||||
|
||||
];
|
||||
];
|
||||
})();
|
||||
|
48
module/Rest/src/Action/Visit/DomainVisitsAction.php
Normal file
48
module/Rest/src/Action/Visit/DomainVisitsAction.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Action\Visit;
|
||||
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||
|
||||
class DomainVisitsAction extends AbstractRestAction
|
||||
{
|
||||
use PagerfantaUtilsTrait;
|
||||
|
||||
protected const ROUTE_PATH = '/domains/{domain}/visits';
|
||||
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
|
||||
|
||||
public function __construct(private VisitsStatsHelperInterface $visitsHelper, private string $defaultDomain)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$domain = $this->resolveDomainParam($request);
|
||||
$params = VisitsParams::fromRawData($request->getQueryParams());
|
||||
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
|
||||
$visits = $this->visitsHelper->visitsForDomain($domain, $params, $apiKey);
|
||||
|
||||
return new JsonResponse([
|
||||
'visits' => $this->serializePaginator($visits),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveDomainParam(Request $request): string
|
||||
{
|
||||
$domainParam = $request->getAttribute('domain', '');
|
||||
if ($domainParam === $this->defaultDomain) {
|
||||
return 'DEFAULT';
|
||||
}
|
||||
|
||||
return $domainParam;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user