diff --git a/data/docs/rest.md b/data/docs/rest.md index f93f6c48..09147518 100644 --- a/data/docs/rest.md +++ b/data/docs/rest.md @@ -222,9 +222,12 @@ Posible errors: **REQUEST** -* `GET` -> `/rest/visits/{shortCode}` +* `GET` -> `/rest/short-codes/{shortCode}/visits` * Route params: * shortCode: `string` -> The shortCode from which we eant to get the visits. +* Query params: + * startDate: `string` -> If provided, only visits older that this date will be returned + * endDate: `string` -> If provided, only visits newer that this date will be returned * Headers: * X-Auth-Token: `string` -> The token provided in the authentication request diff --git a/module/CLI/src/Command/GetVisitsCommand.php b/module/CLI/src/Command/GetVisitsCommand.php index ee4cf3ad..e7221e5e 100644 --- a/module/CLI/src/Command/GetVisitsCommand.php +++ b/module/CLI/src/Command/GetVisitsCommand.php @@ -2,6 +2,7 @@ namespace Shlinkio\Shlink\CLI\Command; use Acelaya\ZsmAnnotatedServices\Annotation\Inject; +use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Symfony\Component\Console\Command\Command; @@ -9,6 +10,7 @@ use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; @@ -35,7 +37,19 @@ class GetVisitsCommand extends Command { $this->setName('shortcode:visits') ->setDescription('Returns the detailed visits information for provided short code') - ->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get'); + ->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get') + ->addOption( + 'startDate', + 's', + InputOption::VALUE_OPTIONAL, + 'Allows to filter visits, returning only those older than start date' + ) + ->addOption( + 'endDate', + 'e', + InputOption::VALUE_OPTIONAL, + 'Allows to filter visits, returning only those newer than end date' + ); } public function interact(InputInterface $input, OutputInterface $output) @@ -60,18 +74,35 @@ class GetVisitsCommand extends Command public function execute(InputInterface $input, OutputInterface $output) { $shortCode = $input->getArgument('shortCode'); - $visits = $this->visitsTracker->info($shortCode); + $startDate = $this->getDateOption($input, 'startDate'); + $endDate = $this->getDateOption($input, 'endDate'); + + $visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate)); $table = new Table($output); $table->setHeaders([ 'Referer', 'Date', - 'Temote Address', + 'Remote Address', 'User agent', ]); foreach ($visits as $row) { - $table->addRow(array_values($row->jsonSerialize())); + $rowData = $row->jsonSerialize(); + // Unset location info + unset($rowData['visitLocation']); + + $table->addRow(array_values($rowData)); } $table->render(); } + + protected function getDateOption(InputInterface $input, $key) + { + $value = $input->getOption($key); + if (isset($value)) { + $value = new \DateTime($value); + } + + return $value; + } } diff --git a/module/Common/src/Util/DateRange.php b/module/Common/src/Util/DateRange.php new file mode 100644 index 00000000..c87f402a --- /dev/null +++ b/module/Common/src/Util/DateRange.php @@ -0,0 +1,44 @@ +startDate = $startDate; + $this->endDate = $endDate; + } + + /** + * @return \DateTimeInterface + */ + public function getStartDate() + { + return $this->startDate; + } + + /** + * @return \DateTimeInterface + */ + public function getEndDate() + { + return $this->endDate; + } + + /** + * @return bool + */ + public function isEmpty() + { + return is_null($this->startDate) && is_null($this->endDate); + } +} diff --git a/module/Core/src/Repository/VisitRepository.php b/module/Core/src/Repository/VisitRepository.php index ef7d7935..743427f6 100644 --- a/module/Core/src/Repository/VisitRepository.php +++ b/module/Core/src/Repository/VisitRepository.php @@ -2,6 +2,8 @@ namespace Shlinkio\Shlink\Core\Repository; use Doctrine\ORM\EntityRepository; +use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; class VisitRepository extends EntityRepository implements VisitRepositoryInterface @@ -16,4 +18,39 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa return $qb->getQuery()->getResult(); } + + /** + * @param ShortUrl|int $shortUrl + * @param DateRange|null $dateRange + * @return Visit[] + */ + public function findVisitsByShortUrl($shortUrl, DateRange $dateRange = null) + { + $shortUrl = $shortUrl instanceof ShortUrl + ? $shortUrl + : $this->getEntityManager()->find(ShortUrl::class, $shortUrl); + if (! isset($dateRange) || $dateRange->isEmpty()) { + $startDate = $shortUrl->getDateCreated(); + $endDate = clone $startDate; + $endDate->add(new \DateInterval('P2D')); + $dateRange = new DateRange($startDate, $endDate); + } + + $qb = $this->createQueryBuilder('v'); + $qb->where($qb->expr()->eq('v.shortUrl', ':shortUrl')) + ->setParameter('shortUrl', $shortUrl) + ->orderBy('v.date', 'DESC') ; + + // Apply date range filtering + if (! empty($dateRange->getStartDate())) { + $qb->andWhere($qb->expr()->gte('v.date', ':startDate')) + ->setParameter('startDate', $dateRange->getStartDate()); + } + if (! empty($dateRange->getEndDate())) { + $qb->andWhere($qb->expr()->lte('v.date', ':endDate')) + ->setParameter('endDate', $dateRange->getEndDate()); + } + + return $qb->getQuery()->getResult(); + } } diff --git a/module/Core/src/Repository/VisitRepositoryInterface.php b/module/Core/src/Repository/VisitRepositoryInterface.php index 6534d7ea..c65f495d 100644 --- a/module/Core/src/Repository/VisitRepositoryInterface.php +++ b/module/Core/src/Repository/VisitRepositoryInterface.php @@ -2,6 +2,8 @@ namespace Shlinkio\Shlink\Core\Repository; use Doctrine\Common\Persistence\ObjectRepository; +use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; interface VisitRepositoryInterface extends ObjectRepository @@ -10,4 +12,11 @@ interface VisitRepositoryInterface extends ObjectRepository * @return Visit[] */ public function findUnlocatedVisits(); + + /** + * @param ShortUrl|int $shortUrl + * @param DateRange|null $dateRange + * @return Visit[] + */ + public function findVisitsByShortUrl($shortUrl, DateRange $dateRange = null); } diff --git a/module/Core/src/Service/VisitsTracker.php b/module/Core/src/Service/VisitsTracker.php index 2fd053c1..87187aad 100644 --- a/module/Core/src/Service/VisitsTracker.php +++ b/module/Core/src/Service/VisitsTracker.php @@ -4,9 +4,10 @@ namespace Shlinkio\Shlink\Core\Service; use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Doctrine\ORM\EntityManagerInterface; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; +use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; -use Zend\Paginator\Paginator; +use Shlinkio\Shlink\Core\Repository\VisitRepository; class VisitsTracker implements VisitsTrackerInterface { @@ -62,12 +63,13 @@ class VisitsTracker implements VisitsTrackerInterface } /** - * Returns the visits on certain shortcode + * Returns the visits on certain short code * * @param $shortCode - * @return Paginator|Visit[] + * @param DateRange $dateRange + * @return Visit[] */ - public function info($shortCode) + public function info($shortCode, DateRange $dateRange = null) { /** @var ShortUrl $shortUrl */ $shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([ @@ -77,10 +79,8 @@ class VisitsTracker implements VisitsTrackerInterface throw new InvalidArgumentException(sprintf('Short code "%s" not found', $shortCode)); } - return $this->em->getRepository(Visit::class)->findBy([ - 'shortUrl' => $shortUrl, - ], [ - 'date' => 'DESC' - ]); + /** @var VisitRepository $repo */ + $repo = $this->em->getRepository(Visit::class); + return $repo->findVisitsByShortUrl($shortUrl, $dateRange); } } diff --git a/module/Core/src/Service/VisitsTrackerInterface.php b/module/Core/src/Service/VisitsTrackerInterface.php index 04966403..cec254d3 100644 --- a/module/Core/src/Service/VisitsTrackerInterface.php +++ b/module/Core/src/Service/VisitsTrackerInterface.php @@ -1,8 +1,8 @@ 'rest-get-visits', - 'path' => '/rest/visits/{shortCode}', + 'path' => '/rest/short-codes/{shortCode}/visits', 'middleware' => Action\GetVisitsMiddleware::class, 'allowed_methods' => ['GET', 'OPTIONS'], ], diff --git a/module/Rest/src/Action/GetVisitsMiddleware.php b/module/Rest/src/Action/GetVisitsMiddleware.php index 148053ee..3e5bfbaf 100644 --- a/module/Rest/src/Action/GetVisitsMiddleware.php +++ b/module/Rest/src/Action/GetVisitsMiddleware.php @@ -5,6 +5,7 @@ use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; +use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Rest\Util\RestUtils; @@ -37,14 +38,15 @@ class GetVisitsMiddleware extends AbstractRestMiddleware public function dispatch(Request $request, Response $response, callable $out = null) { $shortCode = $request->getAttribute('shortCode'); + $startDate = $this->getDateQueryParam($request, 'startDate'); + $endDate = $this->getDateQueryParam($request, 'endDate'); try { - $visits = $this->visitsTracker->info($shortCode); + $visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate)); return new JsonResponse([ 'visits' => [ 'data' => $visits, -// 'pagination' => [], ] ]); } catch (InvalidArgumentException $e) { @@ -59,4 +61,19 @@ class GetVisitsMiddleware extends AbstractRestMiddleware ], 500); } } + + /** + * @param Request $request + * @param $key + * @return \DateTime|null + */ + protected function getDateQueryParam(Request $request, $key) + { + $query = $request->getQueryParams(); + if (! isset($query[$key]) || empty($query[$key])) { + return null; + } + + return new \DateTime($query[$key]); + } }