From 1c6250618ab3996d60e8597f738d200a452c7c37 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 2 May 2016 17:52:53 +0200 Subject: [PATCH 01/28] Removed unique index from too long field --- src/Entity/ShortUrl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/ShortUrl.php b/src/Entity/ShortUrl.php index 35c7ed1b..d1d5c4f7 100644 --- a/src/Entity/ShortUrl.php +++ b/src/Entity/ShortUrl.php @@ -17,7 +17,7 @@ class ShortUrl extends AbstractEntity { /** * @var string - * @ORM\Column(name="original_url", type="string", nullable=false, length=1024, unique=true) + * @ORM\Column(name="original_url", type="string", nullable=false, length=1024) */ protected $originalUrl; /** From 35147fecb29a0602a6d39c61181a16c78e9afe94 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 3 Jul 2016 09:14:27 +0200 Subject: [PATCH 02/28] Replaced cli execution using expressive middleware by symfony/console --- bin/cli | 17 ++-- composer.json | 3 +- config/autoload/services.global.php | 4 + src/CliCommands/GenerateShortcodeCommand.php | 95 ++++++++++++++++++++ 4 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 src/CliCommands/GenerateShortcodeCommand.php diff --git a/bin/cli b/bin/cli index 0aca7dd0..1816b80f 100755 --- a/bin/cli +++ b/bin/cli @@ -1,17 +1,14 @@ #!/usr/bin/env php get(Application::class); -$command = count($_SERVER['argv']) > 1 ? $_SERVER['argv'][1] : ''; -$request = ServerRequestFactory::fromGlobals() - ->withMethod('CLI') - ->withUri(new Uri(sprintf('/%s', $command))); -$app->run($request); +$app = new CliApp(); +$app->addCommands([ + $container->get(GenerateShortcodeCommand::class), +]); +$app->run(); diff --git a/composer.json b/composer.json index d43d7fbb..922e9067 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "zendframework/zend-servicemanager": "^3.0", "doctrine/orm": "^2.5", "guzzlehttp/guzzle": "^6.2", - "acelaya/zsm-annotated-services": "^0.2.0" + "acelaya/zsm-annotated-services": "^0.2.0", + "symfony/console": "^3.0" }, "require-dev": { "phpunit/phpunit": "^4.8", diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 300f1bf3..6027973d 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,4 +1,5 @@ AnnotatedFactory::class, Cache::class => CacheFactory::class, + // Cli commands + CliCommands\GenerateShortcodeCommand::class => AnnotatedFactory::class, + // Middleware Middleware\CliRoutable\GenerateShortcodeMiddleware::class => AnnotatedFactory::class, Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, diff --git a/src/CliCommands/GenerateShortcodeCommand.php b/src/CliCommands/GenerateShortcodeCommand.php new file mode 100644 index 00000000..9f545dce --- /dev/null +++ b/src/CliCommands/GenerateShortcodeCommand.php @@ -0,0 +1,95 @@ +urlShortener = $urlShortener; + $this->domainConfig = $domainConfig; + } + + public function configure() + { + $this->setName('generate-shortcode') + ->setDescription('Generates a shortcode for provided URL and returns the short URL') + ->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse'); + } + + public function interact(InputInterface $input, OutputInterface $output) + { + $longUrl = $input->getArgument('longUrl'); + if (! empty($longUrl)) { + return; + } + + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new Question( + 'A long URL was not provided. Which URL do you want to shorten?: ' + ); + + $longUrl = $helper->ask($input, $output, $question); + if (! empty($longUrl)) { + $input->setArgument('longUrl', $longUrl); + } + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $longUrl = $input->getArgument('longUrl'); + + try { + if (! isset($longUrl)) { + $output->writeln('A URL was not provided!'); + return; + } + + $shortcode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); + $shortUrl = (new Uri())->withPath($shortcode) + ->withScheme($this->domainConfig['schema']) + ->withHost($this->domainConfig['hostname']); + + $output->writeln([ + sprintf('Processed URL %s', $longUrl), + sprintf('Generated URL %s', $shortUrl), + ]); + } catch (InvalidUrlException $e) { + $output->writeln( + sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl) + ); + } catch (\Exception $e) { + $output->writeln('' . $e . ''); + } + } +} From e09915dd211bcd57dbaee755a8592470cd0184c8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 3 Jul 2016 09:16:56 +0200 Subject: [PATCH 03/28] Removed cli-related middlewares and factories --- config/autoload/cli-routes.global.php | 15 --- .../autoload/middleware-pipeline.global.php | 2 - config/autoload/services.global.php | 2 - .../GenerateShortcodeMiddleware.php | 91 ------------------- .../Factory/CliParamsMiddlewareFactory.php | 32 ------- tests/Middleware/CliParamsMiddlewareTest.php | 91 ------------------- .../CliParamsMiddlewareFactoryTest.php | 29 ------ 7 files changed, 262 deletions(-) delete mode 100644 config/autoload/cli-routes.global.php delete mode 100644 src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php delete mode 100644 src/Middleware/Factory/CliParamsMiddlewareFactory.php delete mode 100644 tests/Middleware/CliParamsMiddlewareTest.php delete mode 100644 tests/Middleware/Factory/CliParamsMiddlewareFactoryTest.php diff --git a/config/autoload/cli-routes.global.php b/config/autoload/cli-routes.global.php deleted file mode 100644 index c2aafdf1..00000000 --- a/config/autoload/cli-routes.global.php +++ /dev/null @@ -1,15 +0,0 @@ - [ - [ - 'name' => 'cli-generate-shortcode', - 'path' => '/generate-shortcode', - 'middleware' => CliRoutable\GenerateShortcodeMiddleware::class, - 'allowed_methods' => ['CLI'], - ], - ], - -]; diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index d033f9b2..8a393ce1 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -1,5 +1,4 @@ [ 'middleware' => [ ApplicationFactory::ROUTING_MIDDLEWARE, - CliParamsMiddleware::class, Helper\UrlHelperMiddleware::class, ApplicationFactory::DISPATCH_MIDDLEWARE, ], diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 6027973d..617242b5 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -43,9 +43,7 @@ return [ CliCommands\GenerateShortcodeCommand::class => AnnotatedFactory::class, // Middleware - Middleware\CliRoutable\GenerateShortcodeMiddleware::class => AnnotatedFactory::class, Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, - Middleware\CliParamsMiddleware::class => Middleware\Factory\CliParamsMiddlewareFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php b/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php deleted file mode 100644 index 6573ed60..00000000 --- a/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php +++ /dev/null @@ -1,91 +0,0 @@ -urlShortener = $urlShortener; - $this->domainConfig = $domainConfig; - } - - /** - * 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) - { - $longUrl = $request->getAttribute('longUrl'); - - try { - if (! isset($longUrl)) { - $response->getBody()->write('A URL was not provided!' . PHP_EOL); - return; - } - - $shortcode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); - $shortUrl = (new Uri())->withPath($shortcode) - ->withScheme($this->domainConfig['schema']) - ->withHost($this->domainConfig['hostname']); - - $response->getBody()->write( - sprintf('Processed URL "%s".%sGenerated short URL "%s"', $longUrl, PHP_EOL, $shortUrl) . PHP_EOL - ); - } catch (InvalidUrlException $e) { - $response->getBody()->write( - sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl) . PHP_EOL - ); - } catch (\Exception $e) { - $response->getBody()->write($e); - } finally { - return $response; - } - } -} diff --git a/src/Middleware/Factory/CliParamsMiddlewareFactory.php b/src/Middleware/Factory/CliParamsMiddlewareFactory.php deleted file mode 100644 index 9be7f52e..00000000 --- a/src/Middleware/Factory/CliParamsMiddlewareFactory.php +++ /dev/null @@ -1,32 +0,0 @@ -__invoke( - ServerRequestFactory::fromGlobals(), - $originalResponse, - function ($req, $resp) use (&$invoked) { - $invoked = true; - return $resp; - } - ); - - $this->assertSame($originalResponse, $response); - $this->assertTrue($invoked); - } - - /** - * @test - */ - public function nonSuccessRouteResultJustInvokesNextMiddleware() - { - $middleware = new CliParamsMiddleware([], 'cli'); - - $invoked = false; - $originalResponse = new Response(); - $routeResult = $this->prophesize(RouteResult::class); - $routeResult->isSuccess()->willReturn(false)->shouldBeCalledTimes(1); - - $response = $middleware->__invoke( - ServerRequestFactory::fromGlobals()->withAttribute(RouteResult::class, $routeResult->reveal()), - $originalResponse, - function ($req, $resp) use (&$invoked) { - $invoked = true; - return $resp; - } - ); - - $this->assertSame($originalResponse, $response); - $this->assertTrue($invoked); - } - - /** - * @test - */ - public function properRouteWillInjectAttributeInResponse() - { - $expectedLongUrl = 'http://www.google.com'; - $middleware = new CliParamsMiddleware(['foo', 'bar', $expectedLongUrl], 'cli'); - - $invoked = false; - $originalResponse = new Response(); - $routeResult = $this->prophesize(RouteResult::class); - $routeResult->isSuccess()->willReturn(true)->shouldBeCalledTimes(1); - $routeResult->getMatchedRouteName()->willReturn('cli-generate-shortcode')->shouldBeCalledTimes(1); - /** @var ServerRequestInterface $request */ - $request = null; - - $response = $middleware->__invoke( - ServerRequestFactory::fromGlobals()->withAttribute(RouteResult::class, $routeResult->reveal()), - $originalResponse, - function ($req, $resp) use (&$invoked, &$request) { - $invoked = true; - $request = $req; - return $resp; - } - ); - - $this->assertSame($originalResponse, $response); - $this->assertEquals($expectedLongUrl, $request->getAttribute('longUrl')); - $this->assertTrue($invoked); - } -} diff --git a/tests/Middleware/Factory/CliParamsMiddlewareFactoryTest.php b/tests/Middleware/Factory/CliParamsMiddlewareFactoryTest.php deleted file mode 100644 index 3fad4bab..00000000 --- a/tests/Middleware/Factory/CliParamsMiddlewareFactoryTest.php +++ /dev/null @@ -1,29 +0,0 @@ -factory = new CliParamsMiddlewareFactory(); - } - - /** - * @test - */ - public function serviceIsCreated() - { - $instance = $this->factory->__invoke(new ServiceManager(), ''); - $this->assertInstanceOf(CliParamsMiddleware::class, $instance); - } -} From 8a7d5a499ee2ca137cf06dafe5298619a0cfbe3f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 4 Jul 2016 08:33:22 +0200 Subject: [PATCH 04/28] Dropped unused middleware --- src/Middleware/CliParamsMiddleware.php | 71 -------------------------- 1 file changed, 71 deletions(-) delete mode 100644 src/Middleware/CliParamsMiddleware.php diff --git a/src/Middleware/CliParamsMiddleware.php b/src/Middleware/CliParamsMiddleware.php deleted file mode 100644 index 683a8e86..00000000 --- a/src/Middleware/CliParamsMiddleware.php +++ /dev/null @@ -1,71 +0,0 @@ -argv = $argv; - $this->currentSapi = $currentSapi; - } - - /** - * 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) - { - // When not in CLI, just call next middleware - if ($this->currentSapi !== 'cli') { - return $out($request, $response); - } - - /** @var RouteResult $routeResult */ - $routeResult = $request->getAttribute(RouteResult::class); - if (! $routeResult->isSuccess()) { - return $out($request, $response); - } - - // Inject ARGV params as request attributes - if ($routeResult->getMatchedRouteName() === 'cli-generate-shortcode') { - $request = $request->withAttribute('longUrl', isset($this->argv[2]) ? $this->argv[2] : null); - } - - return $out($request, $response); - } -} From 1fbefbbd15bf365416fe522b53ccdd5493d7b07d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Jun 2016 17:51:30 +0200 Subject: [PATCH 05/28] Created shortcode creation rest endpoint --- composer.json | 4 +- config/autoload/routes.global.php | 9 ++ .../Rest/CreateShortcodeMiddleware.php | 98 +++++++++++++++++++ src/Util/RestUtils.php | 26 +++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 src/Middleware/Rest/CreateShortcodeMiddleware.php create mode 100644 src/Util/RestUtils.php diff --git a/composer.json b/composer.json index 922e9067..15f92bee 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "php": "^5.5 || ^7.0", + "php": "^5.6 || ^7.0", "zendframework/zend-expressive": "^1.0", "zendframework/zend-expressive-helpers": "^2.0", "zendframework/zend-expressive-fastroute": "^1.1", @@ -24,7 +24,7 @@ "symfony/console": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8", + "phpunit/phpunit": "^5.0", "squizlabs/php_codesniffer": "^2.3", "roave/security-advisories": "dev-master", "filp/whoops": "^2.0", diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index 40a3d20b..7f16c641 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -1,5 +1,6 @@ Routable\RedirectMiddleware::class, 'allowed_methods' => ['GET'], ], + + // Rest + [ + 'name' => 'rest-create-shortcode', + 'path' => '/rest/short-code', + 'middleware' => Rest\CreateShortcodeMiddleware::class, + 'allowed_methods' => ['POST'], + ], ], ]; diff --git a/src/Middleware/Rest/CreateShortcodeMiddleware.php b/src/Middleware/Rest/CreateShortcodeMiddleware.php new file mode 100644 index 00000000..21ab4379 --- /dev/null +++ b/src/Middleware/Rest/CreateShortcodeMiddleware.php @@ -0,0 +1,98 @@ +urlShortener = $urlShortener; + $this->domainConfig = $domainConfig; + } + + /** + * 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) + { + $postData = $request->getParsedBody(); + if (! isset($postData['longUrl'])) { + return new JsonResponse([ + 'error' => RestUtils::INVALID_ARGUMENT_ERROR, + 'message' => 'A URL was not provided', + ], 400); + } + $longUrl = $postData['longUrl']; + + try { + $shortcode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); + $shortUrl = (new Uri())->withPath($shortcode) + ->withScheme($this->domainConfig['schema']) + ->withHost($this->domainConfig['hostname']); + + return new JsonResponse([ + 'longUrl' => $longUrl, + 'shortUrl' => $shortUrl->__toString(), + ]); + } catch (InvalidUrlException $e) { + return new JsonResponse([ + 'error' => RestUtils::getRestErrorCodeFromException($e), + 'message' => sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl), + ], 400); + } catch (\Exception $e) { + return new JsonResponse([ + 'error' => RestUtils::UNKNOWN_ERROR, + 'message' => sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl), + ], 500); + } + } +} diff --git a/src/Util/RestUtils.php b/src/Util/RestUtils.php new file mode 100644 index 00000000..d4c7179d --- /dev/null +++ b/src/Util/RestUtils.php @@ -0,0 +1,26 @@ + Date: Sun, 12 Jun 2016 21:31:28 +0200 Subject: [PATCH 06/28] Added get URL rest endpoint --- config/autoload/routes.global.php | 6 ++ config/autoload/services.global.php | 2 + .../Rest/CreateShortcodeMiddleware.php | 2 +- src/Middleware/Rest/ResolveUrlMiddleware.php | 85 +++++++++++++++++++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/Rest/ResolveUrlMiddleware.php diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index 7f16c641..60da6d2a 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -19,6 +19,12 @@ return [ 'middleware' => Rest\CreateShortcodeMiddleware::class, 'allowed_methods' => ['POST'], ], + [ + 'name' => 'rest-resolve-url', + 'path' => '/rest/short-code/{shortCode}', + 'middleware' => Rest\ResolveUrlMiddleware::class, + 'allowed_methods' => ['GET'], + ], ], ]; diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 617242b5..46890750 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -44,6 +44,8 @@ return [ // Middleware Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, + Middleware\Rest\CreateShortcodeMiddleware::class => AnnotatedFactory::class, + Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/src/Middleware/Rest/CreateShortcodeMiddleware.php b/src/Middleware/Rest/CreateShortcodeMiddleware.php index 21ab4379..1e723d48 100644 --- a/src/Middleware/Rest/CreateShortcodeMiddleware.php +++ b/src/Middleware/Rest/CreateShortcodeMiddleware.php @@ -91,7 +91,7 @@ class CreateShortcodeMiddleware implements MiddlewareInterface } catch (\Exception $e) { return new JsonResponse([ 'error' => RestUtils::UNKNOWN_ERROR, - 'message' => sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl), + 'message' => 'Unexpected error occured', ], 500); } } diff --git a/src/Middleware/Rest/ResolveUrlMiddleware.php b/src/Middleware/Rest/ResolveUrlMiddleware.php new file mode 100644 index 00000000..1beee164 --- /dev/null +++ b/src/Middleware/Rest/ResolveUrlMiddleware.php @@ -0,0 +1,85 @@ +urlShortener = $urlShortener; + } + + /** + * 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 { + $longUrl = $this->urlShortener->shortCodeToUrl($shortCode); + if (! isset($longUrl)) { + return new JsonResponse([ + 'error' => RestUtils::INVALID_ARGUMENT_ERROR, + 'message' => sprintf('No URL found for shortcode "%s"', $shortCode), + ], 400); + } + + return new JsonResponse([ + 'longUrl' => $longUrl, + ]); + } catch (InvalidShortCodeException $e) { + return new JsonResponse([ + 'error' => RestUtils::getRestErrorCodeFromException($e), + 'message' => sprintf('Provided short code "%s" has an invalid format', $shortCode), + ], 400); + } catch (\Exception $e) { + return new JsonResponse([ + 'error' => RestUtils::UNKNOWN_ERROR, + 'message' => 'Unexpected error occured', + ], 500); + } + } +} From 305df3a95ba0e6386cad17cfa39edc40dc644da8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Jun 2016 21:51:06 +0200 Subject: [PATCH 07/28] Implemented rest endpoint to return shortcode visits --- config/autoload/routes.global.php | 10 ++- config/autoload/services.global.php | 2 + src/Entity/Visit.php | 19 ++++- src/Exception/InvalidArgumentException.php | 6 ++ src/Middleware/Rest/GetVisitsMiddleware.php | 82 +++++++++++++++++++++ src/Service/VisitsTracker.php | 25 +++++++ src/Service/VisitsTrackerInterface.php | 10 +++ src/Util/RestUtils.php | 14 ++-- 8 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Middleware/Rest/GetVisitsMiddleware.php 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 @@ Date: Sat, 18 Jun 2016 09:43:29 +0200 Subject: [PATCH 08/28] Added list short URLs endpoint to rest api --- config/autoload/routes.global.php | 6 ++ config/autoload/services.global.php | 2 + src/Entity/ShortUrl.php | 19 ++++- .../Rest/ListShortcodesMiddleware.php | 74 +++++++++++++++++++ src/Service/ShortUrlService.php | 33 +++++++++ src/Service/ShortUrlServiceInterface.php | 12 +++ tests/Service/ShortUrlServiceTest.php | 45 +++++++++++ 7 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/Rest/ListShortcodesMiddleware.php create mode 100644 src/Service/ShortUrlService.php create mode 100644 src/Service/ShortUrlServiceInterface.php create mode 100644 tests/Service/ShortUrlServiceTest.php diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index 680a195d..b2976df5 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -25,6 +25,12 @@ return [ 'middleware' => Rest\ResolveUrlMiddleware::class, 'allowed_methods' => ['GET'], ], + [ + 'name' => 'rest-list-shortened-url', + 'path' => '/rest/short-codes', + 'middleware' => Rest\ListShortcodesMiddleware::class, + 'allowed_methods' => ['GET'], + ], [ 'name' => 'rest-get-visits', 'path' => '/rest/visits/{shortCode}', diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 8dada5e8..92a4adce 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -37,6 +37,7 @@ return [ GuzzleHttp\Client::class => InvokableFactory::class, Service\UrlShortener::class => AnnotatedFactory::class, Service\VisitsTracker::class => AnnotatedFactory::class, + Service\ShortUrlService::class => AnnotatedFactory::class, Cache::class => CacheFactory::class, // Cli commands @@ -47,6 +48,7 @@ return [ Middleware\Rest\CreateShortcodeMiddleware::class => AnnotatedFactory::class, Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class, Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class, + Middleware\Rest\ListShortcodesMiddleware::class => AnnotatedFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/src/Entity/ShortUrl.php b/src/Entity/ShortUrl.php index d1d5c4f7..fe96d651 100644 --- a/src/Entity/ShortUrl.php +++ b/src/Entity/ShortUrl.php @@ -13,7 +13,7 @@ use Doctrine\ORM\Mapping as ORM; * @ORM\Entity * @ORM\Table(name="short_urls") */ -class ShortUrl extends AbstractEntity +class ShortUrl extends AbstractEntity implements \JsonSerializable { /** * @var string @@ -117,4 +117,21 @@ class ShortUrl extends AbstractEntity $this->visits = $visits; 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 [ + 'shortCode' => $this->shortCode, + 'originalUrl' => $this->originalUrl, + 'dateCreated' => isset($this->dateCreated) ? $this->dateCreated->format(\DateTime::ISO8601) : null, + 'visitsCount' => count($this->visits), + ]; + } } diff --git a/src/Middleware/Rest/ListShortcodesMiddleware.php b/src/Middleware/Rest/ListShortcodesMiddleware.php new file mode 100644 index 00000000..d437b742 --- /dev/null +++ b/src/Middleware/Rest/ListShortcodesMiddleware.php @@ -0,0 +1,74 @@ +shortUrlService = $shortUrlService; + } + + /** + * 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) + { + try { + $shortUrls = $this->shortUrlService->listShortUrls(); + + return new JsonResponse([ + 'shortUrls' => [ + 'data' => $shortUrls, +// 'pagination' => [], + ] + ]); + } catch (\Exception $e) { + return new JsonResponse([ + 'error' => RestUtils::UNKNOWN_ERROR, + 'message' => 'Unexpected error occured', + ], 500); + } + } +} diff --git a/src/Service/ShortUrlService.php b/src/Service/ShortUrlService.php new file mode 100644 index 00000000..f6dc57fa --- /dev/null +++ b/src/Service/ShortUrlService.php @@ -0,0 +1,33 @@ +em = $em; + } + + /** + * @return ShortUrl[] + */ + public function listShortUrls() + { + return $this->em->getRepository(ShortUrl::class)->findAll(); + } +} diff --git a/src/Service/ShortUrlServiceInterface.php b/src/Service/ShortUrlServiceInterface.php new file mode 100644 index 00000000..5a943ba0 --- /dev/null +++ b/src/Service/ShortUrlServiceInterface.php @@ -0,0 +1,12 @@ +em = $this->prophesize(EntityManagerInterface::class); + $this->service = new ShortUrlService($this->em->reveal()); + } + + /** + * @test + */ + public function listedUrlsAreReturnedFromEntityManager() + { + $repo = $this->prophesize(EntityRepository::class); + $repo->findAll()->willReturn([ + new ShortUrl(), + new ShortUrl(), + new ShortUrl(), + new ShortUrl(), + ])->shouldBeCalledTimes(1); + $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); + + $list = $this->service->listShortUrls(); + $this->assertCount(4, $list); + } +} From 67ef171262197762cb6513c3ee4de60a2fa1f1e5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 3 Jul 2016 08:40:39 +0200 Subject: [PATCH 09/28] Improved middleware pipeline and added cross-domain headers for ajax requests --- .../autoload/middleware-pipeline.global.php | 15 ++++++ config/autoload/services.global.php | 1 + src/Middleware/CrossDomainMiddleware.php | 51 +++++++++++++++++++ src/Middleware/Rest/GetVisitsMiddleware.php | 2 +- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/CrossDomainMiddleware.php diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index 8a393ce1..ab903ac9 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -1,4 +1,5 @@ [ 'middleware' => [ ApplicationFactory::ROUTING_MIDDLEWARE, + ], + 'priority' => 10, + ], + + 'rest' => [ + 'path' => '/rest', + 'middleware' => [ + Middleware\CrossDomainMiddleware::class, + ], + 'priority' => 5, + ], + + 'post-routing' => [ + 'middleware' => [ Helper\UrlHelperMiddleware::class, ApplicationFactory::DISPATCH_MIDDLEWARE, ], diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 92a4adce..08fedf55 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -49,6 +49,7 @@ return [ Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class, Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class, Middleware\Rest\ListShortcodesMiddleware::class => AnnotatedFactory::class, + Middleware\CrossDomainMiddleware::class => InvokableFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/src/Middleware/CrossDomainMiddleware.php b/src/Middleware/CrossDomainMiddleware.php new file mode 100644 index 00000000..c762ed83 --- /dev/null +++ b/src/Middleware/CrossDomainMiddleware.php @@ -0,0 +1,51 @@ +hasHeader('X-Requested-With') + && strtolower($request->getHeaderLine('X-Requested-With')) === 'xmlhttprequest' + ) { + $response = $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') + ->withHeader('Access-Control-Max-Age', '1000') + ->withHeader('Access-Control-Allow-Origin', '*') + ->withHeader('Access-Control-Allow-Headers', '*'); + } + + return $response; + } +} diff --git a/src/Middleware/Rest/GetVisitsMiddleware.php b/src/Middleware/Rest/GetVisitsMiddleware.php index b932622d..1a1b973b 100644 --- a/src/Middleware/Rest/GetVisitsMiddleware.php +++ b/src/Middleware/Rest/GetVisitsMiddleware.php @@ -57,7 +57,7 @@ class GetVisitsMiddleware implements MiddlewareInterface public function __invoke(Request $request, Response $response, callable $out = null) { $shortCode = $request->getAttribute('shortCode'); - + try { $visits = $this->visitsTracker->info($shortCode); From 35f1a4b6722f076f7d60d4a30caf6890a0230da4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 4 Jul 2016 08:57:37 +0200 Subject: [PATCH 10/28] Created stuff to handle pagination on list results --- composer.json | 1 + src/Repository/PaginableRepository.php | 14 +++++++ src/Repository/ShortUrlRepository.php | 42 +++++++++++++++++++ .../ShortUrlRepositoryInterface.php | 8 ++++ src/Service/ShortUrlService.php | 3 +- src/Service/ShortUrlServiceInterface.php | 3 +- src/Service/VisitsTracker.php | 3 +- src/Service/VisitsTrackerInterface.php | 3 +- 8 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 src/Repository/PaginableRepository.php create mode 100644 src/Repository/ShortUrlRepository.php create mode 100644 src/Repository/ShortUrlRepositoryInterface.php diff --git a/composer.json b/composer.json index 15f92bee..374fed26 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "zendframework/zend-expressive-twigrenderer": "^1.0", "zendframework/zend-stdlib": "^2.7", "zendframework/zend-servicemanager": "^3.0", + "zendframework/zend-paginator": "^2.6", "doctrine/orm": "^2.5", "guzzlehttp/guzzle": "^6.2", "acelaya/zsm-annotated-services": "^0.2.0", diff --git a/src/Repository/PaginableRepository.php b/src/Repository/PaginableRepository.php new file mode 100644 index 00000000..ad993961 --- /dev/null +++ b/src/Repository/PaginableRepository.php @@ -0,0 +1,14 @@ +createQueryBuilder('s'); + + if (isset($limit)) { + $qb->setMaxResults($limit); + } + if (isset($offset)) { + $qb->setFirstResult($offset); + } + if (isset($searchTerm)) { + // TODO + } + if (isset($orderBy)) { + if (is_string($orderBy)) { + $qb->orderBy($orderBy); + } elseif (is_array($orderBy)) { + $key = key($orderBy); + $qb->orderBy($key, $orderBy[$key]); + } + } else { + $qb->orderBy('s.dateCreated'); + } + + return $qb->getQuery()->getResult(); + } +} diff --git a/src/Repository/ShortUrlRepositoryInterface.php b/src/Repository/ShortUrlRepositoryInterface.php new file mode 100644 index 00000000..37b013d9 --- /dev/null +++ b/src/Repository/ShortUrlRepositoryInterface.php @@ -0,0 +1,8 @@ + Date: Mon, 4 Jul 2016 09:15:50 +0200 Subject: [PATCH 11/28] Added pagination to ShortUrls list --- src/Entity/ShortUrl.php | 2 +- .../Rest/ListShortcodesMiddleware.php | 8 ++- .../Adapter/PaginableRepositoryAdapter.php | 56 +++++++++++++++++++ ...y.php => PaginableRepositoryInterface.php} | 12 +++- src/Repository/ShortUrlRepository.php | 20 +++++++ .../ShortUrlRepositoryInterface.php | 2 +- src/Service/ShortUrlService.php | 13 ++++- src/Service/ShortUrlServiceInterface.php | 5 +- 8 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 src/Paginator/Adapter/PaginableRepositoryAdapter.php rename src/Repository/{PaginableRepository.php => PaginableRepositoryInterface.php} (51%) diff --git a/src/Entity/ShortUrl.php b/src/Entity/ShortUrl.php index fe96d651..9f63af68 100644 --- a/src/Entity/ShortUrl.php +++ b/src/Entity/ShortUrl.php @@ -10,7 +10,7 @@ use Doctrine\ORM\Mapping as ORM; * @author * @link * - * @ORM\Entity + * @ORM\Entity(repositoryClass="Acelaya\UrlShortener\Repository\ShortUrlRepository") * @ORM\Table(name="short_urls") */ class ShortUrl extends AbstractEntity implements \JsonSerializable diff --git a/src/Middleware/Rest/ListShortcodesMiddleware.php b/src/Middleware/Rest/ListShortcodesMiddleware.php index d437b742..5f3b92a0 100644 --- a/src/Middleware/Rest/ListShortcodesMiddleware.php +++ b/src/Middleware/Rest/ListShortcodesMiddleware.php @@ -8,6 +8,7 @@ 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\Stdlib\ArrayUtils; use Zend\Stratigility\MiddlewareInterface; class ListShortcodesMiddleware implements MiddlewareInterface @@ -60,8 +61,11 @@ class ListShortcodesMiddleware implements MiddlewareInterface return new JsonResponse([ 'shortUrls' => [ - 'data' => $shortUrls, -// 'pagination' => [], + 'data' => ArrayUtils::iteratorToArray($shortUrls->getCurrentItems()), + 'pagination' => [ + 'currentPage' => $shortUrls->getCurrentPageNumber(), + 'pagesCount' => $shortUrls->count(), + ], ] ]); } catch (\Exception $e) { diff --git a/src/Paginator/Adapter/PaginableRepositoryAdapter.php b/src/Paginator/Adapter/PaginableRepositoryAdapter.php new file mode 100644 index 00000000..243e4a1b --- /dev/null +++ b/src/Paginator/Adapter/PaginableRepositoryAdapter.php @@ -0,0 +1,56 @@ +paginableRepository = $paginableRepository; + $this->searchTerm = $searchTerm; + $this->orderBy = $orderBy; + } + + /** + * Returns a collection of items for a page. + * + * @param int $offset Page offset + * @param int $itemCountPerPage Number of items per page + * @return array + */ + public function getItems($offset, $itemCountPerPage) + { + return $this->paginableRepository->findList($itemCountPerPage, $offset, $this->searchTerm, $this->orderBy); + } + + /** + * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *

