Implemented rest endpoint to return shortcode visits

This commit is contained in:
Alejandro Celaya 2016-06-12 21:51:06 +02:00
parent ab8ccd7df1
commit 305df3a95b
8 changed files with 158 additions and 10 deletions

View file

@ -15,16 +15,22 @@ return [
// Rest
[
'name' => 'rest-create-shortcode',
'path' => '/rest/short-code',
'path' => '/rest/short-codes',
'middleware' => Rest\CreateShortcodeMiddleware::class,
'allowed_methods' => ['POST'],
],
[
'name' => 'rest-resolve-url',
'path' => '/rest/short-code/{shortCode}',
'path' => '/rest/short-codes/{shortCode}',
'middleware' => Rest\ResolveUrlMiddleware::class,
'allowed_methods' => ['GET'],
],
[
'name' => 'rest-get-visits',
'path' => '/rest/visits/{shortCode}',
'middleware' => Rest\GetVisitsMiddleware::class,
'allowed_methods' => ['GET'],
],
],
];

View file

@ -46,11 +46,13 @@ return [
Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\CreateShortcodeMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class,
],
'aliases' => [
'em' => EntityManager::class,
'httpClient' => GuzzleHttp\Client::class,
Router\RouterInterface::class => Router\FastRouteRouter::class,
AnnotatedFactory::CACHE_SERVICE => Cache::class,
]
],

View file

@ -11,7 +11,7 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\Entity
* @ORM\Table(name="visits")
*/
class Visit extends AbstractEntity
class Visit extends AbstractEntity implements \JsonSerializable
{
/**
* @var string
@ -134,4 +134,21 @@ class Visit extends AbstractEntity
$this->userAgent = $userAgent;
return $this;
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return [
'referer' => $this->referer,
'date' => isset($this->date) ? $this->date->format(\DateTime::ISO8601) : null,
'remoteAddr' => $this->remoteAddr,
'userAgent' => $this->userAgent,
];
}
}

View file

@ -0,0 +1,6 @@
<?php
namespace Acelaya\UrlShortener\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View file

@ -0,0 +1,82 @@
<?php
namespace Acelaya\UrlShortener\Middleware\Rest;
use Acelaya\UrlShortener\Exception\InvalidArgumentException;
use Acelaya\UrlShortener\Service\VisitsTracker;
use Acelaya\UrlShortener\Service\VisitsTrackerInterface;
use Acelaya\UrlShortener\Util\RestUtils;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Stratigility\MiddlewareInterface;
class GetVisitsMiddleware implements MiddlewareInterface
{
/**
* @var VisitsTrackerInterface
*/
private $visitsTracker;
/**
* GetVisitsMiddleware constructor.
* @param VisitsTrackerInterface|VisitsTracker $visitsTracker
*
* @Inject({VisitsTracker::class})
*/
public function __construct(VisitsTrackerInterface $visitsTracker)
{
$this->visitsTracker = $visitsTracker;
}
/**
* Process an incoming request and/or response.
*
* Accepts a server-side request and a response instance, and does
* something with them.
*
* If the response is not complete and/or further processing would not
* interfere with the work done in the middleware, or if the middleware
* wants to delegate to another process, it can use the `$out` callable
* if present.
*
* If the middleware does not return a value, execution of the current
* request is considered complete, and the response instance provided will
* be considered the response to return.
*
* Alternately, the middleware may return a response instance.
*
* Often, middleware will `return $out();`, with the assumption that a
* later middleware will return a response.
*
* @param Request $request
* @param Response $response
* @param null|callable $out
* @return null|Response
*/
public function __invoke(Request $request, Response $response, callable $out = null)
{
$shortCode = $request->getAttribute('shortCode');
try {
$visits = $this->visitsTracker->info($shortCode);
return new JsonResponse([
'visits' => [
'data' => $visits,
// 'pagination' => [],
]
]);
} catch (InvalidArgumentException $e) {
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => sprintf('Provided short code "%s" is invalid', $shortCode),
], 400);
} catch (\Exception $e) {
return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR,
'message' => 'Unexpected error occured',
], 500);
}
}
}

View file

@ -3,6 +3,8 @@ namespace Acelaya\UrlShortener\Service;
use Acelaya\UrlShortener\Entity\ShortUrl;
use Acelaya\UrlShortener\Entity\Visit;
use Acelaya\UrlShortener\Exception\InvalidArgumentException;
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Doctrine\ORM\EntityManagerInterface;
@ -58,4 +60,27 @@ class VisitsTracker implements VisitsTrackerInterface
{
return isset($array[$key]) ? $array[$key] : $default;
}
/**
* Returns the visits on certain shortcode
*
* @param $shortCode
* @return Visit[]
*/
public function info($shortCode)
{
/** @var ShortUrl $shortUrl */
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
'shortCode' => $shortCode,
]);
if (! isset($shortUrl)) {
throw new InvalidArgumentException(sprintf('Short code "%s" not found', $shortCode));
}
return $this->em->getRepository(Visit::class)->findBy([
'shortUrl' => $shortUrl,
], [
'date' => 'DESC'
]);
}
}

View file

@ -1,6 +1,8 @@
<?php
namespace Acelaya\UrlShortener\Service;
use Acelaya\UrlShortener\Entity\Visit;
interface VisitsTrackerInterface
{
/**
@ -10,4 +12,12 @@ interface VisitsTrackerInterface
* @param array $visitorData Defaults to global $_SERVER
*/
public function track($shortCode, array $visitorData = null);
/**
* Returns the visits on certain shortcode
*
* @param $shortCode
* @return Visit[]
*/
public function info($shortCode);
}

View file

@ -1,24 +1,24 @@
<?php
namespace Acelaya\UrlShortener\Util;
use Acelaya\UrlShortener\Exception\ExceptionInterface;
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
use Acelaya\UrlShortener\Exception\InvalidUrlException;
use Acelaya\UrlShortener\Exception;
class RestUtils
{
const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE';
const INVALID_URL_ERROR = 'INVALID_URL';
const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMEN';
const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT';
const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
public static function getRestErrorCodeFromException(ExceptionInterface $e)
public static function getRestErrorCodeFromException(Exception\ExceptionInterface $e)
{
switch (true) {
case $e instanceof InvalidShortCodeException:
case $e instanceof Exception\InvalidShortCodeException:
return self::INVALID_SHORTCODE_ERROR;
case $e instanceof InvalidUrlException:
case $e instanceof Exception\InvalidUrlException:
return self::INVALID_URL_ERROR;
case $e instanceof Exception\InvalidArgumentException:
return self::INVALID_ARGUMENT_ERROR;
default:
return self::UNKNOWN_ERROR;
}