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