diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 50669f66..43586e16 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -26,16 +26,20 @@ return [ Options\UrlShortenerOptions::class => ConfigAbstractFactory::class, Service\UrlShortener::class => ConfigAbstractFactory::class, - Visit\VisitsTracker::class => ConfigAbstractFactory::class, Service\ShortUrlService::class => ConfigAbstractFactory::class, - Visit\VisitLocator::class => ConfigAbstractFactory::class, - Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class, - Tag\TagService::class => ConfigAbstractFactory::class, Service\ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class, Service\ShortUrl\ShortUrlResolver::class => ConfigAbstractFactory::class, Service\ShortUrl\ShortCodeHelper::class => ConfigAbstractFactory::class, + + Tag\TagService::class => ConfigAbstractFactory::class, + Domain\DomainService::class => ConfigAbstractFactory::class, + Visit\VisitsTracker::class => ConfigAbstractFactory::class, + Visit\VisitLocator::class => ConfigAbstractFactory::class, + Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class, + Visit\Transformer\OrphanVisitDataTransformer::class => InvokableFactory::class, + Util\UrlValidator::class => ConfigAbstractFactory::class, Util\DoctrineBatchHelper::class => ConfigAbstractFactory::class, Util\RedirectResponseHelper::class => ConfigAbstractFactory::class, diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index f9a67e3d..00954049 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -9,6 +9,7 @@ use DateTimeInterface; use Fig\Http\Message\StatusCodeInterface; use Laminas\InputFilter\InputFilter; use PUGX\Shortid\Factory as ShortIdFactory; +use Shlinkio\Shlink\Common\Util\DateRange; use function Functional\reduce_left; use function is_array; @@ -44,6 +45,26 @@ function parseDateFromQuery(array $query, string $dateName): ?Chronos return ! isset($query[$dateName]) || empty($query[$dateName]) ? null : Chronos::parse($query[$dateName]); } +function parseDateRangeFromQuery(array $query, string $startDateName, string $endDateName): DateRange +{ + $startDate = parseDateFromQuery($query, $startDateName); + $endDate = parseDateFromQuery($query, $endDateName); + + if ($startDate === null && $endDate === null) { + return DateRange::emptyInstance(); + } + + if ($startDate !== null && $endDate !== null) { + return DateRange::withStartAndEndDate($startDate, $endDate); + } + + if ($startDate !== null) { + return DateRange::withStartDate($startDate); + } + + return DateRange::withEndDate($endDate); +} + /** * @param string|DateTimeInterface|Chronos|null $date */ diff --git a/module/Core/src/Entity/Visit.php b/module/Core/src/Entity/Visit.php index efcf5d8f..61739dec 100644 --- a/module/Core/src/Entity/Visit.php +++ b/module/Core/src/Entity/Visit.php @@ -109,6 +109,16 @@ class Visit extends AbstractEntity implements JsonSerializable return $this->shortUrl === null; } + public function visitedUrl(): ?string + { + return $this->visitedUrl; + } + + public function type(): string + { + return $this->type; + } + public function jsonSerialize(): array { return [ diff --git a/module/Core/src/Model/VisitsParams.php b/module/Core/src/Model/VisitsParams.php index 041aed9f..b579239b 100644 --- a/module/Core/src/Model/VisitsParams.php +++ b/module/Core/src/Model/VisitsParams.php @@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Core\Model; use Shlinkio\Shlink\Common\Util\DateRange; -use function Shlinkio\Shlink\Core\parseDateFromQuery; +use function Shlinkio\Shlink\Core\parseDateRangeFromQuery; final class VisitsParams { @@ -36,7 +36,7 @@ final class VisitsParams public static function fromRawData(array $query): self { return new self( - new DateRange(parseDateFromQuery($query, 'startDate'), parseDateFromQuery($query, 'endDate')), + parseDateRangeFromQuery($query, 'startDate', 'endDate'), (int) ($query['page'] ?? 1), isset($query['itemsPerPage']) ? (int) $query['itemsPerPage'] : null, ); diff --git a/module/Core/src/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php b/module/Core/src/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php new file mode 100644 index 00000000..7167b9e7 --- /dev/null +++ b/module/Core/src/Paginator/Adapter/OrphanVisitsPaginatorAdapter.php @@ -0,0 +1,30 @@ +repo = $repo; + $this->params = $params; + } + + protected function doCount(): int + { + return $this->repo->countOrphanVisits($this->params->getDateRange()); + } + + public function getSlice($offset, $length): iterable // phpcs:ignore + { + return $this->repo->findOrphanVisits($this->params->getDateRange(), $length, $offset); + } +} diff --git a/module/Core/src/Visit/Transformer/OrphanVisitDataTransformer.php b/module/Core/src/Visit/Transformer/OrphanVisitDataTransformer.php new file mode 100644 index 00000000..9f4842f5 --- /dev/null +++ b/module/Core/src/Visit/Transformer/OrphanVisitDataTransformer.php @@ -0,0 +1,24 @@ +jsonSerialize(); + $serializedVisit['visitedUrl'] = $visit->visitedUrl(); + $serializedVisit['type'] = $visit->type(); + + return $serializedVisit; + } +} diff --git a/module/Core/src/Visit/VisitsStatsHelper.php b/module/Core/src/Visit/VisitsStatsHelper.php index 0cb58897..61d879fd 100644 --- a/module/Core/src/Visit/VisitsStatsHelper.php +++ b/module/Core/src/Visit/VisitsStatsHelper.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Visit; use Doctrine\ORM\EntityManagerInterface; +use Pagerfanta\Adapter\AdapterInterface; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Tag; @@ -13,6 +14,7 @@ use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\TagNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\VisitsParams; +use Shlinkio\Shlink\Core\Paginator\Adapter\OrphanVisitsPaginatorAdapter; use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsForTagPaginatorAdapter; use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter; use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; @@ -58,11 +60,8 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface /** @var VisitRepositoryInterface $repo */ $repo = $this->em->getRepository(Visit::class); - $paginator = new Paginator(new VisitsPaginatorAdapter($repo, $identifier, $params, $spec)); - $paginator->setMaxPerPage($params->getItemsPerPage()) - ->setCurrentPage($params->getPage()); - return $paginator; + return $this->createPaginator(new VisitsPaginatorAdapter($repo, $identifier, $params, $spec), $params); } /** @@ -79,9 +78,26 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface /** @var VisitRepositoryInterface $repo */ $repo = $this->em->getRepository(Visit::class); - $paginator = new Paginator(new VisitsForTagPaginatorAdapter($repo, $tag, $params, $apiKey)); + + return $this->createPaginator(new VisitsForTagPaginatorAdapter($repo, $tag, $params, $apiKey), $params); + } + + /** + * @return Visit[]|Paginator + */ + public function orphanVisits(VisitsParams $params): Paginator + { + /** @var VisitRepositoryInterface $repo */ + $repo = $this->em->getRepository(Visit::class); + + return $this->createPaginator(new OrphanVisitsPaginatorAdapter($repo, $params), $params); + } + + private function createPaginator(AdapterInterface $adapter, VisitsParams $params): Paginator + { + $paginator = new Paginator($adapter); $paginator->setMaxPerPage($params->getItemsPerPage()) - ->setCurrentPage($params->getPage()); + ->setCurrentPage($params->getPage()); return $paginator; } diff --git a/module/Core/src/Visit/VisitsStatsHelperInterface.php b/module/Core/src/Visit/VisitsStatsHelperInterface.php index a67c8dcd..d2bf6032 100644 --- a/module/Core/src/Visit/VisitsStatsHelperInterface.php +++ b/module/Core/src/Visit/VisitsStatsHelperInterface.php @@ -32,4 +32,9 @@ interface VisitsStatsHelperInterface * @throws TagNotFoundException */ public function visitsForTag(string $tag, VisitsParams $params, ?ApiKey $apiKey = null): Paginator; + + /** + * @return Visit[]|Paginator + */ + public function orphanVisits(VisitsParams $params): Paginator; } diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 5b68ada7..e1a869df 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -34,6 +34,7 @@ return [ Action\Visit\ShortUrlVisitsAction::class => ConfigAbstractFactory::class, Action\Visit\TagVisitsAction::class => ConfigAbstractFactory::class, Action\Visit\GlobalVisitsAction::class => ConfigAbstractFactory::class, + Action\Visit\OrphanVisitsAction::class => ConfigAbstractFactory::class, Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class, Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class, Action\Tag\CreateTagsAction::class => ConfigAbstractFactory::class, @@ -69,6 +70,10 @@ return [ Action\Visit\ShortUrlVisitsAction::class => [Visit\VisitsStatsHelper::class], Action\Visit\TagVisitsAction::class => [Visit\VisitsStatsHelper::class], Action\Visit\GlobalVisitsAction::class => [Visit\VisitsStatsHelper::class], + Action\Visit\OrphanVisitsAction::class => [ + Visit\VisitsStatsHelper::class, + Visit\Transformer\OrphanVisitDataTransformer::class, + ], Action\ShortUrl\ListShortUrlsAction::class => [Service\ShortUrlService::class, ShortUrlDataTransformer::class], Action\ShortUrl\EditShortUrlTagsAction::class => [Service\ShortUrlService::class], Action\Tag\ListTagsAction::class => [TagService::class], diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index a5382c38..9b09a266 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -34,6 +34,7 @@ return [ Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]), Action\Visit\TagVisitsAction::getRouteDef(), Action\Visit\GlobalVisitsAction::getRouteDef(), + Action\Visit\OrphanVisitsAction::getRouteDef(), // Tags Action\Tag\ListTagsAction::getRouteDef(), diff --git a/module/Rest/src/Action/Visit/OrphanVisitsAction.php b/module/Rest/src/Action/Visit/OrphanVisitsAction.php new file mode 100644 index 00000000..7a65b920 --- /dev/null +++ b/module/Rest/src/Action/Visit/OrphanVisitsAction.php @@ -0,0 +1,43 @@ +visitsHelper = $visitsHelper; + $this->orphanVisitTransformer = $orphanVisitTransformer; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $params = VisitsParams::fromRawData($request->getQueryParams()); + $visits = $this->visitsHelper->orphanVisits($params); + + return new JsonResponse([ + 'visits' => $this->serializePaginator($visits, $this->orphanVisitTransformer), + ]); + } +}