+ *

+ * The return value is cast to an integer. + * @since 5.1.0 + */ + public function count() + { + return $this->paginableRepository->countList($this->searchTerm); + } +} diff --git a/src/Repository/PaginableRepository.php b/src/Repository/PaginableRepositoryInterface.php similarity index 51% rename from src/Repository/PaginableRepository.php rename to src/Repository/PaginableRepositoryInterface.php index ad993961..99c8696e 100644 --- a/src/Repository/PaginableRepository.php +++ b/src/Repository/PaginableRepositoryInterface.php @@ -1,9 +1,11 @@ getQuery()->getResult(); } + + /** + * Counts the number of elements in a list using provided filtering data + * + * @param null $searchTerm + * @return int + */ + public function countList($searchTerm = null) + { + $qb = $this->getEntityManager()->createQueryBuilder(); + $qb->select('COUNT(s)') + ->from(ShortUrl::class, 's'); + + if (isset($searchTerm)) { + // TODO + } + + return (int) $qb->getQuery()->getSingleScalarResult(); + } } diff --git a/src/Repository/ShortUrlRepositoryInterface.php b/src/Repository/ShortUrlRepositoryInterface.php index 37b013d9..be7f3fff 100644 --- a/src/Repository/ShortUrlRepositoryInterface.php +++ b/src/Repository/ShortUrlRepositoryInterface.php @@ -3,6 +3,6 @@ namespace Acelaya\UrlShortener\Repository; use Doctrine\Common\Persistence\ObjectRepository; -interface ShortUrlRepositoryInterface extends ObjectRepository, PaginableRepository +interface ShortUrlRepositoryInterface extends ObjectRepository, PaginableRepositoryInterface { } diff --git a/src/Service/ShortUrlService.php b/src/Service/ShortUrlService.php index 2ffc640c..9dbc176b 100644 --- a/src/Service/ShortUrlService.php +++ b/src/Service/ShortUrlService.php @@ -2,6 +2,8 @@ namespace Acelaya\UrlShortener\Service; use Acelaya\UrlShortener\Entity\ShortUrl; +use Acelaya\UrlShortener\Paginator\Adapter\PaginableRepositoryAdapter; +use Acelaya\UrlShortener\Repository\ShortUrlRepository; use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Doctrine\ORM\EntityManagerInterface; use Zend\Paginator\Paginator; @@ -25,10 +27,17 @@ class ShortUrlService implements ShortUrlServiceInterface } /** + * @param int $page * @return Paginator|ShortUrl[] */ - public function listShortUrls() + public function listShortUrls($page = 1) { - return $this->em->getRepository(ShortUrl::class)->findAll(); + /** @var ShortUrlRepository $repo */ + $repo = $this->em->getRepository(ShortUrl::class); + $paginator = new Paginator(new PaginableRepositoryAdapter($repo)); + $paginator->setItemCountPerPage(PaginableRepositoryAdapter::ITEMS_PER_PAGE) + ->setCurrentPageNumber($page); + + return $paginator; } } diff --git a/src/Service/ShortUrlServiceInterface.php b/src/Service/ShortUrlServiceInterface.php index 9f0a219c..a9d182d2 100644 --- a/src/Service/ShortUrlServiceInterface.php +++ b/src/Service/ShortUrlServiceInterface.php @@ -7,7 +7,8 @@ use Zend\Paginator\Paginator; interface ShortUrlServiceInterface { /** - * @return Paginator|ShortUrl[] + * @param int $page + * @return ShortUrl[]|Paginator */ - public function listShortUrls(); + public function listShortUrls($page = 1); } From 30773c66d05716a9c64c0a895158d9efd6f9f7ad Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 4 Jul 2016 09:18:10 +0200 Subject: [PATCH 12/28] Fixed ShortUrlServiceTest --- tests/Service/ShortUrlServiceTest.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/Service/ShortUrlServiceTest.php b/tests/Service/ShortUrlServiceTest.php index 6d325d17..3b77ee6a 100644 --- a/tests/Service/ShortUrlServiceTest.php +++ b/tests/Service/ShortUrlServiceTest.php @@ -2,10 +2,11 @@ namespace AcelayaTest\UrlShortener\Service; use Acelaya\UrlShortener\Entity\ShortUrl; +use Acelaya\UrlShortener\Repository\ShortUrlRepository; use Acelaya\UrlShortener\Service\ShortUrlService; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityRepository; use PHPUnit_Framework_TestCase as TestCase; +use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; class ShortUrlServiceTest extends TestCase @@ -30,16 +31,19 @@ class ShortUrlServiceTest extends TestCase */ public function listedUrlsAreReturnedFromEntityManager() { - $repo = $this->prophesize(EntityRepository::class); - $repo->findAll()->willReturn([ + $list = [ new ShortUrl(), new ShortUrl(), new ShortUrl(), new ShortUrl(), - ])->shouldBeCalledTimes(1); + ]; + + $repo = $this->prophesize(ShortUrlRepository::class); + $repo->findList(Argument::cetera())->willReturn($list)->shouldBeCalledTimes(1); + $repo->countList(Argument::cetera())->willReturn(count($list))->shouldBeCalledTimes(1); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); $list = $this->service->listShortUrls(); - $this->assertCount(4, $list); + $this->assertEquals(4, $list->getCurrentItemCount()); } } From b4e6fe7135b488a3e530e707855eb51bcdee5cce Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 4 Jul 2016 12:50:06 +0200 Subject: [PATCH 13/28] Created trait to serialize paginators --- .../Rest/ListShortcodesMiddleware.php | 17 ++++++----------- .../Util/PaginatorSerializerTrait.php | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 src/Paginator/Util/PaginatorSerializerTrait.php diff --git a/src/Middleware/Rest/ListShortcodesMiddleware.php b/src/Middleware/Rest/ListShortcodesMiddleware.php index 5f3b92a0..6a4a627f 100644 --- a/src/Middleware/Rest/ListShortcodesMiddleware.php +++ b/src/Middleware/Rest/ListShortcodesMiddleware.php @@ -1,6 +1,7 @@ shortUrlService->listShortUrls(); - - return new JsonResponse([ - 'shortUrls' => [ - 'data' => ArrayUtils::iteratorToArray($shortUrls->getCurrentItems()), - 'pagination' => [ - 'currentPage' => $shortUrls->getCurrentPageNumber(), - 'pagesCount' => $shortUrls->count(), - ], - ] - ]); + $query = $request->getQueryParams(); + $shortUrls = $this->shortUrlService->listShortUrls(isset($query['page']) ? $query['page'] : 1); + return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls)]); } catch (\Exception $e) { return new JsonResponse([ 'error' => RestUtils::UNKNOWN_ERROR, diff --git a/src/Paginator/Util/PaginatorSerializerTrait.php b/src/Paginator/Util/PaginatorSerializerTrait.php new file mode 100644 index 00000000..573832ca --- /dev/null +++ b/src/Paginator/Util/PaginatorSerializerTrait.php @@ -0,0 +1,19 @@ + ArrayUtils::iteratorToArray($paginator->getCurrentItems()), + 'pagination' => [ + 'currentPage' => $paginator->getCurrentPageNumber(), + 'pagesCount' => $paginator->count(), + ], + ]; + } +} From bbef3444c214eeb29964d97a7dd9ebef6f8d31bd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 4 Jul 2016 13:14:01 +0200 Subject: [PATCH 14/28] Added errorhanler local config distributable file --- config/autoload/errorhandler.local.php.dist | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 config/autoload/errorhandler.local.php.dist diff --git a/config/autoload/errorhandler.local.php.dist b/config/autoload/errorhandler.local.php.dist new file mode 100644 index 00000000..92a92497 --- /dev/null +++ b/config/autoload/errorhandler.local.php.dist @@ -0,0 +1,21 @@ + [ + 'invokables' => [ + 'Zend\Expressive\Whoops' => Whoops\Run::class, + 'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class, + ], + 'factories' => [ + 'Zend\Expressive\FinalHandler' => Zend\Expressive\Container\WhoopsErrorHandlerFactory::class, + ], + ], + + 'whoops' => [ + 'json_exceptions' => [ + 'display' => true, + 'show_trace' => true, + 'ajax_only' => true, + ], + ], +]; From 56b2bd3d56073ec3ba4f61d14e824cb21c9cc496 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 4 Jul 2016 14:04:10 +0200 Subject: [PATCH 15/28] Created entity to persist rest tokens --- .env.dist | 4 ++ config/autoload/rest.global.php | 9 ++++ src/Entity/RestToken.php | 89 +++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 config/autoload/rest.global.php create mode 100644 src/Entity/RestToken.php diff --git a/.env.dist b/.env.dist index a25e8c2c..d6271d57 100644 --- a/.env.dist +++ b/.env.dist @@ -8,3 +8,7 @@ SHORTCODE_CHARS= DB_USER= DB_PASSWORD= DB_NAME= + +# Rest authentication +REST_USER= +REST_PASSWORD= diff --git a/config/autoload/rest.global.php b/config/autoload/rest.global.php new file mode 100644 index 00000000..6e3fc216 --- /dev/null +++ b/config/autoload/rest.global.php @@ -0,0 +1,9 @@ + [ + 'username' => getenv('REST_USER'), + 'password' => getenv('REST_PASSWORD'), + ], + +]; diff --git a/src/Entity/RestToken.php b/src/Entity/RestToken.php new file mode 100644 index 00000000..d23dc10f --- /dev/null +++ b/src/Entity/RestToken.php @@ -0,0 +1,89 @@ +updateExpiration(); + } + + /** + * @return \DateTime + */ + public function getExpirationDate() + { + return $this->expirationDate; + } + + /** + * @param \DateTime $expirationDate + * @return $this + */ + public function setExpirationDate($expirationDate) + { + $this->expirationDate = $expirationDate; + return $this; + } + + /** + * @return string + */ + public function getToken() + { + return $this->token; + } + + /** + * @param string $token + * @return $this + */ + public function setToken($token) + { + $this->token = $token; + return $this; + } + + /** + * @return bool + */ + public function isExpired() + { + return new \DateTime() > $this->expirationDate; + } + + /** + * Updates the expiration of the token, setting it to the default interval in the future + * @return $this + */ + public function updateExpiration() + { + return $this->setExpirationDate((new \DateTime())->add(new \DateInterval(self::DEFAULT_INTERVAL))); + } +} From dfc5bfd0f2b9576aa88aefdedb943d2296ad96de Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 4 Jul 2016 14:45:18 +0200 Subject: [PATCH 16/28] Created rest route to perform authentication --- config/autoload/routes.global.php | 6 ++ config/autoload/services.global.php | 2 + src/Entity/RestToken.php | 13 +++ src/Exception/AuthenticationException.php | 10 +++ .../Rest/AuthenticateMiddleware.php | 77 ++++++++++++++++ src/Service/RestTokenService.php | 87 +++++++++++++++++++ src/Service/RestTokenServiceInterface.php | 25 ++++++ src/Util/RestUtils.php | 3 + src/Util/StringUtilsTrait.php | 40 +++++++++ 9 files changed, 263 insertions(+) create mode 100644 src/Exception/AuthenticationException.php create mode 100644 src/Middleware/Rest/AuthenticateMiddleware.php create mode 100644 src/Service/RestTokenService.php create mode 100644 src/Service/RestTokenServiceInterface.php create mode 100644 src/Util/StringUtilsTrait.php diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index b2976df5..7ffdbc74 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -13,6 +13,12 @@ return [ ], // Rest + [ + 'name' => 'rest-authenticate', + 'path' => '/rest/authenticate', + 'middleware' => Rest\AuthenticateMiddleware::class, + 'allowed_methods' => ['POST'], + ], [ 'name' => 'rest-create-shortcode', 'path' => '/rest/short-codes', diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 08fedf55..d5de3d4a 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -38,6 +38,7 @@ return [ Service\UrlShortener::class => AnnotatedFactory::class, Service\VisitsTracker::class => AnnotatedFactory::class, Service\ShortUrlService::class => AnnotatedFactory::class, + Service\RestTokenService::class => AnnotatedFactory::class, Cache::class => CacheFactory::class, // Cli commands @@ -45,6 +46,7 @@ return [ // Middleware Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, + Middleware\Rest\AuthenticateMiddleware::class => AnnotatedFactory::class, Middleware\Rest\CreateShortcodeMiddleware::class => AnnotatedFactory::class, Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class, Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class, diff --git a/src/Entity/RestToken.php b/src/Entity/RestToken.php index d23dc10f..90a70f0e 100644 --- a/src/Entity/RestToken.php +++ b/src/Entity/RestToken.php @@ -1,6 +1,7 @@ updateExpiration(); + $this->setRandomTokenKey(); } /** @@ -86,4 +90,13 @@ class RestToken extends AbstractEntity { return $this->setExpirationDate((new \DateTime())->add(new \DateInterval(self::DEFAULT_INTERVAL))); } + + /** + * Sets a random unique token key for this RestToken + * @return RestToken + */ + public function setRandomTokenKey() + { + return $this->setToken($this->generateV4Uuid()); + } } diff --git a/src/Exception/AuthenticationException.php b/src/Exception/AuthenticationException.php new file mode 100644 index 00000000..0876be75 --- /dev/null +++ b/src/Exception/AuthenticationException.php @@ -0,0 +1,10 @@ + "%s". Password -> "%s"', $username, $password)); + } +} diff --git a/src/Middleware/Rest/AuthenticateMiddleware.php b/src/Middleware/Rest/AuthenticateMiddleware.php new file mode 100644 index 00000000..0189b249 --- /dev/null +++ b/src/Middleware/Rest/AuthenticateMiddleware.php @@ -0,0 +1,77 @@ +restTokenService = $restTokenService; + } + + /** + * 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) + { + $authData = $request->getParsedBody(); + if (! isset($authData['username'], $authData['password'])) { + return new JsonResponse([ + 'error' => RestUtils::INVALID_ARGUMENT_ERROR, + 'message' => 'You have to provide both "username" and "password"' + ], 400); + } + + try { + $token = $this->restTokenService->createToken($authData['username'], $authData['password']); + return new JsonResponse(['token' => $token->getToken()]); + } catch (AuthenticationException $e) { + return new JsonResponse([ + 'error' => RestUtils::getRestErrorCodeFromException($e), + 'message' => 'Invalid username and/or password', + ], 401); + } + } +} diff --git a/src/Service/RestTokenService.php b/src/Service/RestTokenService.php new file mode 100644 index 00000000..aa9ea0b8 --- /dev/null +++ b/src/Service/RestTokenService.php @@ -0,0 +1,87 @@ +em = $em; + $this->restConfig = $restConfig; + } + + /** + * @param string $token + * @return RestToken + * @throws InvalidArgumentException + */ + public function getByToken($token) + { + $restToken = $this->em->getRepository(RestToken::class)->findOneBy([ + 'token' => $token, + ]); + if (! isset($restToken)) { + throw new InvalidArgumentException(sprintf('RestToken not found for token "%s"', $token)); + } + + return $restToken; + } + + /** + * Creates and returns a new RestToken if username and password are correct + * @param $username + * @param $password + * @return RestToken + * @throws AuthenticationException + */ + public function createToken($username, $password) + { + $this->processCredentials($username, $password); + + $restToken = new RestToken(); + $this->em->persist($restToken); + $this->em->flush(); + + return $restToken; + } + + /** + * @param string $username + * @param string $password + */ + protected function processCredentials($username, $password) + { + $configUsername = strtolower(trim($this->restConfig['username'])); + $providedUsername = strtolower(trim($username)); + $configPassword = trim($this->restConfig['password']); + $providedPassword = trim($password); + + if ($configUsername === $providedUsername && $configPassword === $providedPassword) { + return; + } + + // If credentials are not correct, throw exception + throw AuthenticationException::fromCredentials($providedUsername, $providedPassword); + } +} diff --git a/src/Service/RestTokenServiceInterface.php b/src/Service/RestTokenServiceInterface.php new file mode 100644 index 00000000..fb45483d --- /dev/null +++ b/src/Service/RestTokenServiceInterface.php @@ -0,0 +1,25 @@ + Date: Mon, 4 Jul 2016 17:54:24 +0200 Subject: [PATCH 17/28] Created middleware that checks authentication --- .../autoload/middleware-pipeline.global.php | 1 + config/autoload/services.global.php | 1 + .../CheckAuthenticationMiddleware.php | 100 ++++++++++++++++++ src/Service/RestTokenService.php | 11 ++ src/Service/RestTokenServiceInterface.php | 7 ++ src/Util/RestUtils.php | 5 +- 6 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/Middleware/CheckAuthenticationMiddleware.php diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index ab903ac9..fc6f85f0 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -23,6 +23,7 @@ return [ 'rest' => [ 'path' => '/rest', 'middleware' => [ + Middleware\CheckAuthenticationMiddleware::class, Middleware\CrossDomainMiddleware::class, ], 'priority' => 5, diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index d5de3d4a..b08229e7 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -52,6 +52,7 @@ return [ Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class, Middleware\Rest\ListShortcodesMiddleware::class => AnnotatedFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, + Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/src/Middleware/CheckAuthenticationMiddleware.php b/src/Middleware/CheckAuthenticationMiddleware.php new file mode 100644 index 00000000..92081ad8 --- /dev/null +++ b/src/Middleware/CheckAuthenticationMiddleware.php @@ -0,0 +1,100 @@ +restTokenService = $restTokenService; + } + + /** + * 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) + { + // If current route is the authenticate route, continue to the next middleware + /** @var RouteResult $routeResult */ + $routeResult = $request->getAttribute(RouteResult::class); + if (isset($routeResult) && $routeResult->getMatchedRouteName() === 'rest-authenticate') { + return $out($request, $response); + } + + // Check that the auth header was provided, and that it belongs to a non-expired token + if (! $request->hasHeader(self::AUTH_TOKEN_HEADER)) { + return $this->createTokenErrorResponse(); + } + + $authToken = $request->getHeaderLine(self::AUTH_TOKEN_HEADER); + try { + $restToken = $this->restTokenService->getByToken($authToken); + if ($restToken->isExpired()) { + return $this->createTokenErrorResponse(); + } + + // Update the token expiration and continue to next middleware + $this->restTokenService->updateExpiration($restToken); + return $out($request, $response); + } catch (InvalidArgumentException $e) { + return $this->createTokenErrorResponse(); + } + } + + protected function createTokenErrorResponse() + { + return new JsonResponse([ + 'error' => RestUtils::INVALID_AUTH_TOKEN_ERROR, + 'message' => sprintf( + 'Missing or invalid auth token provided. Perform a new authentication request and send provided token ' + . 'on every new request on the "%s" header', + self::AUTH_TOKEN_HEADER + ), + ], 401); + } +} diff --git a/src/Service/RestTokenService.php b/src/Service/RestTokenService.php index aa9ea0b8..26d7f34c 100644 --- a/src/Service/RestTokenService.php +++ b/src/Service/RestTokenService.php @@ -84,4 +84,15 @@ class RestTokenService implements RestTokenServiceInterface // If credentials are not correct, throw exception throw AuthenticationException::fromCredentials($providedUsername, $providedPassword); } + + /** + * Updates the expiration of provided token, extending its life + * + * @param RestToken $token + */ + public function updateExpiration(RestToken $token) + { + $token->updateExpiration(); + $this->em->flush(); + } } diff --git a/src/Service/RestTokenServiceInterface.php b/src/Service/RestTokenServiceInterface.php index fb45483d..0cdec822 100644 --- a/src/Service/RestTokenServiceInterface.php +++ b/src/Service/RestTokenServiceInterface.php @@ -22,4 +22,11 @@ interface RestTokenServiceInterface * @throws AuthenticationException */ public function createToken($username, $password); + + /** + * Updates the expiration of provided token, extending its life + * + * @param RestToken $token + */ + public function updateExpiration(RestToken $token); } diff --git a/src/Util/RestUtils.php b/src/Util/RestUtils.php index 94ab47ec..f0c37a00 100644 --- a/src/Util/RestUtils.php +++ b/src/Util/RestUtils.php @@ -8,7 +8,8 @@ class RestUtils const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE'; const INVALID_URL_ERROR = 'INVALID_URL'; const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT'; - const INVALID_CREDENTIALS = 'INVALID_CREDENTIALS'; + const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; + const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN_ERROR'; const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; public static function getRestErrorCodeFromException(Exception\ExceptionInterface $e) @@ -21,7 +22,7 @@ class RestUtils case $e instanceof Exception\InvalidArgumentException: return self::INVALID_ARGUMENT_ERROR; case $e instanceof Exception\AuthenticationException: - return self::INVALID_CREDENTIALS; + return self::INVALID_CREDENTIALS_ERROR; default: return self::UNKNOWN_ERROR; } From bd36c65a7347fbea387f2a6c65c4dd59dc76c34b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 5 Jul 2016 19:08:34 +0200 Subject: [PATCH 18/28] Fixed some cross-origin issues --- config/autoload/routes.global.php | 2 +- src/Middleware/CrossDomainMiddleware.php | 13 +++++++------ src/Middleware/Rest/AuthenticateMiddleware.php | 4 ++++ src/Middleware/Rest/CreateShortcodeMiddleware.php | 5 +++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index 7ffdbc74..06e5f733 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -17,7 +17,7 @@ return [ 'name' => 'rest-authenticate', 'path' => '/rest/authenticate', 'middleware' => Rest\AuthenticateMiddleware::class, - 'allowed_methods' => ['POST'], + 'allowed_methods' => ['POST', 'OPTIONS'], ], [ 'name' => 'rest-create-shortcode', diff --git a/src/Middleware/CrossDomainMiddleware.php b/src/Middleware/CrossDomainMiddleware.php index c762ed83..c76d4d73 100644 --- a/src/Middleware/CrossDomainMiddleware.php +++ b/src/Middleware/CrossDomainMiddleware.php @@ -37,15 +37,16 @@ class CrossDomainMiddleware implements MiddlewareInterface /** @var Response $response */ $response = $out($request, $response); - if ($request->hasHeader('X-Requested-With') - && strtolower($request->getHeaderLine('X-Requested-With')) === 'xmlhttprequest' - ) { + if (strtolower($request->getMethod()) === 'options') { $response = $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') ->withHeader('Access-Control-Max-Age', '1000') - ->withHeader('Access-Control-Allow-Origin', '*') - ->withHeader('Access-Control-Allow-Headers', '*'); + ->withHeader( + // Allow all requested headers + 'Access-Control-Allow-Headers', + $request->getHeaderLine('Access-Control-Request-Headers') + ); } - return $response; + return $response->withHeader('Access-Control-Allow-Origin', '*'); } } diff --git a/src/Middleware/Rest/AuthenticateMiddleware.php b/src/Middleware/Rest/AuthenticateMiddleware.php index 0189b249..85d12330 100644 --- a/src/Middleware/Rest/AuthenticateMiddleware.php +++ b/src/Middleware/Rest/AuthenticateMiddleware.php @@ -56,6 +56,10 @@ class AuthenticateMiddleware implements MiddlewareInterface */ public function __invoke(Request $request, Response $response, callable $out = null) { + if (strtolower($request->getMethod()) === 'options') { + return $response; + } + $authData = $request->getParsedBody(); if (! isset($authData['username'], $authData['password'])) { return new JsonResponse([ diff --git a/src/Middleware/Rest/CreateShortcodeMiddleware.php b/src/Middleware/Rest/CreateShortcodeMiddleware.php index 1e723d48..b68c551c 100644 --- a/src/Middleware/Rest/CreateShortcodeMiddleware.php +++ b/src/Middleware/Rest/CreateShortcodeMiddleware.php @@ -74,14 +74,15 @@ class CreateShortcodeMiddleware implements MiddlewareInterface $longUrl = $postData['longUrl']; try { - $shortcode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); - $shortUrl = (new Uri())->withPath($shortcode) + $shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); + $shortUrl = (new Uri())->withPath($shortCode) ->withScheme($this->domainConfig['schema']) ->withHost($this->domainConfig['hostname']); return new JsonResponse([ 'longUrl' => $longUrl, 'shortUrl' => $shortUrl->__toString(), + 'shortCode' => $shortCode, ]); } catch (InvalidUrlException $e) { return new JsonResponse([ From baf5936cf18d778e2e5a9d7b64c52836d5f700bd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 5 Jul 2016 19:19:23 +0200 Subject: [PATCH 19/28] More cross-domain improvements --- config/autoload/routes.global.php | 6 +-- .../CheckAuthenticationMiddleware.php | 6 ++- .../Rest/AbstractRestMiddleware.php | 51 +++++++++++++++++++ .../Rest/AuthenticateMiddleware.php | 30 ++--------- .../Rest/CreateShortcodeMiddleware.php | 26 ++-------- src/Middleware/Rest/GetVisitsMiddleware.php | 26 ++-------- .../Rest/ListShortcodesMiddleware.php | 26 ++-------- src/Middleware/Rest/ResolveUrlMiddleware.php | 26 ++-------- 8 files changed, 73 insertions(+), 124 deletions(-) create mode 100644 src/Middleware/Rest/AbstractRestMiddleware.php diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index 06e5f733..87133f09 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -23,13 +23,13 @@ return [ 'name' => 'rest-create-shortcode', 'path' => '/rest/short-codes', 'middleware' => Rest\CreateShortcodeMiddleware::class, - 'allowed_methods' => ['POST'], + 'allowed_methods' => ['POST', 'OPTIONS'], ], [ 'name' => 'rest-resolve-url', 'path' => '/rest/short-codes/{shortCode}', 'middleware' => Rest\ResolveUrlMiddleware::class, - 'allowed_methods' => ['GET'], + 'allowed_methods' => ['GET', 'OPTIONS'], ], [ 'name' => 'rest-list-shortened-url', @@ -41,7 +41,7 @@ return [ 'name' => 'rest-get-visits', 'path' => '/rest/visits/{shortCode}', 'middleware' => Rest\GetVisitsMiddleware::class, - 'allowed_methods' => ['GET'], + 'allowed_methods' => ['GET', 'OPTIONS'], ], ], diff --git a/src/Middleware/CheckAuthenticationMiddleware.php b/src/Middleware/CheckAuthenticationMiddleware.php index 92081ad8..8f7327e7 100644 --- a/src/Middleware/CheckAuthenticationMiddleware.php +++ b/src/Middleware/CheckAuthenticationMiddleware.php @@ -59,10 +59,12 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface */ public function __invoke(Request $request, Response $response, callable $out = null) { - // If current route is the authenticate route, continue to the next middleware + // If current route is the authenticate route or an OPTIONS request, continue to the next middleware /** @var RouteResult $routeResult */ $routeResult = $request->getAttribute(RouteResult::class); - if (isset($routeResult) && $routeResult->getMatchedRouteName() === 'rest-authenticate') { + if ((isset($routeResult) && $routeResult->getMatchedRouteName() === 'rest-authenticate') + || strtolower($request->getMethod()) === 'options' + ) { return $out($request, $response); } diff --git a/src/Middleware/Rest/AbstractRestMiddleware.php b/src/Middleware/Rest/AbstractRestMiddleware.php new file mode 100644 index 00000000..1168ff60 --- /dev/null +++ b/src/Middleware/Rest/AbstractRestMiddleware.php @@ -0,0 +1,51 @@ +getMethod()) === 'options') { + return $response; + } + + return $this->dispatch($request, $response, $out); + } + + /** + * @param Request $request + * @param Response $response + * @param callable|null $out + * @return null|Response + */ + abstract protected function dispatch(Request $request, Response $response, callable $out = null); +} diff --git a/src/Middleware/Rest/AuthenticateMiddleware.php b/src/Middleware/Rest/AuthenticateMiddleware.php index 85d12330..88c9df60 100644 --- a/src/Middleware/Rest/AuthenticateMiddleware.php +++ b/src/Middleware/Rest/AuthenticateMiddleware.php @@ -9,9 +9,8 @@ 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 AuthenticateMiddleware implements MiddlewareInterface +class AuthenticateMiddleware extends AbstractRestMiddleware { /** * @var RestTokenServiceInterface @@ -30,36 +29,13 @@ class AuthenticateMiddleware implements MiddlewareInterface } /** - * 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 + * @param callable|null $out * @return null|Response */ - public function __invoke(Request $request, Response $response, callable $out = null) + public function dispatch(Request $request, Response $response, callable $out = null) { - if (strtolower($request->getMethod()) === 'options') { - return $response; - } - $authData = $request->getParsedBody(); if (! isset($authData['username'], $authData['password'])) { return new JsonResponse([ diff --git a/src/Middleware/Rest/CreateShortcodeMiddleware.php b/src/Middleware/Rest/CreateShortcodeMiddleware.php index b68c551c..f5ee3228 100644 --- a/src/Middleware/Rest/CreateShortcodeMiddleware.php +++ b/src/Middleware/Rest/CreateShortcodeMiddleware.php @@ -10,9 +10,8 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Uri; -use Zend\Stratigility\MiddlewareInterface; -class CreateShortcodeMiddleware implements MiddlewareInterface +class CreateShortcodeMiddleware extends AbstractRestMiddleware { /** * @var UrlShortener|UrlShortenerInterface @@ -38,31 +37,12 @@ class CreateShortcodeMiddleware implements MiddlewareInterface } /** - * 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 + * @param callable|null $out * @return null|Response */ - public function __invoke(Request $request, Response $response, callable $out = null) + public function dispatch(Request $request, Response $response, callable $out = null) { $postData = $request->getParsedBody(); if (! isset($postData['longUrl'])) { diff --git a/src/Middleware/Rest/GetVisitsMiddleware.php b/src/Middleware/Rest/GetVisitsMiddleware.php index 1a1b973b..a6ca954e 100644 --- a/src/Middleware/Rest/GetVisitsMiddleware.php +++ b/src/Middleware/Rest/GetVisitsMiddleware.php @@ -9,9 +9,8 @@ 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 +class GetVisitsMiddleware extends AbstractRestMiddleware { /** * @var VisitsTrackerInterface @@ -30,31 +29,12 @@ class GetVisitsMiddleware implements MiddlewareInterface } /** - * 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 + * @param callable|null $out * @return null|Response */ - public function __invoke(Request $request, Response $response, callable $out = null) + public function dispatch(Request $request, Response $response, callable $out = null) { $shortCode = $request->getAttribute('shortCode'); diff --git a/src/Middleware/Rest/ListShortcodesMiddleware.php b/src/Middleware/Rest/ListShortcodesMiddleware.php index 6a4a627f..6b74241c 100644 --- a/src/Middleware/Rest/ListShortcodesMiddleware.php +++ b/src/Middleware/Rest/ListShortcodesMiddleware.php @@ -10,9 +10,8 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Zend\Diactoros\Response\JsonResponse; use Zend\Stdlib\ArrayUtils; -use Zend\Stratigility\MiddlewareInterface; -class ListShortcodesMiddleware implements MiddlewareInterface +class ListShortcodesMiddleware extends AbstractRestMiddleware { use PaginatorSerializerTrait; @@ -33,31 +32,12 @@ class ListShortcodesMiddleware implements MiddlewareInterface } /** - * 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 + * @param callable|null $out * @return null|Response */ - public function __invoke(Request $request, Response $response, callable $out = null) + public function dispatch(Request $request, Response $response, callable $out = null) { try { $query = $request->getQueryParams(); diff --git a/src/Middleware/Rest/ResolveUrlMiddleware.php b/src/Middleware/Rest/ResolveUrlMiddleware.php index 1beee164..4529e973 100644 --- a/src/Middleware/Rest/ResolveUrlMiddleware.php +++ b/src/Middleware/Rest/ResolveUrlMiddleware.php @@ -9,9 +9,8 @@ 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 ResolveUrlMiddleware implements MiddlewareInterface +class ResolveUrlMiddleware extends AbstractRestMiddleware { /** * @var UrlShortenerInterface @@ -30,31 +29,12 @@ class ResolveUrlMiddleware implements MiddlewareInterface } /** - * 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 + * @param callable|null $out * @return null|Response */ - public function __invoke(Request $request, Response $response, callable $out = null) + public function dispatch(Request $request, Response $response, callable $out = null) { $shortCode = $request->getAttribute('shortCode'); From f691bb00d13d965046dbecc323a1f68f3a592ae0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 5 Jul 2016 19:28:47 +0200 Subject: [PATCH 20/28] Created rest documentation --- data/docs/rest.md | 278 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 data/docs/rest.md diff --git a/data/docs/rest.md b/data/docs/rest.md new file mode 100644 index 00000000..f93f6c48 --- /dev/null +++ b/data/docs/rest.md @@ -0,0 +1,278 @@ + +# REST API documentation + +## Error management + +Statuses: + +* 400 -> controlled error +* 401 -> authentication error +* 500 -> unexpected error + +[TODO] + +## Authentication + +[TODO] + +## Endpoints + +#### Authenticate + +**REQUEST** + +* `POST` -> `/rest/authenticate` +* Params: + * username: `string` + * password: `string` + +**SUCCESS RESPONSE** + +```json +{ + "token": "9f741eb0-33d7-4c56-b8f7-3719e9929946" +} +``` + +**ERROR RESPONSE** + +```json +{ + "error": "INVALID_ARGUMENT", + "message": "You have to provide both \"username\" and \"password\"" +} +``` + +Posible errors: + +* **INVALID_ARGUMENT**: Username or password were not provided. +* **INVALID_CREDENTIALS**: Username or password are incorrect. + + +#### Create shortcode + +**REQUEST** + +* `POST` -> `/rest/short-codes` +* Params: + * longUrl: `string` -> The URL to shorten +* Headers: + * X-Auth-Token: `string` -> The token provided in the authentication request + +**SUCCESS RESPONSE** + +```json +{ + "longUrl": "https://www.facebook.com/something/something", + "shortUrl": "https://doma.in/rY9Kr", + "shortCode": "rY9Kr" +} +``` + +**ERROR RESPONSE** + +```json +{ + "error": "INVALID_URL", + "message": "Provided URL \"wfwef\" is invalid. Try with a different one." +} +``` + +Posible errors: + +* **INVALID_ARGUMENT**: The longUrl was not provided. +* **INVALID_URL**: Provided longUrl has an invalid format or does not resolve. +* **UNKNOWN_ERROR**: Something unexpected happened. + + +#### Resolve URL + +**REQUEST** + +* `GET` -> `/rest/short-codes/{shortCode}` +* Route params: + * shortCode: `string` -> The short code we want to resolve +* Headers: + * X-Auth-Token: `string` -> The token provided in the authentication request + +**SUCCESS RESPONSE** + +```json +{ + "longUrl": "https://www.facebook.com/something/something" +} +``` + +**ERROR RESPONSE** + +```json +{ + "error": "INVALID_SHORTCODE", + "message": "Provided short code \"abc123\" has an invalid format" +} +``` + +Posible errors: + +* **INVALID_ARGUMENT**: No longUrl was found for provided shortCode. +* **INVALID_SHORTCODE**: Provided shortCode does not match the character set used by the app to generate short codes. +* **UNKNOWN_ERROR**: Something unexpected happened. + + +#### List shortened URLs + +**REQUEST** + +* `GET` -> `/rest/short-codes` +* Query params: + * page: `integer` -> The page to list. Defaults to 1 if not provided. +* Headers: + * X-Auth-Token: `string` -> The token provided in the authentication request + +**SUCCESS RESPONSE** + +```json +{ + "shortUrls": { + "data": [ + { + "shortCode": "abc123", + "originalUrl": "http://www.alejandrocelaya.com", + "dateCreated": "2016-04-30T18:01:47+0200", + "visitsCount": 4 + }, + { + "shortCode": "def456", + "originalUrl": "http://www.alejandrocelaya.com/en", + "dateCreated": "2016-04-30T18:03:43+0200", + "visitsCount": 0 + }, + { + "shortCode": "ghi789", + "originalUrl": "http://www.alejandrocelaya.com/es", + "dateCreated": "2016-04-30T18:10:38+0200", + "visitsCount": 0 + }, + { + "shortCode": "jkl987", + "originalUrl": "http://www.alejandrocelaya.com/es/", + "dateCreated": "2016-04-30T18:10:57+0200", + "visitsCount": 0 + }, + { + "shortCode": "mno654", + "originalUrl": "http://blog.alejandrocelaya.com/2016/04/09/improving-zend-service-manager-workflow-with-annotations/", + "dateCreated": "2016-04-30T19:21:05+0200", + "visitsCount": 1 + }, + { + "shortCode": "pqr321", + "originalUrl": "http://www.google.com", + "dateCreated": "2016-05-01T11:19:53+0200", + "visitsCount": 0 + }, + { + "shortCode": "stv159", + "originalUrl": "http://www.acelaya.com", + "dateCreated": "2016-06-12T17:49:21+0200", + "visitsCount": 0 + }, + { + "shortCode": "wxy753", + "originalUrl": "http://www.atomic-reader.com", + "dateCreated": "2016-06-12T17:50:27+0200", + "visitsCount": 0 + }, + { + "shortCode": "zab852", + "originalUrl": "http://foo.com", + "dateCreated": "2016-07-03T09:07:36+0200", + "visitsCount": 0 + }, + { + "shortCode": "cde963", + "originalUrl": "https://www.facebook.com.com", + "dateCreated": "2016-07-03T09:12:35+0200", + "visitsCount": 0 + } + ], + "pagination": { + "currentPage": 4, + "pagesCount": 15 + } + } +} +``` + +**ERROR RESPONSE** + +```json +{ + "error": "UNKNOWN_ERROR", + "message": "Unexpected error occured" +} +``` + +Posible errors: + +* **UNKNOWN_ERROR**: Something unexpected happened. + + +#### Get visits + +**REQUEST** + +* `GET` -> `/rest/visits/{shortCode}` +* Route params: + * shortCode: `string` -> The shortCode from which we eant to get the visits. +* Headers: + * X-Auth-Token: `string` -> The token provided in the authentication request + +**SUCCESS RESPONSE** + +```json +{ + "shortUrls": { + "data": [ + { + "referer": null, + "date": "2016-06-18T09:32:22+0200", + "remoteAddr": "127.0.0.1", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36" + }, + { + "referer": null, + "date": "2016-04-30T19:20:06+0200", + "remoteAddr": "127.0.0.1", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36" + }, + { + "referer": "google.com", + "date": "2016-04-30T19:19:57+0200", + "remoteAddr": "1.2.3.4", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36" + }, + { + "referer": null, + "date": "2016-04-30T19:17:35+0200", + "remoteAddr": "127.0.0.1", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36" + } + ], + } +} +``` + +**ERROR RESPONSE** + +```json +{ + "error": "INVALID_ARGUMENT", + "message": "Provided short code \"abc123\" is invalid" +} +``` + +Posible errors: + +* **INVALID_ARGUMENT**: The shortcode does not belong to any short URL +* **UNKNOWN_ERROR**: Something unexpected happened. From 371e264ebeac10ab9c817d857362697cc72b9bde Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 5 Jul 2016 19:54:16 +0200 Subject: [PATCH 21/28] Removed PHP5.5 from travis environments --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 77f1332b..eb2ead28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ branches: - develop php: - - 5.5 - 5.6 - 7 - hhvm From 490b72539eabbadafaa7f88c85b64e57bb060974 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 5 Jul 2016 20:09:10 +0200 Subject: [PATCH 22/28] Created CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b6f92d16 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +## CHANGELOG + +### 0.2.0 + +**Enhancements:** + +* [9: Use symfony/console to dispatch console requests, instead of trying to integrate the process with expressive](https://github.com/acelaya/url-shortener/issues/9) +* [8: Create a REST API](https://github.com/acelaya/url-shortener/issues/8) + +**Tasks** + +* [5: Create CHANGELOG file](https://github.com/acelaya/url-shortener/issues/5) From 9ce5e255f1122250e07506936c6a22fb599ed13f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 5 Jul 2016 23:16:23 +0200 Subject: [PATCH 23/28] Created new CLI command to parse a shortcode --- bin/cli | 2 + config/autoload/services.global.php | 1 + src/CliCommands/GenerateShortcodeCommand.php | 2 +- src/CliCommands/ResolveUrlCommand.php | 80 ++++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/CliCommands/ResolveUrlCommand.php diff --git a/bin/cli b/bin/cli index 1816b80f..8a2983c1 100755 --- a/bin/cli +++ b/bin/cli @@ -1,6 +1,7 @@ #!/usr/bin/env php addCommands([ $container->get(GenerateShortcodeCommand::class), + $container->get(ResolveUrlCommand::class), ]); $app->run(); diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index b08229e7..b3103ac3 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -43,6 +43,7 @@ return [ // Cli commands CliCommands\GenerateShortcodeCommand::class => AnnotatedFactory::class, + CliCommands\ResolveUrlCommand::class => AnnotatedFactory::class, // Middleware Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, diff --git a/src/CliCommands/GenerateShortcodeCommand.php b/src/CliCommands/GenerateShortcodeCommand.php index 9f545dce..68b076c3 100644 --- a/src/CliCommands/GenerateShortcodeCommand.php +++ b/src/CliCommands/GenerateShortcodeCommand.php @@ -41,7 +41,7 @@ class GenerateShortcodeCommand extends Command public function configure() { - $this->setName('generate-shortcode') + $this->setName('shortcode:generate') ->setDescription('Generates a shortcode for provided URL and returns the short URL') ->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse'); } diff --git a/src/CliCommands/ResolveUrlCommand.php b/src/CliCommands/ResolveUrlCommand.php new file mode 100644 index 00000000..e10cd73e --- /dev/null +++ b/src/CliCommands/ResolveUrlCommand.php @@ -0,0 +1,80 @@ +urlShortener = $urlShortener; + } + + public function configure() + { + $this->setName('shortcode:parse') + ->setDescription('Returns the long URL behind a short code') + ->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse'); + } + + public function interact(InputInterface $input, OutputInterface $output) + { + $shortCode = $input->getArgument('shortCode'); + if (! empty($shortCode)) { + return; + } + + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new Question( + 'A short code was not provided. Which short code do you want to parse?: ' + ); + + $shortCode = $helper->ask($input, $output, $question); + if (! empty($shortCode)) { + $input->setArgument('shortCode', $shortCode); + } + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $shortCode = $input->getArgument('shortCode'); + + try { + $longUrl = $this->urlShortener->shortCodeToUrl($shortCode); + if (! isset($longUrl)) { + $output->writeln(sprintf('No URL found for short code "%s"', $shortCode)); + return; + } + + $output->writeln(sprintf('Long URL %s', $longUrl)); + } catch (InvalidShortCodeException $e) { + $output->writeln( + sprintf('Provided short code "%s" has an invalid format.', $shortCode) + ); + } catch (\Exception $e) { + $output->writeln('' . $e . ''); + } + } +} From 96478f34006bd43031690f375bbd4133238fd9f3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 5 Jul 2016 23:25:39 +0200 Subject: [PATCH 24/28] Created the console application via a factory --- bin/cli | 8 +--- config/autoload/cli.global.php | 13 ++++++ config/autoload/services.global.php | 12 +++--- .../Command}/GenerateShortcodeCommand.php | 2 +- .../Command}/ResolveUrlCommand.php | 2 +- src/CLI/Factory/ApplicationFactory.php | 41 +++++++++++++++++++ 6 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 config/autoload/cli.global.php rename src/{CliCommands => CLI/Command}/GenerateShortcodeCommand.php (98%) rename src/{CliCommands => CLI/Command}/ResolveUrlCommand.php (98%) create mode 100644 src/CLI/Factory/ApplicationFactory.php diff --git a/bin/cli b/bin/cli index 8a2983c1..e400bef8 100755 --- a/bin/cli +++ b/bin/cli @@ -1,16 +1,10 @@ #!/usr/bin/env php addCommands([ - $container->get(GenerateShortcodeCommand::class), - $container->get(ResolveUrlCommand::class), -]); +$app = $container->get(CliApp::class); $app->run(); diff --git a/config/autoload/cli.global.php b/config/autoload/cli.global.php new file mode 100644 index 00000000..f1a36b7d --- /dev/null +++ b/config/autoload/cli.global.php @@ -0,0 +1,13 @@ + [ + 'commands' => [ + Command\GenerateShortcodeCommand::class, + Command\ResolveUrlCommand::class, + ] + ], + +]; diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index b3103ac3..9f4447f4 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,5 +1,5 @@ [ 'factories' => [ - Application::class => Container\ApplicationFactory::class, + Expressive\Application::class => Container\ApplicationFactory::class, + Console\Application::class => CLI\Factory\ApplicationFactory::class, // Url helpers Helper\UrlHelper::class => Helper\UrlHelperFactory::class, @@ -42,8 +44,8 @@ return [ Cache::class => CacheFactory::class, // Cli commands - CliCommands\GenerateShortcodeCommand::class => AnnotatedFactory::class, - CliCommands\ResolveUrlCommand::class => AnnotatedFactory::class, + CLI\Command\GenerateShortcodeCommand::class => AnnotatedFactory::class, + CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class, // Middleware Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, diff --git a/src/CliCommands/GenerateShortcodeCommand.php b/src/CLI/Command/GenerateShortcodeCommand.php similarity index 98% rename from src/CliCommands/GenerateShortcodeCommand.php rename to src/CLI/Command/GenerateShortcodeCommand.php index 68b076c3..ea6fc929 100644 --- a/src/CliCommands/GenerateShortcodeCommand.php +++ b/src/CLI/Command/GenerateShortcodeCommand.php @@ -1,5 +1,5 @@ get('config')['cli']; + $app = new CliApp(); + + $commands = isset($config['commands']) ? $config['commands'] : []; + foreach ($commands as $command) { + if (! $container->has($command)) { + continue; + } + + $app->add($container->get($command)); + } + + return $app; + } +} From 60f5e5290eb789376bc3f8b90c9c23be9f9460ae Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 6 Jul 2016 19:41:24 +0200 Subject: [PATCH 25/28] Created new command to list short urls --- config/autoload/cli.global.php | 1 + config/autoload/services.global.php | 1 + src/CLI/Command/ListShortcodesCommand.php | 72 +++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 src/CLI/Command/ListShortcodesCommand.php diff --git a/config/autoload/cli.global.php b/config/autoload/cli.global.php index f1a36b7d..1d5ea78f 100644 --- a/config/autoload/cli.global.php +++ b/config/autoload/cli.global.php @@ -7,6 +7,7 @@ return [ 'commands' => [ Command\GenerateShortcodeCommand::class, Command\ResolveUrlCommand::class, + Command\ListShortcodesCommand::class, ] ], diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 9f4447f4..1133642d 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -46,6 +46,7 @@ return [ // Cli commands CLI\Command\GenerateShortcodeCommand::class => AnnotatedFactory::class, CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class, + CLI\Command\ListShortcodesCommand::class => AnnotatedFactory::class, // Middleware Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, diff --git a/src/CLI/Command/ListShortcodesCommand.php b/src/CLI/Command/ListShortcodesCommand.php new file mode 100644 index 00000000..7df61588 --- /dev/null +++ b/src/CLI/Command/ListShortcodesCommand.php @@ -0,0 +1,72 @@ +shortUrlService = $shortUrlService; + } + + public function configure() + { + $this->setName('shortcode:list') + ->setDescription('List all short URLs') + ->addArgument( + 'page', + InputArgument::OPTIONAL, + sprintf('The first page to list (%s items per page)', PaginableRepositoryAdapter::ITEMS_PER_PAGE), + 1 + ); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $page = intval($input->getArgument('page')); + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + + do { + $result = $this->shortUrlService->listShortUrls($page); + $page++; + $table = new Table($output); + $table->setHeaders([ + 'Short code', + 'Original URL', + 'Date created', + 'Visits count', + ]); + + foreach ($result as $row) { + $table->addRow(array_values($row->jsonSerialize())); + } + $table->render(); + + $question = new ConfirmationQuestion('Continue with next page? (y/N) ', false); + } while ($helper->ask($input, $output, $question)); + } +} From 43f1f790ddb92308cd403d860022ba4b3154303a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 6 Jul 2016 20:10:19 +0200 Subject: [PATCH 26/28] Improved ListShortciodesCommand --- src/CLI/Command/ListShortcodesCommand.php | 24 ++++++++++++++----- .../Rest/ListShortcodesMiddleware.php | 4 ++-- ...lizerTrait.php => PaginatorUtilsTrait.php} | 13 +++++++++- 3 files changed, 32 insertions(+), 9 deletions(-) rename src/Paginator/Util/{PaginatorSerializerTrait.php => PaginatorUtilsTrait.php} (61%) diff --git a/src/CLI/Command/ListShortcodesCommand.php b/src/CLI/Command/ListShortcodesCommand.php index 7df61588..ac85d50f 100644 --- a/src/CLI/Command/ListShortcodesCommand.php +++ b/src/CLI/Command/ListShortcodesCommand.php @@ -2,19 +2,22 @@ namespace Acelaya\UrlShortener\CLI\Command; use Acelaya\UrlShortener\Paginator\Adapter\PaginableRepositoryAdapter; +use Acelaya\UrlShortener\Paginator\Util\PaginatorUtilsTrait; use Acelaya\UrlShortener\Service\ShortUrlService; use Acelaya\UrlShortener\Service\ShortUrlServiceInterface; use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Symfony\Component\Console\Command\Command; 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\ConfirmationQuestion; class ListShortcodesCommand extends Command { + use PaginatorUtilsTrait; + /** * @var ShortUrlServiceInterface */ @@ -36,9 +39,10 @@ class ListShortcodesCommand extends Command { $this->setName('shortcode:list') ->setDescription('List all short URLs') - ->addArgument( + ->addOption( 'page', - InputArgument::OPTIONAL, + 'p', + InputOption::VALUE_OPTIONAL, sprintf('The first page to list (%s items per page)', PaginableRepositoryAdapter::ITEMS_PER_PAGE), 1 ); @@ -46,7 +50,7 @@ class ListShortcodesCommand extends Command public function execute(InputInterface $input, OutputInterface $output) { - $page = intval($input->getArgument('page')); + $page = intval($input->getOption('page')); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); @@ -66,7 +70,15 @@ class ListShortcodesCommand extends Command } $table->render(); - $question = new ConfirmationQuestion('Continue with next page? (y/N) ', false); - } while ($helper->ask($input, $output, $question)); + if ($this->isLastPage($result)) { + $continue = false; + $output->writeln('You have reached last page'); + } else { + $continue = $helper->ask($input, $output, new ConfirmationQuestion( + sprintf('Continue with page %s? (y/N) ', $page), + false + )); + } + } while ($continue); } } diff --git a/src/Middleware/Rest/ListShortcodesMiddleware.php b/src/Middleware/Rest/ListShortcodesMiddleware.php index 6b74241c..99d82454 100644 --- a/src/Middleware/Rest/ListShortcodesMiddleware.php +++ b/src/Middleware/Rest/ListShortcodesMiddleware.php @@ -1,7 +1,7 @@ getCurrentPageNumber() >= $paginator->count(); + } } From 2e00a8dec69c2979873fd811db633f434f3687a5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 6 Jul 2016 20:21:34 +0200 Subject: [PATCH 27/28] Created command to list visits for a shortcode --- config/autoload/cli.global.php | 1 + config/autoload/services.global.php | 1 + src/CLI/Command/GenerateShortcodeCommand.php | 2 - src/CLI/Command/GetVisitsCommand.php | 77 ++++++++++++++++++++ src/CLI/Command/ResolveUrlCommand.php | 2 - 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 src/CLI/Command/GetVisitsCommand.php diff --git a/config/autoload/cli.global.php b/config/autoload/cli.global.php index 1d5ea78f..1276cc9e 100644 --- a/config/autoload/cli.global.php +++ b/config/autoload/cli.global.php @@ -8,6 +8,7 @@ return [ Command\GenerateShortcodeCommand::class, Command\ResolveUrlCommand::class, Command\ListShortcodesCommand::class, + Command\GetVisitsCommand::class, ] ], diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 1133642d..4d6cc40e 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -47,6 +47,7 @@ return [ CLI\Command\GenerateShortcodeCommand::class => AnnotatedFactory::class, CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class, CLI\Command\ListShortcodesCommand::class => AnnotatedFactory::class, + CLI\Command\GetVisitsCommand::class => AnnotatedFactory::class, // Middleware Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, diff --git a/src/CLI/Command/GenerateShortcodeCommand.php b/src/CLI/Command/GenerateShortcodeCommand.php index ea6fc929..ffc355db 100644 --- a/src/CLI/Command/GenerateShortcodeCommand.php +++ b/src/CLI/Command/GenerateShortcodeCommand.php @@ -88,8 +88,6 @@ class GenerateShortcodeCommand extends Command $output->writeln( sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl) ); - } catch (\Exception $e) { - $output->writeln('' . $e . ''); } } } diff --git a/src/CLI/Command/GetVisitsCommand.php b/src/CLI/Command/GetVisitsCommand.php new file mode 100644 index 00000000..3c4e796d --- /dev/null +++ b/src/CLI/Command/GetVisitsCommand.php @@ -0,0 +1,77 @@ +visitsTracker = $visitsTracker; + } + + public function configure() + { + $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'); + } + + public function interact(InputInterface $input, OutputInterface $output) + { + $shortCode = $input->getArgument('shortCode'); + if (! empty($shortCode)) { + return; + } + + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new Question( + 'A short code was not provided. Which short code do you want to use?: ' + ); + + $shortCode = $helper->ask($input, $output, $question); + if (! empty($shortCode)) { + $input->setArgument('shortCode', $shortCode); + } + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $shortCode = $input->getArgument('shortCode'); + $visits = $this->visitsTracker->info($shortCode); + $table = new Table($output); + $table->setHeaders([ + 'Referer', + 'Date', + 'Temote Address', + 'User agent', + ]); + + foreach ($visits as $row) { + $table->addRow(array_values($row->jsonSerialize())); + } + $table->render(); + } +} diff --git a/src/CLI/Command/ResolveUrlCommand.php b/src/CLI/Command/ResolveUrlCommand.php index 73b330d9..4eb5ff41 100644 --- a/src/CLI/Command/ResolveUrlCommand.php +++ b/src/CLI/Command/ResolveUrlCommand.php @@ -73,8 +73,6 @@ class ResolveUrlCommand extends Command $output->writeln( sprintf('Provided short code "%s" has an invalid format.', $shortCode) ); - } catch (\Exception $e) { - $output->writeln('' . $e . ''); } } } From cdeffe9cc785047c18d1916c9fa9de00ce943049 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 6 Jul 2016 20:23:38 +0200 Subject: [PATCH 28/28] Added missing issue to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f92d16..c08baeb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [9: Use symfony/console to dispatch console requests, instead of trying to integrate the process with expressive](https://github.com/acelaya/url-shortener/issues/9) * [8: Create a REST API](https://github.com/acelaya/url-shortener/issues/8) +* [10: Add more CLI functionality](https://github.com/acelaya/url-shortener/issues/10) **Tasks**