diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index 60da6d2a..680a195d 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -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'], + ], ], ]; diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 46890750..8dada5e8 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -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, ] ], diff --git a/src/Entity/Visit.php b/src/Entity/Visit.php index 2e8fad6f..42aebec5 100644 --- a/src/Entity/Visit.php +++ b/src/Entity/Visit.php @@ -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 json_encode, + * 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, + ]; + } } diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..4b851930 --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,6 @@ +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); + } + } +} diff --git a/src/Service/VisitsTracker.php b/src/Service/VisitsTracker.php index 453627d9..4d091288 100644 --- a/src/Service/VisitsTracker.php +++ b/src/Service/VisitsTracker.php @@ -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' + ]); + } } diff --git a/src/Service/VisitsTrackerInterface.php b/src/Service/VisitsTrackerInterface.php index 3b2fc874..0d524223 100644 --- a/src/Service/VisitsTrackerInterface.php +++ b/src/Service/VisitsTrackerInterface.php @@ -1,6 +1,8 @@