Merge branch 'feature/21' into develop

This commit is contained in:
Alejandro Celaya 2016-07-21 10:16:52 +02:00
commit b413b16c86
9 changed files with 163 additions and 21 deletions

View file

@ -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

View file

@ -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;
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Shlinkio\Shlink\Common\Util;
class DateRange
{
/**
* @var \DateTimeInterface
*/
private $startDate;
/**
* @var \DateTimeInterface
*/
private $endDate;
public function __construct(\DateTimeInterface $startDate = null, \DateTimeInterface $endDate = null)
{
$this->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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -1,8 +1,8 @@
<?php
namespace Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Zend\Paginator\Paginator;
interface VisitsTrackerInterface
{
@ -15,10 +15,11 @@ interface VisitsTrackerInterface
public function track($shortCode, array $visitorData = null);
/**
* 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);
}

View file

@ -30,7 +30,7 @@ return [
],
[
'name' => 'rest-get-visits',
'path' => '/rest/visits/{shortCode}',
'path' => '/rest/short-codes/{shortCode}/visits',
'middleware' => Action\GetVisitsMiddleware::class,
'allowed_methods' => ['GET', 'OPTIONS'],
],

View file

@ -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]);
}
}