From d117f82bcb00b09164b73dcb4f713d032d17e505 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 13 Apr 2017 09:39:35 +0200 Subject: [PATCH 01/55] Installed expressive tooling --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ec6e6ce9..637a66ab 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "roave/security-advisories": "dev-master", "filp/whoops": "^2.0", "symfony/var-dumper": "^3.0", - "vlucas/phpdotenv": "^2.2" + "vlucas/phpdotenv": "^2.2", + "zendframework/zend-expressive-tooling": "^0.4" }, "autoload": { "psr-4": { From 596d1ee7971a84e563f98ec2aa3f9c7fa777657e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 13 Apr 2017 09:43:11 +0200 Subject: [PATCH 02/55] Registered implicit options middleware --- config/autoload/middleware-pipeline.global.php | 7 ++++--- module/Rest/src/Middleware/CrossDomainMiddleware.php | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index e07149a5..bf81f13e 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -4,7 +4,7 @@ use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware; use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware; use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware; use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware; -use Zend\Expressive\Container\ApplicationFactory; +use Zend\Expressive; use Zend\Stratigility\Middleware\ErrorHandler; return [ @@ -27,7 +27,7 @@ return [ 'routing' => [ 'middleware' => [ - ApplicationFactory::ROUTING_MIDDLEWARE, + Expressive\Application::ROUTING_MIDDLEWARE, ], 'priority' => 10, ], @@ -36,6 +36,7 @@ return [ 'path' => '/rest', 'middleware' => [ CrossDomainMiddleware::class, + Expressive\Middleware\ImplicitOptionsMiddleware::class, BodyParserMiddleware::class, CheckAuthenticationMiddleware::class, ], @@ -44,7 +45,7 @@ return [ 'post-routing' => [ 'middleware' => [ - ApplicationFactory::DISPATCH_MIDDLEWARE, + Expressive\Application::DISPATCH_MIDDLEWARE, ], 'priority' => 1, ], diff --git a/module/Rest/src/Middleware/CrossDomainMiddleware.php b/module/Rest/src/Middleware/CrossDomainMiddleware.php index 0ce57e4f..4d73acbf 100644 --- a/module/Rest/src/Middleware/CrossDomainMiddleware.php +++ b/module/Rest/src/Middleware/CrossDomainMiddleware.php @@ -1,12 +1,13 @@ withHeader('Access-Control-Allow-Origin', $request->getHeader('Origin')) ->withHeader('Access-Control-Expose-Headers', 'Authorization'); - if ($request->getMethod() !== 'OPTIONS') { + if ($request->getMethod() !== self::METHOD_OPTIONS) { return $response; } From ec4a413a5b5afc3c2418533e1ac2ef18604e0d8a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 13 Apr 2017 09:45:31 +0200 Subject: [PATCH 03/55] Removed options bypass in actions in favor of implicit options middleware --- module/Rest/src/Action/AbstractRestAction.php | 29 ------------------- module/Rest/src/Action/AuthenticateAction.php | 2 +- .../Rest/src/Action/CreateShortcodeAction.php | 2 +- module/Rest/src/Action/EditTagsAction.php | 2 +- module/Rest/src/Action/GetVisitsAction.php | 2 +- .../Rest/src/Action/ListShortcodesAction.php | 3 +- module/Rest/src/Action/ResolveUrlAction.php | 2 +- 7 files changed, 6 insertions(+), 36 deletions(-) diff --git a/module/Rest/src/Action/AbstractRestAction.php b/module/Rest/src/Action/AbstractRestAction.php index c71c0176..01b8c13d 100644 --- a/module/Rest/src/Action/AbstractRestAction.php +++ b/module/Rest/src/Action/AbstractRestAction.php @@ -3,13 +3,9 @@ namespace Shlinkio\Shlink\Rest\Action; use Fig\Http\Message\RequestMethodInterface; use Fig\Http\Message\StatusCodeInterface; -use Interop\Http\ServerMiddleware\DelegateInterface; use Interop\Http\ServerMiddleware\MiddlewareInterface; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use Zend\Diactoros\Response\EmptyResponse; abstract class AbstractRestAction implements MiddlewareInterface, RequestMethodInterface, StatusCodeInterface { @@ -22,29 +18,4 @@ abstract class AbstractRestAction implements MiddlewareInterface, RequestMethodI { $this->logger = $logger ?: new NullLogger(); } - - /** - * Process an incoming server request and return a response, optionally delegating - * to the next middleware component to create the response. - * - * @param Request $request - * @param DelegateInterface $delegate - * - * @return Response - */ - public function process(Request $request, DelegateInterface $delegate) - { - if ($request->getMethod() === self::METHOD_OPTIONS) { - return new EmptyResponse(); - } - - return $this->dispatch($request, $delegate); - } - - /** - * @param Request $request - * @param DelegateInterface $delegate - * @return null|Response - */ - abstract protected function dispatch(Request $request, DelegateInterface $delegate); } diff --git a/module/Rest/src/Action/AuthenticateAction.php b/module/Rest/src/Action/AuthenticateAction.php index 27d67bc3..73d6a7df 100644 --- a/module/Rest/src/Action/AuthenticateAction.php +++ b/module/Rest/src/Action/AuthenticateAction.php @@ -55,7 +55,7 @@ class AuthenticateAction extends AbstractRestAction * @param DelegateInterface $delegate * @return null|Response */ - public function dispatch(Request $request, DelegateInterface $delegate) + public function process(Request $request, DelegateInterface $delegate) { $authData = $request->getParsedBody(); if (! isset($authData['apiKey'])) { diff --git a/module/Rest/src/Action/CreateShortcodeAction.php b/module/Rest/src/Action/CreateShortcodeAction.php index e63db63a..5384aac9 100644 --- a/module/Rest/src/Action/CreateShortcodeAction.php +++ b/module/Rest/src/Action/CreateShortcodeAction.php @@ -56,7 +56,7 @@ class CreateShortcodeAction extends AbstractRestAction * @param DelegateInterface $delegate * @return null|Response */ - public function dispatch(Request $request, DelegateInterface $delegate) + public function process(Request $request, DelegateInterface $delegate) { $postData = $request->getParsedBody(); if (! isset($postData['longUrl'])) { diff --git a/module/Rest/src/Action/EditTagsAction.php b/module/Rest/src/Action/EditTagsAction.php index a61e940a..075e65f0 100644 --- a/module/Rest/src/Action/EditTagsAction.php +++ b/module/Rest/src/Action/EditTagsAction.php @@ -47,7 +47,7 @@ class EditTagsAction extends AbstractRestAction * @param DelegateInterface $delegate * @return null|Response */ - protected function dispatch(Request $request, DelegateInterface $delegate) + public function process(Request $request, DelegateInterface $delegate) { $shortCode = $request->getAttribute('shortCode'); $bodyParams = $request->getParsedBody(); diff --git a/module/Rest/src/Action/GetVisitsAction.php b/module/Rest/src/Action/GetVisitsAction.php index c5e6b71b..681c9338 100644 --- a/module/Rest/src/Action/GetVisitsAction.php +++ b/module/Rest/src/Action/GetVisitsAction.php @@ -48,7 +48,7 @@ class GetVisitsAction extends AbstractRestAction * @param DelegateInterface $delegate * @return null|Response */ - public function dispatch(Request $request, DelegateInterface $delegate) + public function process(Request $request, DelegateInterface $delegate) { $shortCode = $request->getAttribute('shortCode'); $startDate = $this->getDateQueryParam($request, 'startDate'); diff --git a/module/Rest/src/Action/ListShortcodesAction.php b/module/Rest/src/Action/ListShortcodesAction.php index b7099c1c..43803a7f 100644 --- a/module/Rest/src/Action/ListShortcodesAction.php +++ b/module/Rest/src/Action/ListShortcodesAction.php @@ -6,7 +6,6 @@ use Interop\Http\ServerMiddleware\DelegateInterface; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait; use Shlinkio\Shlink\Core\Service\ShortUrlService; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; @@ -50,7 +49,7 @@ class ListShortcodesAction extends AbstractRestAction * @param DelegateInterface $delegate * @return null|Response */ - public function dispatch(Request $request, DelegateInterface $delegate) + public function process(Request $request, DelegateInterface $delegate) { try { $params = $this->queryToListParams($request->getQueryParams()); diff --git a/module/Rest/src/Action/ResolveUrlAction.php b/module/Rest/src/Action/ResolveUrlAction.php index b705814a..55280922 100644 --- a/module/Rest/src/Action/ResolveUrlAction.php +++ b/module/Rest/src/Action/ResolveUrlAction.php @@ -47,7 +47,7 @@ class ResolveUrlAction extends AbstractRestAction * @param DelegateInterface $delegate * @return null|Response */ - public function dispatch(Request $request, DelegateInterface $delegate) + public function process(Request $request, DelegateInterface $delegate) { $shortCode = $request->getAttribute('shortCode'); From 5d2698e8a1253de8bc7b68054b60048fbaf33ceb Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 13 Apr 2017 09:52:17 +0200 Subject: [PATCH 04/55] Created EmptyResponseImplicitOptionsMiddlewareFactoryTest --- config/autoload/dependencies.global.php | 3 ++ ...sponseImplicitOptionsMiddlewareFactory.php | 30 +++++++++++++ ...seImplicitOptionsMiddlewareFactoryTest.php | 45 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 module/Common/src/Factory/EmptyResponseImplicitOptionsMiddlewareFactory.php create mode 100644 module/Common/test/Factory/EmptyResponseImplicitOptionsMiddlewareFactoryTest.php diff --git a/config/autoload/dependencies.global.php b/config/autoload/dependencies.global.php index e2b88ba8..83a893dc 100644 --- a/config/autoload/dependencies.global.php +++ b/config/autoload/dependencies.global.php @@ -1,6 +1,8 @@ Twig\TwigEnvironmentFactory::class, Router\RouterInterface::class => Router\FastRouteRouterFactory::class, ErrorHandler::class => Container\ErrorHandlerFactory::class, + Middleware\ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class, ], ], diff --git a/module/Common/src/Factory/EmptyResponseImplicitOptionsMiddlewareFactory.php b/module/Common/src/Factory/EmptyResponseImplicitOptionsMiddlewareFactory.php new file mode 100644 index 00000000..1551d8cd --- /dev/null +++ b/module/Common/src/Factory/EmptyResponseImplicitOptionsMiddlewareFactory.php @@ -0,0 +1,30 @@ +factory = new EmptyResponseImplicitOptionsMiddlewareFactory(); + } + + /** + * @test + */ + public function serviceIsCreated() + { + $instance = $this->factory->__invoke(new ServiceManager(), ''); + $this->assertInstanceOf(ImplicitOptionsMiddleware::class, $instance); + } + + /** + * @test + */ + public function responsePrototypeIsEmptyResponse() + { + $instance = $this->factory->__invoke(new ServiceManager(), ''); + + $ref = new \ReflectionObject($instance); + $prop = $ref->getProperty('response'); + $prop->setAccessible(true); + $this->assertInstanceOf(EmptyResponse::class, $prop->getValue($instance)); + } +} From a365faef9c452b0657185525efa5c3cd295eccee Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Apr 2017 12:52:24 +0200 Subject: [PATCH 05/55] Removed requirement of OPTIONS on every route --- module/Rest/config/routes.config.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 8d107d8b..4edf4658 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -8,19 +8,19 @@ return [ 'name' => 'rest-authenticate', 'path' => '/rest/v{version:1}/authenticate', 'middleware' => Action\AuthenticateAction::class, - 'allowed_methods' => ['POST', 'OPTIONS'], + 'allowed_methods' => ['POST'], ], [ 'name' => 'rest-create-shortcode', 'path' => '/rest/v{version:1}/short-codes', 'middleware' => Action\CreateShortcodeAction::class, - 'allowed_methods' => ['POST', 'OPTIONS'], + 'allowed_methods' => ['POST'], ], [ 'name' => 'rest-resolve-url', 'path' => '/rest/v{version:1}/short-codes/{shortCode}', 'middleware' => Action\ResolveUrlAction::class, - 'allowed_methods' => ['GET', 'OPTIONS'], + 'allowed_methods' => ['GET'], ], [ 'name' => 'rest-list-shortened-url', @@ -32,13 +32,13 @@ return [ 'name' => 'rest-get-visits', 'path' => '/rest/v{version:1}/short-codes/{shortCode}/visits', 'middleware' => Action\GetVisitsAction::class, - 'allowed_methods' => ['GET', 'OPTIONS'], + 'allowed_methods' => ['GET'], ], [ 'name' => 'rest-edit-tags', 'path' => '/rest/v{version:1}/short-codes/{shortCode}/tags', 'middleware' => Action\EditTagsAction::class, - 'allowed_methods' => ['PUT', 'OPTIONS'], + 'allowed_methods' => ['PUT'], ], ], From 62b49dcb19b245874c32ab8bdc329540d7597931 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Apr 2017 12:55:34 +0200 Subject: [PATCH 06/55] Set cross domain allow-methods header with the same value as the allow header --- module/Rest/src/Middleware/CrossDomainMiddleware.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/module/Rest/src/Middleware/CrossDomainMiddleware.php b/module/Rest/src/Middleware/CrossDomainMiddleware.php index 4d73acbf..eabbe0ac 100644 --- a/module/Rest/src/Middleware/CrossDomainMiddleware.php +++ b/module/Rest/src/Middleware/CrossDomainMiddleware.php @@ -17,6 +17,7 @@ class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterfa * @param DelegateInterface $delegate * * @return Response + * @throws \InvalidArgumentException */ public function process(Request $request, DelegateInterface $delegate) { @@ -35,7 +36,8 @@ class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterfa // Add OPTIONS-specific headers foreach ([ - 'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS', // TODO Should be based on path +// 'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS', // TODO Should be based on path + 'Access-Control-Allow-Methods' => $response->getHeaderLine('Allow'), 'Access-Control-Max-Age' => '1000', 'Access-Control-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'), ] as $key => $value) { From 52478ca60a61b867eaee27390edfdde59914f3b7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Apr 2017 13:27:41 +0200 Subject: [PATCH 07/55] Returned all allowed methods until fast route router is fixed --- module/Rest/src/Middleware/CrossDomainMiddleware.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/Rest/src/Middleware/CrossDomainMiddleware.php b/module/Rest/src/Middleware/CrossDomainMiddleware.php index eabbe0ac..213a3541 100644 --- a/module/Rest/src/Middleware/CrossDomainMiddleware.php +++ b/module/Rest/src/Middleware/CrossDomainMiddleware.php @@ -36,8 +36,8 @@ class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterfa // Add OPTIONS-specific headers foreach ([ -// 'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS', // TODO Should be based on path - 'Access-Control-Allow-Methods' => $response->getHeaderLine('Allow'), + 'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS', // TODO Should be dynamic +// 'Access-Control-Allow-Methods' => $response->getHeaderLine('Allow'), 'Access-Control-Max-Age' => '1000', 'Access-Control-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'), ] as $key => $value) { From 10da57572fa9047465eddc892f083d9201250072 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Apr 2017 10:27:27 +0200 Subject: [PATCH 08/55] Fixed date format returned by the API --- .phpstorm.meta.php | 2 +- docs/swagger/paths/v1_authenticate.json | 5 +++++ docs/swagger/paths/v1_short-codes.json | 22 +++++++++++++++++++ .../CLI/src/Command/Api/ListKeysCommand.php | 2 +- module/Core/src/Entity/ShortUrl.php | 2 +- module/Core/src/Entity/Visit.php | 2 +- 6 files changed, 31 insertions(+), 4 deletions(-) diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 1a5e12de..5acf91bd 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -1,7 +1,7 @@ {$formatMethod}($this->getEnabledSymbol($row)); } - $rowData[] = isset($expiration) ? $expiration->format(\DateTime::ISO8601) : '-'; + $rowData[] = isset($expiration) ? $expiration->format(\DateTime::ATOM) : '-'; $table->addRow($rowData); } diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index cf1ed87e..44e992ce 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -176,7 +176,7 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable return [ 'shortCode' => $this->shortCode, 'originalUrl' => $this->originalUrl, - 'dateCreated' => isset($this->dateCreated) ? $this->dateCreated->format(\DateTime::ISO8601) : null, + 'dateCreated' => isset($this->dateCreated) ? $this->dateCreated->format(\DateTime::ATOM) : null, 'visitsCount' => count($this->visits), 'tags' => $this->tags->toArray(), ]; diff --git a/module/Core/src/Entity/Visit.php b/module/Core/src/Entity/Visit.php index a95c61b0..c1ab7333 100644 --- a/module/Core/src/Entity/Visit.php +++ b/module/Core/src/Entity/Visit.php @@ -171,7 +171,7 @@ class Visit extends AbstractEntity implements \JsonSerializable { return [ 'referer' => $this->referer, - 'date' => isset($this->date) ? $this->date->format(\DateTime::ISO8601) : null, + 'date' => isset($this->date) ? $this->date->format(\DateTime::ATOM) : null, 'remoteAddr' => $this->remoteAddr, 'userAgent' => $this->userAgent, 'visitLocation' => $this->visitLocation, From 17be221920892a93f47f81f48b22f09e7174a5ec Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Apr 2017 10:45:52 +0200 Subject: [PATCH 09/55] Added response examples to swagger docs --- docs/swagger/paths/v1_short-codes.json | 18 ++++++++++++- .../paths/v1_short-codes_{shortCode}.json | 5 ++++ .../v1_short-codes_{shortCode}_tags.json | 8 ++++++ .../v1_short-codes_{shortCode}_visits.json | 26 +++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/swagger/paths/v1_short-codes.json b/docs/swagger/paths/v1_short-codes.json index 0f0778e8..bdeffe3f 100644 --- a/docs/swagger/paths/v1_short-codes.json +++ b/docs/swagger/paths/v1_short-codes.json @@ -74,7 +74,7 @@ "shortUrls": { "data": [ { - "shortCode": "", + "shortCode": "12C18", "originalUrl": "https://store.steampowered.com", "dateCreated": "2016-08-21T20:34:16+02:00", "visitsCount": 328, @@ -82,6 +82,22 @@ "games", "tech" ] + }, + { + "shortCode": "12Kb3", + "originalUrl": "https://shlink.io", + "dateCreated": "2016-05-01T20:34:16+02:00", + "visitsCount": 1029, + "tags": [ + "shlink" + ] + }, + { + "shortCode": "123bA", + "originalUrl": "https://www.google.com", + "dateCreated": "2015-10-01T20:34:16+02:00", + "visitsCount": 25, + "tags": [] } ], "pagination": { diff --git a/docs/swagger/paths/v1_short-codes_{shortCode}.json b/docs/swagger/paths/v1_short-codes_{shortCode}.json index a706f675..78e54f61 100644 --- a/docs/swagger/paths/v1_short-codes_{shortCode}.json +++ b/docs/swagger/paths/v1_short-codes_{shortCode}.json @@ -28,6 +28,11 @@ "description": "The original long URL behind the short code." } } + }, + "examples": { + "application/json": { + "longUrl": "https://shlink.io" + } } }, "400": { diff --git a/docs/swagger/paths/v1_short-codes_{shortCode}_tags.json b/docs/swagger/paths/v1_short-codes_{shortCode}_tags.json index 3cbfc952..5420d4ee 100644 --- a/docs/swagger/paths/v1_short-codes_{shortCode}_tags.json +++ b/docs/swagger/paths/v1_short-codes_{shortCode}_tags.json @@ -41,6 +41,14 @@ } } } + }, + "examples": { + "application/json": { + "tags": [ + "games", + "tech" + ] + } } }, "400": { diff --git a/docs/swagger/paths/v1_short-codes_{shortCode}_visits.json b/docs/swagger/paths/v1_short-codes_{shortCode}_visits.json index f90daf7a..ae68a8dd 100644 --- a/docs/swagger/paths/v1_short-codes_{shortCode}_visits.json +++ b/docs/swagger/paths/v1_short-codes_{shortCode}_visits.json @@ -36,6 +36,32 @@ } } } + }, + "examples": { + "application/json": { + "visits": { + "data": [ + { + "referer": "https://twitter.com", + "date": "2015-08-20T05:05:03+04:00", + "remoteAddr": "10.20.30.40", + "userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0" + }, + { + "referer": "https://t.co", + "date": "2015-08-20T05:05:03+04:00", + "remoteAddr": "11.22.33.44", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" + }, + { + "referer": null, + "date": "2015-08-20T05:05:03+04:00", + "remoteAddr": "110.220.5.6", + "userAgent": "some_web_crawler/1.4" + } + ] + } + } } }, "404": { From c45cb7bacb8e884b4d0caabf3a76e2cad0df486c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 3 Jul 2017 12:49:32 +0200 Subject: [PATCH 10/55] Updated build script --- build.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.sh b/build.sh index ae952aae..550ea963 100755 --- a/build.sh +++ b/build.sh @@ -8,7 +8,7 @@ if [ "$#" -ne 1 ]; then fi version=$1 -builtcontent=$(readlink -f '../shlink_build_tmp') +builtcontent=$(readlink -f '../shlink_${version}_dist') projectdir=$(pwd) # Copy project content to temp dir @@ -31,6 +31,8 @@ rm build.sh rm CHANGELOG.md rm composer.* rm LICENSE +rm indocker +rm docker-compose.yml rm php* rm README.md rm -r build From 04c479148a136e5e7980250eb3c1f1bf202d311c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 3 Jul 2017 12:54:50 +0200 Subject: [PATCH 11/55] Updated build script so that generated zip contains one folder --- build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 550ea963..b3c65a53 100755 --- a/build.sh +++ b/build.sh @@ -8,7 +8,7 @@ if [ "$#" -ne 1 ]; then fi version=$1 -builtcontent=$(readlink -f '../shlink_${version}_dist') +builtcontent=$(readlink -f "../shlink_${version}_dist") projectdir=$(pwd) # Copy project content to temp dir @@ -44,5 +44,5 @@ rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore} # Compressing file rm -f "${projectdir}"/build/shlink_${version}_dist.zip -zip -ry "${projectdir}"/build/shlink_${version}_dist.zip . +zip -ry "${projectdir}"/build/shlink_${version}_dist.zip "../shlink_${version}_dist" rm -rf "${builtcontent}" From 584e1f5643b6e0807c94291794ce912037839d92 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 3 Jul 2017 13:10:16 +0200 Subject: [PATCH 12/55] Created common abstract command for update and install --- bin/install | 5 +- bin/update | 5 +- .../Install/AbstractInstallCommand.php | 323 ++++++++++++++++++ .../src/Command/Install/InstallCommand.php | 313 +---------------- .../CLI/src/Command/Install/UpdateCommand.php | 4 +- 5 files changed, 331 insertions(+), 319 deletions(-) create mode 100644 module/CLI/src/Command/Install/AbstractInstallCommand.php diff --git a/bin/install b/bin/install index c47036a9..b563f395 100755 --- a/bin/install +++ b/bin/install @@ -9,6 +9,7 @@ chdir(dirname(__DIR__)); require __DIR__ . '/../vendor/autoload.php'; $app = new Application(); -$app->add(new InstallCommand(new PhpArray())); -$app->setDefaultCommand('shlink:install'); +$command = new InstallCommand(new PhpArray()); +$app->add($command); +$app->setDefaultCommand($command->getName()); $app->run(); diff --git a/bin/update b/bin/update index b226a9fa..86b7efc9 100755 --- a/bin/update +++ b/bin/update @@ -9,6 +9,7 @@ chdir(dirname(__DIR__)); require __DIR__ . '/../vendor/autoload.php'; $app = new Application(); -$app->add(new UpdateCommand(new PhpArray())); -$app->setDefaultCommand('shlink:install'); +$command = new UpdateCommand(new PhpArray()); +$app->add($command); +$app->setDefaultCommand($command->getName()); $app->run(); diff --git a/module/CLI/src/Command/Install/AbstractInstallCommand.php b/module/CLI/src/Command/Install/AbstractInstallCommand.php new file mode 100644 index 00000000..472377d4 --- /dev/null +++ b/module/CLI/src/Command/Install/AbstractInstallCommand.php @@ -0,0 +1,323 @@ + 'pdo_mysql', + 'PostgreSQL' => 'pdo_pgsql', + 'SQLite' => 'pdo_sqlite', + ]; + const SUPPORTED_LANGUAGES = ['en', 'es']; + + /** + * @var InputInterface + */ + protected $input; + /** + * @var OutputInterface + */ + protected $output; + /** + * @var QuestionHelper + */ + private $questionHelper; + /** + * @var ProcessHelper + */ + private $processHelper; + /** + * @var WriterInterface + */ + private $configWriter; + + /** + * InstallCommand constructor. + * @param WriterInterface $configWriter + */ + public function __construct(WriterInterface $configWriter) + { + parent::__construct(null); + $this->configWriter = $configWriter; + } + + public function configure() + { + $this->setName('shlink:install') + ->setDescription('Installs Shlink'); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + $this->questionHelper = $this->getHelper('question'); + $this->processHelper = $this->getHelper('process'); + $params = []; + + $output->writeln([ + 'Welcome to Shlink!!', + 'This will guide you through the installation process.', + ]); + + // Check if a cached config file exists and drop it if so + if (file_exists('data/cache/app_config.php')) { + $output->write('Deleting old cached config...'); + if (unlink('data/cache/app_config.php')) { + $output->writeln(' Success'); + } else { + $output->writeln( + ' Failed! You will have to manually delete the data/cache/app_config.php file to get' + . ' new config applied.' + ); + } + } + + // Ask for custom config params + $params['DATABASE'] = $this->askDatabase(); + $params['URL_SHORTENER'] = $this->askUrlShortener(); + $params['LANGUAGE'] = $this->askLanguage(); + $params['APP'] = $this->askApplication(); + + // Generate config params files + $config = $this->buildAppConfig($params); + $this->configWriter->toFile('config/params/generated_config.php', $config, false); + $output->writeln(['Custom configuration properly generated!', '']); + + // Generate database + if (! $this->createDatabase()) { + return; + } + + // Run database migrations + $output->writeln('Updating database...'); + if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) { + return; + } + + // Generate proxies + $output->writeln('Generating proxies...'); + if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) { + return; + } + } + + protected function askDatabase() + { + $params = []; + $this->printTitle('DATABASE'); + + // Select database type + $databases = array_keys(self::DATABASE_DRIVERS); + $dbType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( + 'Select database type (defaults to ' . $databases[0] . '):', + $databases, + 0 + )); + $params['DRIVER'] = self::DATABASE_DRIVERS[$dbType]; + + // Ask for connection params if database is not SQLite + if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) { + $params['NAME'] = $this->ask('Database name', 'shlink'); + $params['USER'] = $this->ask('Database username'); + $params['PASSWORD'] = $this->ask('Database password'); + $params['HOST'] = $this->ask('Database host', 'localhost'); + $params['PORT'] = $this->ask('Database port', $this->getDefaultDbPort($params['DRIVER'])); + } + + return $params; + } + + protected function getDefaultDbPort($driver) + { + return $driver === 'pdo_mysql' ? '3306' : '5432'; + } + + protected function askUrlShortener() + { + $this->printTitle('URL SHORTENER'); + + // Ask for URL shortener params + return [ + 'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( + 'Select schema for generated short URLs (defaults to http):', + ['http', 'https'], + 0 + )), + 'HOSTNAME' => $this->ask('Hostname for generated URLs'), + 'CHARS' => $this->ask( + 'Character set for generated short codes (leave empty to autogenerate one)', + null, + true + ) ?: str_shuffle(UrlShortener::DEFAULT_CHARS) + ]; + } + + protected function askLanguage() + { + $this->printTitle('LANGUAGE'); + + return [ + 'DEFAULT' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( + 'Select default language for the application in general (defaults to ' + . self::SUPPORTED_LANGUAGES[0] . '):', + self::SUPPORTED_LANGUAGES, + 0 + )), + 'CLI' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( + 'Select default language for CLI executions (defaults to ' + . self::SUPPORTED_LANGUAGES[0] . '):', + self::SUPPORTED_LANGUAGES, + 0 + )), + ]; + } + + protected function askApplication() + { + $this->printTitle('APPLICATION'); + + return [ + 'SECRET' => $this->ask( + 'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)', + null, + true + ) ?: $this->generateRandomString(32), + ]; + } + + /** + * @param string $text + */ + protected function printTitle($text) + { + $text = trim($text); + $length = strlen($text) + 4; + $header = str_repeat('*', $length); + + $this->output->writeln([ + '', + '' . $header . '', + '* ' . strtoupper($text) . ' *', + '' . $header . '', + ]); + } + + /** + * @param string $text + * @param string|null $default + * @param bool $allowEmpty + * @return string + * @throws RuntimeException + */ + protected function ask($text, $default = null, $allowEmpty = false) + { + if (isset($default)) { + $text .= ' (defaults to ' . $default . ')'; + } + do { + $value = $this->questionHelper->ask($this->input, $this->output, new Question( + '' . $text . ': ', + $default + )); + if (empty($value) && ! $allowEmpty) { + $this->output->writeln('Value can\'t be empty'); + } + } while (empty($value) && $default === null && ! $allowEmpty); + + return $value; + } + + /** + * @param array $params + * @return array + */ + protected function buildAppConfig(array $params) + { + // Build simple config + $config = [ + 'app_options' => [ + 'secret_key' => $params['APP']['SECRET'], + ], + 'entity_manager' => [ + 'connection' => [ + 'driver' => $params['DATABASE']['DRIVER'], + ], + ], + 'translator' => [ + 'locale' => $params['LANGUAGE']['DEFAULT'], + ], + 'cli' => [ + 'locale' => $params['LANGUAGE']['CLI'], + ], + 'url_shortener' => [ + 'domain' => [ + 'schema' => $params['URL_SHORTENER']['SCHEMA'], + 'hostname' => $params['URL_SHORTENER']['HOSTNAME'], + ], + 'shortcode_chars' => $params['URL_SHORTENER']['CHARS'], + ], + ]; + + // Build dynamic database config + if ($params['DATABASE']['DRIVER'] === 'pdo_sqlite') { + $config['entity_manager']['connection']['path'] = 'data/database.sqlite'; + } else { + $config['entity_manager']['connection']['user'] = $params['DATABASE']['USER']; + $config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD']; + $config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME']; + $config['entity_manager']['connection']['host'] = $params['DATABASE']['HOST']; + $config['entity_manager']['connection']['port'] = $params['DATABASE']['PORT']; + + if ($params['DATABASE']['DRIVER'] === 'pdo_mysql') { + $config['entity_manager']['connection']['driverOptions'] = [ + \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', + ]; + } + } + + return $config; + } + + /** + * @return bool + */ + abstract protected function createDatabase(); + + /** + * @param string $command + * @param string $errorMessage + * @return bool + */ + protected function runCommand($command, $errorMessage) + { + $process = $this->processHelper->run($this->output, $command); + if ($process->isSuccessful()) { + $this->output->writeln(' Success!'); + return true; + } + + if ($this->output->isVerbose()) { + return false; + } + + $this->output->writeln( + ' ' . $errorMessage . ' Run this command with -vvv to see specific error info.' + ); + return false; + } +} diff --git a/module/CLI/src/Command/Install/InstallCommand.php b/module/CLI/src/Command/Install/InstallCommand.php index b331511a..a8e07de1 100644 --- a/module/CLI/src/Command/Install/InstallCommand.php +++ b/module/CLI/src/Command/Install/InstallCommand.php @@ -1,322 +1,11 @@ 'pdo_mysql', - 'PostgreSQL' => 'pdo_pgsql', - 'SQLite' => 'pdo_sqlite', - ]; - const SUPPORTED_LANGUAGES = ['en', 'es']; - - /** - * @var InputInterface - */ - private $input; - /** - * @var OutputInterface - */ - private $output; - /** - * @var QuestionHelper - */ - private $questionHelper; - /** - * @var ProcessHelper - */ - private $processHelper; - /** - * @var WriterInterface - */ - private $configWriter; - - /** - * InstallCommand constructor. - * @param WriterInterface $configWriter - * @param callable|null $databaseCreationLogic - */ - public function __construct(WriterInterface $configWriter) - { - parent::__construct(null); - $this->configWriter = $configWriter; - } - - public function configure() - { - $this->setName('shlink:install') - ->setDescription('Installs Shlink'); - } - - public function execute(InputInterface $input, OutputInterface $output) - { - $this->input = $input; - $this->output = $output; - $this->questionHelper = $this->getHelper('question'); - $this->processHelper = $this->getHelper('process'); - $params = []; - - $output->writeln([ - 'Welcome to Shlink!!', - 'This process will guide you through the installation.', - ]); - - // Check if a cached config file exists and drop it if so - if (file_exists('data/cache/app_config.php')) { - $output->write('Deleting old cached config...'); - if (unlink('data/cache/app_config.php')) { - $output->writeln(' Success'); - } else { - $output->writeln( - ' Failed! You will have to manually delete the data/cache/app_config.php file to get' - . ' new config applied.' - ); - } - } - - // Ask for custom config params - $params['DATABASE'] = $this->askDatabase(); - $params['URL_SHORTENER'] = $this->askUrlShortener(); - $params['LANGUAGE'] = $this->askLanguage(); - $params['APP'] = $this->askApplication(); - - // Generate config params files - $config = $this->buildAppConfig($params); - $this->configWriter->toFile('config/params/generated_config.php', $config, false); - $output->writeln(['Custom configuration properly generated!', '']); - - // Generate database - if (! $this->createDatabase()) { - return; - } - - // Run database migrations - $output->writeln('Updating database...'); - if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) { - return; - } - - // Generate proxies - $output->writeln('Generating proxies...'); - if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) { - return; - } - } - - protected function askDatabase() - { - $params = []; - $this->printTitle('DATABASE'); - - // Select database type - $databases = array_keys(self::DATABASE_DRIVERS); - $dbType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select database type (defaults to ' . $databases[0] . '):', - $databases, - 0 - )); - $params['DRIVER'] = self::DATABASE_DRIVERS[$dbType]; - - // Ask for connection params if database is not SQLite - if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) { - $params['NAME'] = $this->ask('Database name', 'shlink'); - $params['USER'] = $this->ask('Database username'); - $params['PASSWORD'] = $this->ask('Database password'); - $params['HOST'] = $this->ask('Database host', 'localhost'); - $params['PORT'] = $this->ask('Database port', $this->getDefaultDbPort($params['DRIVER'])); - } - - return $params; - } - - protected function getDefaultDbPort($driver) - { - return $driver === 'pdo_mysql' ? '3306' : '5432'; - } - - protected function askUrlShortener() - { - $this->printTitle('URL SHORTENER'); - - // Ask for URL shortener params - return [ - 'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select schema for generated short URLs (defaults to http):', - ['http', 'https'], - 0 - )), - 'HOSTNAME' => $this->ask('Hostname for generated URLs'), - 'CHARS' => $this->ask( - 'Character set for generated short codes (leave empty to autogenerate one)', - null, - true - ) ?: str_shuffle(UrlShortener::DEFAULT_CHARS) - ]; - } - - protected function askLanguage() - { - $this->printTitle('LANGUAGE'); - - return [ - 'DEFAULT' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select default language for the application in general (defaults to ' - . self::SUPPORTED_LANGUAGES[0] . '):', - self::SUPPORTED_LANGUAGES, - 0 - )), - 'CLI' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select default language for CLI executions (defaults to ' - . self::SUPPORTED_LANGUAGES[0] . '):', - self::SUPPORTED_LANGUAGES, - 0 - )), - ]; - } - - protected function askApplication() - { - $this->printTitle('APPLICATION'); - - return [ - 'SECRET' => $this->ask( - 'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)', - null, - true - ) ?: $this->generateRandomString(32), - ]; - } - - /** - * @param string $text - */ - protected function printTitle($text) - { - $text = trim($text); - $length = strlen($text) + 4; - $header = str_repeat('*', $length); - - $this->output->writeln([ - '', - '' . $header . '', - '* ' . strtoupper($text) . ' *', - '' . $header . '', - ]); - } - - /** - * @param string $text - * @param string|null $default - * @param bool $allowEmpty - * @return string - */ - protected function ask($text, $default = null, $allowEmpty = false) - { - if (isset($default)) { - $text .= ' (defaults to ' . $default . ')'; - } - do { - $value = $this->questionHelper->ask($this->input, $this->output, new Question( - '' . $text . ': ', - $default - )); - if (empty($value) && ! $allowEmpty) { - $this->output->writeln('Value can\'t be empty'); - } - } while (empty($value) && empty($default) && ! $allowEmpty); - - return $value; - } - - /** - * @param array $params - * @return array - */ - protected function buildAppConfig(array $params) - { - // Build simple config - $config = [ - 'app_options' => [ - 'secret_key' => $params['APP']['SECRET'], - ], - 'entity_manager' => [ - 'connection' => [ - 'driver' => $params['DATABASE']['DRIVER'], - ], - ], - 'translator' => [ - 'locale' => $params['LANGUAGE']['DEFAULT'], - ], - 'cli' => [ - 'locale' => $params['LANGUAGE']['CLI'], - ], - 'url_shortener' => [ - 'domain' => [ - 'schema' => $params['URL_SHORTENER']['SCHEMA'], - 'hostname' => $params['URL_SHORTENER']['HOSTNAME'], - ], - 'shortcode_chars' => $params['URL_SHORTENER']['CHARS'], - ], - ]; - - // Build dynamic database config - if ($params['DATABASE']['DRIVER'] === 'pdo_sqlite') { - $config['entity_manager']['connection']['path'] = 'data/database.sqlite'; - } else { - $config['entity_manager']['connection']['user'] = $params['DATABASE']['USER']; - $config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD']; - $config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME']; - $config['entity_manager']['connection']['host'] = $params['DATABASE']['HOST']; - $config['entity_manager']['connection']['port'] = $params['DATABASE']['PORT']; - - if ($params['DATABASE']['DRIVER'] === 'pdo_mysql') { - $config['entity_manager']['connection']['driverOptions'] = [ - \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', - ]; - } - } - - return $config; - } - protected function createDatabase() { $this->output->writeln('Initializing database...'); return $this->runCommand('php vendor/bin/doctrine.php orm:schema-tool:create', 'Error generating database.'); } - - /** - * @param string $command - * @param string $errorMessage - * @return bool - */ - protected function runCommand($command, $errorMessage) - { - $process = $this->processHelper->run($this->output, $command); - if ($process->isSuccessful()) { - $this->output->writeln(' Success!'); - return true; - } else { - if ($this->output->isVerbose()) { - return false; - } - $this->output->writeln( - ' ' . $errorMessage . ' Run this command with -vvv to see specific error info.' - ); - return false; - } - } } diff --git a/module/CLI/src/Command/Install/UpdateCommand.php b/module/CLI/src/Command/Install/UpdateCommand.php index 3aed5512..def28d06 100644 --- a/module/CLI/src/Command/Install/UpdateCommand.php +++ b/module/CLI/src/Command/Install/UpdateCommand.php @@ -1,9 +1,7 @@ Date: Mon, 3 Jul 2017 13:11:45 +0200 Subject: [PATCH 13/55] Fixed inspections --- module/CLI/src/Command/Install/AbstractInstallCommand.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/module/CLI/src/Command/Install/AbstractInstallCommand.php b/module/CLI/src/Command/Install/AbstractInstallCommand.php index 472377d4..9b186281 100644 --- a/module/CLI/src/Command/Install/AbstractInstallCommand.php +++ b/module/CLI/src/Command/Install/AbstractInstallCommand.php @@ -4,6 +4,7 @@ namespace Shlinkio\Shlink\CLI\Command\Install; use Shlinkio\Shlink\Common\Util\StringUtilsTrait; use Shlinkio\Shlink\Core\Service\UrlShortener; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Console\Helper\QuestionHelper; @@ -48,10 +49,11 @@ abstract class AbstractInstallCommand extends Command /** * InstallCommand constructor. * @param WriterInterface $configWriter + * @throws LogicException */ public function __construct(WriterInterface $configWriter) { - parent::__construct(null); + parent::__construct(); $this->configWriter = $configWriter; } @@ -226,7 +228,7 @@ abstract class AbstractInstallCommand extends Command */ protected function ask($text, $default = null, $allowEmpty = false) { - if (isset($default)) { + if ($default !== null) { $text .= ' (defaults to ' . $default . ')'; } do { From 1fe2e6f6bdef8e5ce3d95ee633449df8fc2765f7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 3 Jul 2017 13:17:44 +0200 Subject: [PATCH 14/55] Improved check on update and install commands --- .../Install/AbstractInstallCommand.php | 22 ++++++++++++------- .../src/Command/Install/InstallCommand.php | 8 ++++--- .../CLI/src/Command/Install/UpdateCommand.php | 5 ++++- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/module/CLI/src/Command/Install/AbstractInstallCommand.php b/module/CLI/src/Command/Install/AbstractInstallCommand.php index 9b186281..5c271231 100644 --- a/module/CLI/src/Command/Install/AbstractInstallCommand.php +++ b/module/CLI/src/Command/Install/AbstractInstallCommand.php @@ -100,9 +100,15 @@ abstract class AbstractInstallCommand extends Command $this->configWriter->toFile('config/params/generated_config.php', $config, false); $output->writeln(['Custom configuration properly generated!', '']); - // Generate database - if (! $this->createDatabase()) { - return; + // If current command is not update, generate database + if (! $this->isUpdate()) { + $this->output->writeln('Initializing database...'); + if (! $this->runCommand( + 'php vendor/bin/doctrine.php orm:schema-tool:create', + 'Error generating database.' + )) { + return; + } } // Run database migrations @@ -295,11 +301,6 @@ abstract class AbstractInstallCommand extends Command return $config; } - /** - * @return bool - */ - abstract protected function createDatabase(); - /** * @param string $command * @param string $errorMessage @@ -322,4 +323,9 @@ abstract class AbstractInstallCommand extends Command ); return false; } + + /** + * @return bool + */ + abstract protected function isUpdate(); } diff --git a/module/CLI/src/Command/Install/InstallCommand.php b/module/CLI/src/Command/Install/InstallCommand.php index a8e07de1..e414bbe0 100644 --- a/module/CLI/src/Command/Install/InstallCommand.php +++ b/module/CLI/src/Command/Install/InstallCommand.php @@ -3,9 +3,11 @@ namespace Shlinkio\Shlink\CLI\Command\Install; class InstallCommand extends AbstractInstallCommand { - protected function createDatabase() + /** + * @return bool + */ + protected function isUpdate() { - $this->output->writeln('Initializing database...'); - return $this->runCommand('php vendor/bin/doctrine.php orm:schema-tool:create', 'Error generating database.'); + return false; } } diff --git a/module/CLI/src/Command/Install/UpdateCommand.php b/module/CLI/src/Command/Install/UpdateCommand.php index def28d06..425dc06a 100644 --- a/module/CLI/src/Command/Install/UpdateCommand.php +++ b/module/CLI/src/Command/Install/UpdateCommand.php @@ -3,7 +3,10 @@ namespace Shlinkio\Shlink\CLI\Command\Install; class UpdateCommand extends AbstractInstallCommand { - public function createDatabase() + /** + * @return bool + */ + protected function isUpdate() { return true; } From f9c56d7cb1d1a3fe881e7ba8cbd00bb42d7b91cb Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 3 Jul 2017 13:43:46 +0200 Subject: [PATCH 15/55] Added process to import previous configuration when updating shlink --- .../Install/AbstractInstallCommand.php | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/module/CLI/src/Command/Install/AbstractInstallCommand.php b/module/CLI/src/Command/Install/AbstractInstallCommand.php index 5c271231..f6341627 100644 --- a/module/CLI/src/Command/Install/AbstractInstallCommand.php +++ b/module/CLI/src/Command/Install/AbstractInstallCommand.php @@ -11,6 +11,7 @@ use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use Zend\Config\Writer\WriterInterface; @@ -24,6 +25,7 @@ abstract class AbstractInstallCommand extends Command 'SQLite' => 'pdo_sqlite', ]; const SUPPORTED_LANGUAGES = ['en', 'es']; + const GENERATED_CONFIG_PATH = 'config/params/generated_config.php'; /** * @var InputInterface @@ -86,9 +88,15 @@ abstract class AbstractInstallCommand extends Command ' Failed! You will have to manually delete the data/cache/app_config.php file to get' . ' new config applied.' ); + return; } } + // If running update command, ask the user to import previous config + if ($this->isUpdate()) { + $this->importConfig(); + } + // Ask for custom config params $params['DATABASE'] = $this->askDatabase(); $params['URL_SHORTENER'] = $this->askUrlShortener(); @@ -97,7 +105,7 @@ abstract class AbstractInstallCommand extends Command // Generate config params files $config = $this->buildAppConfig($params); - $this->configWriter->toFile('config/params/generated_config.php', $config, false); + $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config, false); $output->writeln(['Custom configuration properly generated!', '']); // If current command is not update, generate database @@ -124,6 +132,35 @@ abstract class AbstractInstallCommand extends Command } } + protected function importConfig() + { + // Ask the user if he/she wants to import an older configuration + $importConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( + 'Do you want to import previous configuration? (Y/n): ' + )); + if (! $importConfig) { + return; + } + + // Ask the user for the older shlink path + $keepAsking = true; + do { + $installationPath = $this->ask('Previous shlink installation path from which to import config'); + $configFile = $installationPath . '/' . self::GENERATED_CONFIG_PATH; + $configExists = file_exists($configFile); + + if (! $configExists) { + $keepAsking = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( + 'Provided path does not seem to be a valid shlink root path. ' + . 'Do you want to try another path? (Y/n): ' + )); + } + } while (! $configExists && $keepAsking); + + // Read the config file + $previousConfig = include $configFile; + } + protected function askDatabase() { $params = []; From e7f7cbcaac8acae8c1a430c196a51b367f61bd0f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 3 Jul 2017 20:46:35 +0200 Subject: [PATCH 16/55] Improved installation command, reducing duplication and moving serialization logic to specific model --- bin/update | 4 +- .../Install/AbstractInstallCommand.php | 368 ------------------ .../src/Command/Install/InstallCommand.php | 354 ++++++++++++++++- .../CLI/src/Command/Install/UpdateCommand.php | 13 - .../CLI/src/Model/CustomizableAppConfig.php | 190 +++++++++ .../Command/Install/InstallCommandTest.php | 1 + module/Common/src/Util/StringUtilsTrait.php | 2 +- 7 files changed, 546 insertions(+), 386 deletions(-) delete mode 100644 module/CLI/src/Command/Install/AbstractInstallCommand.php delete mode 100644 module/CLI/src/Command/Install/UpdateCommand.php create mode 100644 module/CLI/src/Model/CustomizableAppConfig.php diff --git a/bin/update b/bin/update index 86b7efc9..a10ef3f1 100755 --- a/bin/update +++ b/bin/update @@ -1,6 +1,6 @@ #!/usr/bin/env php add($command); $app->setDefaultCommand($command->getName()); $app->run(); diff --git a/module/CLI/src/Command/Install/AbstractInstallCommand.php b/module/CLI/src/Command/Install/AbstractInstallCommand.php deleted file mode 100644 index f6341627..00000000 --- a/module/CLI/src/Command/Install/AbstractInstallCommand.php +++ /dev/null @@ -1,368 +0,0 @@ - 'pdo_mysql', - 'PostgreSQL' => 'pdo_pgsql', - 'SQLite' => 'pdo_sqlite', - ]; - const SUPPORTED_LANGUAGES = ['en', 'es']; - const GENERATED_CONFIG_PATH = 'config/params/generated_config.php'; - - /** - * @var InputInterface - */ - protected $input; - /** - * @var OutputInterface - */ - protected $output; - /** - * @var QuestionHelper - */ - private $questionHelper; - /** - * @var ProcessHelper - */ - private $processHelper; - /** - * @var WriterInterface - */ - private $configWriter; - - /** - * InstallCommand constructor. - * @param WriterInterface $configWriter - * @throws LogicException - */ - public function __construct(WriterInterface $configWriter) - { - parent::__construct(); - $this->configWriter = $configWriter; - } - - public function configure() - { - $this->setName('shlink:install') - ->setDescription('Installs Shlink'); - } - - public function execute(InputInterface $input, OutputInterface $output) - { - $this->input = $input; - $this->output = $output; - $this->questionHelper = $this->getHelper('question'); - $this->processHelper = $this->getHelper('process'); - $params = []; - - $output->writeln([ - 'Welcome to Shlink!!', - 'This will guide you through the installation process.', - ]); - - // Check if a cached config file exists and drop it if so - if (file_exists('data/cache/app_config.php')) { - $output->write('Deleting old cached config...'); - if (unlink('data/cache/app_config.php')) { - $output->writeln(' Success'); - } else { - $output->writeln( - ' Failed! You will have to manually delete the data/cache/app_config.php file to get' - . ' new config applied.' - ); - return; - } - } - - // If running update command, ask the user to import previous config - if ($this->isUpdate()) { - $this->importConfig(); - } - - // Ask for custom config params - $params['DATABASE'] = $this->askDatabase(); - $params['URL_SHORTENER'] = $this->askUrlShortener(); - $params['LANGUAGE'] = $this->askLanguage(); - $params['APP'] = $this->askApplication(); - - // Generate config params files - $config = $this->buildAppConfig($params); - $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config, false); - $output->writeln(['Custom configuration properly generated!', '']); - - // If current command is not update, generate database - if (! $this->isUpdate()) { - $this->output->writeln('Initializing database...'); - if (! $this->runCommand( - 'php vendor/bin/doctrine.php orm:schema-tool:create', - 'Error generating database.' - )) { - return; - } - } - - // Run database migrations - $output->writeln('Updating database...'); - if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) { - return; - } - - // Generate proxies - $output->writeln('Generating proxies...'); - if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) { - return; - } - } - - protected function importConfig() - { - // Ask the user if he/she wants to import an older configuration - $importConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( - 'Do you want to import previous configuration? (Y/n): ' - )); - if (! $importConfig) { - return; - } - - // Ask the user for the older shlink path - $keepAsking = true; - do { - $installationPath = $this->ask('Previous shlink installation path from which to import config'); - $configFile = $installationPath . '/' . self::GENERATED_CONFIG_PATH; - $configExists = file_exists($configFile); - - if (! $configExists) { - $keepAsking = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( - 'Provided path does not seem to be a valid shlink root path. ' - . 'Do you want to try another path? (Y/n): ' - )); - } - } while (! $configExists && $keepAsking); - - // Read the config file - $previousConfig = include $configFile; - } - - protected function askDatabase() - { - $params = []; - $this->printTitle('DATABASE'); - - // Select database type - $databases = array_keys(self::DATABASE_DRIVERS); - $dbType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select database type (defaults to ' . $databases[0] . '):', - $databases, - 0 - )); - $params['DRIVER'] = self::DATABASE_DRIVERS[$dbType]; - - // Ask for connection params if database is not SQLite - if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) { - $params['NAME'] = $this->ask('Database name', 'shlink'); - $params['USER'] = $this->ask('Database username'); - $params['PASSWORD'] = $this->ask('Database password'); - $params['HOST'] = $this->ask('Database host', 'localhost'); - $params['PORT'] = $this->ask('Database port', $this->getDefaultDbPort($params['DRIVER'])); - } - - return $params; - } - - protected function getDefaultDbPort($driver) - { - return $driver === 'pdo_mysql' ? '3306' : '5432'; - } - - protected function askUrlShortener() - { - $this->printTitle('URL SHORTENER'); - - // Ask for URL shortener params - return [ - 'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select schema for generated short URLs (defaults to http):', - ['http', 'https'], - 0 - )), - 'HOSTNAME' => $this->ask('Hostname for generated URLs'), - 'CHARS' => $this->ask( - 'Character set for generated short codes (leave empty to autogenerate one)', - null, - true - ) ?: str_shuffle(UrlShortener::DEFAULT_CHARS) - ]; - } - - protected function askLanguage() - { - $this->printTitle('LANGUAGE'); - - return [ - 'DEFAULT' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select default language for the application in general (defaults to ' - . self::SUPPORTED_LANGUAGES[0] . '):', - self::SUPPORTED_LANGUAGES, - 0 - )), - 'CLI' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select default language for CLI executions (defaults to ' - . self::SUPPORTED_LANGUAGES[0] . '):', - self::SUPPORTED_LANGUAGES, - 0 - )), - ]; - } - - protected function askApplication() - { - $this->printTitle('APPLICATION'); - - return [ - 'SECRET' => $this->ask( - 'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)', - null, - true - ) ?: $this->generateRandomString(32), - ]; - } - - /** - * @param string $text - */ - protected function printTitle($text) - { - $text = trim($text); - $length = strlen($text) + 4; - $header = str_repeat('*', $length); - - $this->output->writeln([ - '', - '' . $header . '', - '* ' . strtoupper($text) . ' *', - '' . $header . '', - ]); - } - - /** - * @param string $text - * @param string|null $default - * @param bool $allowEmpty - * @return string - * @throws RuntimeException - */ - protected function ask($text, $default = null, $allowEmpty = false) - { - if ($default !== null) { - $text .= ' (defaults to ' . $default . ')'; - } - do { - $value = $this->questionHelper->ask($this->input, $this->output, new Question( - '' . $text . ': ', - $default - )); - if (empty($value) && ! $allowEmpty) { - $this->output->writeln('Value can\'t be empty'); - } - } while (empty($value) && $default === null && ! $allowEmpty); - - return $value; - } - - /** - * @param array $params - * @return array - */ - protected function buildAppConfig(array $params) - { - // Build simple config - $config = [ - 'app_options' => [ - 'secret_key' => $params['APP']['SECRET'], - ], - 'entity_manager' => [ - 'connection' => [ - 'driver' => $params['DATABASE']['DRIVER'], - ], - ], - 'translator' => [ - 'locale' => $params['LANGUAGE']['DEFAULT'], - ], - 'cli' => [ - 'locale' => $params['LANGUAGE']['CLI'], - ], - 'url_shortener' => [ - 'domain' => [ - 'schema' => $params['URL_SHORTENER']['SCHEMA'], - 'hostname' => $params['URL_SHORTENER']['HOSTNAME'], - ], - 'shortcode_chars' => $params['URL_SHORTENER']['CHARS'], - ], - ]; - - // Build dynamic database config - if ($params['DATABASE']['DRIVER'] === 'pdo_sqlite') { - $config['entity_manager']['connection']['path'] = 'data/database.sqlite'; - } else { - $config['entity_manager']['connection']['user'] = $params['DATABASE']['USER']; - $config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD']; - $config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME']; - $config['entity_manager']['connection']['host'] = $params['DATABASE']['HOST']; - $config['entity_manager']['connection']['port'] = $params['DATABASE']['PORT']; - - if ($params['DATABASE']['DRIVER'] === 'pdo_mysql') { - $config['entity_manager']['connection']['driverOptions'] = [ - \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', - ]; - } - } - - return $config; - } - - /** - * @param string $command - * @param string $errorMessage - * @return bool - */ - protected function runCommand($command, $errorMessage) - { - $process = $this->processHelper->run($this->output, $command); - if ($process->isSuccessful()) { - $this->output->writeln(' Success!'); - return true; - } - - if ($this->output->isVerbose()) { - return false; - } - - $this->output->writeln( - ' ' . $errorMessage . ' Run this command with -vvv to see specific error info.' - ); - return false; - } - - /** - * @return bool - */ - abstract protected function isUpdate(); -} diff --git a/module/CLI/src/Command/Install/InstallCommand.php b/module/CLI/src/Command/Install/InstallCommand.php index e414bbe0..2ff9e9da 100644 --- a/module/CLI/src/Command/Install/InstallCommand.php +++ b/module/CLI/src/Command/Install/InstallCommand.php @@ -1,13 +1,363 @@ 'pdo_mysql', + 'PostgreSQL' => 'pdo_pgsql', + 'SQLite' => 'pdo_sqlite', + ]; + const SUPPORTED_LANGUAGES = ['en', 'es']; + const GENERATED_CONFIG_PATH = 'config/params/generated_config.php'; + /** + * @var InputInterface + */ + private $input; + /** + * @var OutputInterface + */ + private $output; + /** + * @var QuestionHelper + */ + private $questionHelper; + /** + * @var ProcessHelper + */ + private $processHelper; + /** + * @var WriterInterface + */ + private $configWriter; + /** + * @var bool + */ + private $isUpdate; + + /** + * InstallCommand constructor. + * @param WriterInterface $configWriter + * @param bool $isUpdate + * @throws LogicException + */ + public function __construct(WriterInterface $configWriter, $isUpdate = false) + { + parent::__construct(); + $this->configWriter = $configWriter; + $this->isUpdate = $isUpdate; + } + + public function configure() + { + $this->setName('shlink:install') + ->setDescription('Installs Shlink'); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + $this->questionHelper = $this->getHelper('question'); + $this->processHelper = $this->getHelper('process'); + + $output->writeln([ + 'Welcome to Shlink!!', + 'This will guide you through the installation process.', + ]); + + // Check if a cached config file exists and drop it if so + if (file_exists('data/cache/app_config.php')) { + $output->write('Deleting old cached config...'); + if (unlink('data/cache/app_config.php')) { + $output->writeln(' Success'); + } else { + $output->writeln( + ' Failed! You will have to manually delete the data/cache/app_config.php file to get' + . ' new config applied.' + ); + return; + } + } + + // If running update command, ask the user to import previous config + $config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig(); + + // Ask for custom config params + $this->askDatabase($config); + $this->askUrlShortener($config); + $this->askLanguage($config); + $this->askApplication($config); + + // Generate config params files + $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false); + $output->writeln(['Custom configuration properly generated!', '']); + + // If current command is not update, generate database + if (! $this->isUpdate) { + $this->output->writeln('Initializing database...'); + if (! $this->runCommand( + 'php vendor/bin/doctrine.php orm:schema-tool:create', + 'Error generating database.' + )) { + return; + } + } + + // Run database migrations + $output->writeln('Updating database...'); + if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) { + return; + } + + // Generate proxies + $output->writeln('Generating proxies...'); + if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) { + return; + } + } + + /** + * @return CustomizableAppConfig + * @throws RuntimeException + */ + protected function importConfig() + { + $config = new CustomizableAppConfig(); + + // Ask the user if he/she wants to import an older configuration + $importConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( + 'Do you want to import previous configuration? (Y/n): ' + )); + if (! $importConfig) { + return $config; + } + + // Ask the user for the older shlink path + $keepAsking = true; + do { + $installationPath = $this->ask('Previous shlink installation path from which to import config'); + $configFile = $installationPath . '/' . self::GENERATED_CONFIG_PATH; + $configExists = file_exists($configFile); + + if (! $configExists) { + $keepAsking = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( + 'Provided path does not seem to be a valid shlink root path. ' + . 'Do you want to try another path? (Y/n): ' + )); + } + } while (! $configExists && $keepAsking); + + // If after some retries the user has chosen not to test another path, return + if (! $configExists) { + return $config; + } + + // Read the config file + $config->exchangeArray(include $configFile); + return $config; + } + + protected function askDatabase(CustomizableAppConfig $config) + { + $this->printTitle('DATABASE'); + + if ($config->hasDatabase()) { + $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( + 'Do you want to keep imported database config? (Y/n): ' + )); + if ($keepConfig) { + return; + } + } + + // Select database type + $params = []; + $databases = array_keys(self::DATABASE_DRIVERS); + $dbType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( + 'Select database type (defaults to ' . $databases[0] . '):', + $databases, + 0 + )); + $params['DRIVER'] = self::DATABASE_DRIVERS[$dbType]; + + // Ask for connection params if database is not SQLite + if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) { + $params['NAME'] = $this->ask('Database name', 'shlink'); + $params['USER'] = $this->ask('Database username'); + $params['PASSWORD'] = $this->ask('Database password'); + $params['HOST'] = $this->ask('Database host', 'localhost'); + $params['PORT'] = $this->ask('Database port', $this->getDefaultDbPort($params['DRIVER'])); + } + + $config->setDatabase($params); + } + + protected function getDefaultDbPort($driver) + { + return $driver === 'pdo_mysql' ? '3306' : '5432'; + } + + protected function askUrlShortener(CustomizableAppConfig $config) + { + $this->printTitle('URL SHORTENER'); + + if ($config->hasUrlShortener()) { + $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( + 'Do you want to keep imported URL shortener config? (Y/n): ' + )); + if ($keepConfig) { + return; + } + } + + // Ask for URL shortener params + $config->setUrlShortener([ + 'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( + 'Select schema for generated short URLs (defaults to http):', + ['http', 'https'], + 0 + )), + 'HOSTNAME' => $this->ask('Hostname for generated URLs'), + 'CHARS' => $this->ask( + 'Character set for generated short codes (leave empty to autogenerate one)', + null, + true + ) ?: str_shuffle(UrlShortener::DEFAULT_CHARS) + ]); + } + + protected function askLanguage(CustomizableAppConfig $config) + { + $this->printTitle('LANGUAGE'); + + if ($config->hasLanguage()) { + $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( + 'Do you want to keep imported language? (Y/n): ' + )); + if ($keepConfig) { + return; + } + } + + $config->setLanguage([ + 'DEFAULT' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( + 'Select default language for the application in general (defaults to ' + . self::SUPPORTED_LANGUAGES[0] . '):', + self::SUPPORTED_LANGUAGES, + 0 + )), + 'CLI' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( + 'Select default language for CLI executions (defaults to ' + . self::SUPPORTED_LANGUAGES[0] . '):', + self::SUPPORTED_LANGUAGES, + 0 + )), + ]); + } + + protected function askApplication(CustomizableAppConfig $config) + { + $this->printTitle('APPLICATION'); + + if ($config->hasApp()) { + $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( + 'Do you want to keep imported application config? (Y/n): ' + )); + if ($keepConfig) { + return; + } + } + + $config->setApp([ + 'SECRET' => $this->ask( + 'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)', + null, + true + ) ?: $this->generateRandomString(32), + ]); + } + + /** + * @param string $text + */ + protected function printTitle($text) + { + $text = trim($text); + $length = strlen($text) + 4; + $header = str_repeat('*', $length); + + $this->output->writeln([ + '', + '' . $header . '', + '* ' . strtoupper($text) . ' *', + '' . $header . '', + ]); + } + + /** + * @param string $text + * @param string|null $default + * @param bool $allowEmpty + * @return string + * @throws RuntimeException + */ + protected function ask($text, $default = null, $allowEmpty = false) + { + if ($default !== null) { + $text .= ' (defaults to ' . $default . ')'; + } + do { + $value = $this->questionHelper->ask($this->input, $this->output, new Question( + '' . $text . ': ', + $default + )); + if (empty($value) && ! $allowEmpty) { + $this->output->writeln('Value can\'t be empty'); + } + } while (empty($value) && $default === null && ! $allowEmpty); + + return $value; + } + + /** + * @param string $command + * @param string $errorMessage * @return bool */ - protected function isUpdate() + protected function runCommand($command, $errorMessage) { + $process = $this->processHelper->run($this->output, $command); + if ($process->isSuccessful()) { + $this->output->writeln(' Success!'); + return true; + } + + if ($this->output->isVerbose()) { + return false; + } + + $this->output->writeln( + ' ' . $errorMessage . ' Run this command with -vvv to see specific error info.' + ); return false; } } diff --git a/module/CLI/src/Command/Install/UpdateCommand.php b/module/CLI/src/Command/Install/UpdateCommand.php deleted file mode 100644 index 425dc06a..00000000 --- a/module/CLI/src/Command/Install/UpdateCommand.php +++ /dev/null @@ -1,13 +0,0 @@ -database; + } + + /** + * @param array $database + * @return $this + */ + public function setDatabase(array $database) + { + $this->database = $database; + return $this; + } + + /** + * @return bool + */ + public function hasDatabase() + { + return ! empty($this->database); + } + + /** + * @return array + */ + public function getUrlShortener() + { + return $this->urlShortener; + } + + /** + * @param array $urlShortener + * @return $this + */ + public function setUrlShortener(array $urlShortener) + { + $this->urlShortener = $urlShortener; + return $this; + } + + /** + * @return bool + */ + public function hasUrlShortener() + { + return ! empty($this->urlShortener); + } + + /** + * @return array + */ + public function getLanguage() + { + return $this->language; + } + + /** + * @param array $language + * @return $this + */ + public function setLanguage(array $language) + { + $this->language = $language; + return $this; + } + + /** + * @return bool + */ + public function hasLanguage() + { + return ! empty($this->language); + } + + /** + * @return array + */ + public function getApp() + { + return $this->app; + } + + /** + * @param array $app + * @return $this + */ + public function setApp(array $app) + { + $this->app = $app; + return $this; + } + + /** + * @return bool + */ + public function hasApp() + { + return ! empty($this->app); + } + + /** + * Exchange internal values from provided array + * + * @param array $array + * @return void + */ + public function exchangeArray(array $array) + { + + } + + /** + * Return an array representation of the object + * + * @return array + */ + public function getArrayCopy() + { + $config = [ + 'app_options' => [ + 'secret_key' => $this->app['SECRET'], + ], + 'entity_manager' => [ + 'connection' => [ + 'driver' => $this->database['DRIVER'], + ], + ], + 'translator' => [ + 'locale' => $this->language['DEFAULT'], + ], + 'cli' => [ + 'locale' => $this->language['CLI'], + ], + 'url_shortener' => [ + 'domain' => [ + 'schema' => $this->urlShortener['SCHEMA'], + 'hostname' => $this->urlShortener['HOSTNAME'], + ], + 'shortcode_chars' => $this->urlShortener['CHARS'], + ], + ]; + + // Build dynamic database config based on selected driver + if ($this->database['DRIVER'] === 'pdo_sqlite') { + $config['entity_manager']['connection']['path'] = 'data/database.sqlite'; + } else { + $config['entity_manager']['connection']['user'] = $this->database['USER']; + $config['entity_manager']['connection']['password'] = $this->database['PASSWORD']; + $config['entity_manager']['connection']['dbname'] = $this->database['NAME']; + $config['entity_manager']['connection']['host'] = $this->database['HOST']; + $config['entity_manager']['connection']['port'] = $this->database['PORT']; + + if ($this->database['DRIVER'] === 'pdo_mysql') { + $config['entity_manager']['connection']['driverOptions'] = [ + \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', + ]; + } + } + + return $config; + } +} diff --git a/module/CLI/test/Command/Install/InstallCommandTest.php b/module/CLI/test/Command/Install/InstallCommandTest.php index ebfb7e29..4b49d7ec 100644 --- a/module/CLI/test/Command/Install/InstallCommandTest.php +++ b/module/CLI/test/Command/Install/InstallCommandTest.php @@ -104,6 +104,7 @@ CLI_INPUT 'shortcode_chars' => 'abc123BCA', ], ], false)->shouldBeCalledTimes(1); + $this->commandTester->execute([ 'command' => 'shlink:install', ]); diff --git a/module/Common/src/Util/StringUtilsTrait.php b/module/Common/src/Util/StringUtilsTrait.php index 648dbfcb..9680aa49 100644 --- a/module/Common/src/Util/StringUtilsTrait.php +++ b/module/Common/src/Util/StringUtilsTrait.php @@ -9,7 +9,7 @@ trait StringUtilsTrait $charactersLength = strlen($characters); $randomString = ''; for ($i = 0; $i < $length; $i++) { - $randomString .= $characters[rand(0, $charactersLength - 1)]; + $randomString .= $characters[mt_rand(0, $charactersLength - 1)]; } return $randomString; From cc688fa3ce05fc61a6aec9362b4b824ac3b30c36 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 4 Jul 2017 19:48:53 +0200 Subject: [PATCH 17/55] Implemented method to deserialize customizable config --- .../CLI/src/Model/CustomizableAppConfig.php | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/module/CLI/src/Model/CustomizableAppConfig.php b/module/CLI/src/Model/CustomizableAppConfig.php index b1964979..4b7043c0 100644 --- a/module/CLI/src/Model/CustomizableAppConfig.php +++ b/module/CLI/src/Model/CustomizableAppConfig.php @@ -134,7 +134,50 @@ final class CustomizableAppConfig implements ArraySerializableInterface */ public function exchangeArray(array $array) { + if (isset($array['app_options'], $array['app_options']['secret_key'])) { + $this->setApp([ + 'SECRET' => $array['app_options']['secret_key'], + ]); + } + if (isset($array['entity_manager'], $array['entity_manager']['connection'])) { + $this->deserializeDatabase($array['entity_manager']['connection']); + } + + if (isset($array['translator'], $array['translator']['locale'], $array['cli'], $array['cli']['locale'])) { + $this->setLanguage([ + 'DEFAULT' => $array['translator']['locale'], + 'CLI' => $array['cli']['locale'], + ]); + } + + if (isset($array['url_shortener'])) { + $urlShortener = $array['url_shortener']; + $this->setUrlShortener([ + 'SCHEMA' => $urlShortener['domain']['schema'], + 'HOSTNAME' => $urlShortener['domain']['hostname'], + 'CHARS' => $urlShortener['shortcode_chars'], + ]); + } + } + + private function deserializeDatabase(array $conn) + { + if (! isset($conn['driver'])) { + return; + } + $driver = $conn['driver']; + + $params = ['DRIVER' => $driver]; + if ($driver !== 'pdo_sqlite') { + $params['USER'] = $conn['user']; + $params['PASSWORD'] = $conn['password']; + $params['NAME'] = $conn['dbname']; + $params['HOST'] = $conn['host']; + $params['PORT'] = $conn['port']; + } + + $this->setDatabase($params); } /** From 102f5c4e12373ea968ce7305c5cff459c4e282a6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 4 Jul 2017 20:01:42 +0200 Subject: [PATCH 18/55] Updated instalation script to import sqlte database file when importing the rets of the config --- .../CLI/src/Command/Install/InstallCommand.php | 16 ++++++++++++++-- module/CLI/src/Model/CustomizableAppConfig.php | 4 +++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/module/CLI/src/Command/Install/InstallCommand.php b/module/CLI/src/Command/Install/InstallCommand.php index 2ff9e9da..d036c78e 100644 --- a/module/CLI/src/Command/Install/InstallCommand.php +++ b/module/CLI/src/Command/Install/InstallCommand.php @@ -44,6 +44,10 @@ class InstallCommand extends Command * @var ProcessHelper */ private $processHelper; + /** + * @var string + */ + private $importedInstallationPath; /** * @var WriterInterface */ @@ -154,8 +158,8 @@ class InstallCommand extends Command // Ask the user for the older shlink path $keepAsking = true; do { - $installationPath = $this->ask('Previous shlink installation path from which to import config'); - $configFile = $installationPath . '/' . self::GENERATED_CONFIG_PATH; + $this->importedInstallationPath = $this->ask('Previous shlink installation path from which to import config'); + $configFile = $this->importedInstallationPath . '/' . self::GENERATED_CONFIG_PATH; $configExists = file_exists($configFile); if (! $configExists) { @@ -185,6 +189,14 @@ class InstallCommand extends Command 'Do you want to keep imported database config? (Y/n): ' )); if ($keepConfig) { + // If the user selected to keep DB config and is configured to use sqlite, copy DB file + if ($config->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) { + copy( + $this->importedInstallationPath . '/' . CustomizableAppConfig::SQLITE_DB_PATH, + CustomizableAppConfig::SQLITE_DB_PATH + ); + } + return; } } diff --git a/module/CLI/src/Model/CustomizableAppConfig.php b/module/CLI/src/Model/CustomizableAppConfig.php index 4b7043c0..a1f48bbd 100644 --- a/module/CLI/src/Model/CustomizableAppConfig.php +++ b/module/CLI/src/Model/CustomizableAppConfig.php @@ -5,6 +5,8 @@ use Zend\Stdlib\ArraySerializableInterface; final class CustomizableAppConfig implements ArraySerializableInterface { + const SQLITE_DB_PATH = 'data/database.sqlite'; + /** * @var array */ @@ -213,7 +215,7 @@ final class CustomizableAppConfig implements ArraySerializableInterface // Build dynamic database config based on selected driver if ($this->database['DRIVER'] === 'pdo_sqlite') { - $config['entity_manager']['connection']['path'] = 'data/database.sqlite'; + $config['entity_manager']['connection']['path'] = self::SQLITE_DB_PATH; } else { $config['entity_manager']['connection']['user'] = $this->database['USER']; $config['entity_manager']['connection']['password'] = $this->database['PASSWORD']; From dcc09975a9ee417d67442ea657759af3f70f551c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 4 Jul 2017 20:14:22 +0200 Subject: [PATCH 19/55] Abstracted filesystem manipulation in InstallCommand --- bin/install | 3 +- bin/update | 3 +- .../src/Command/Install/InstallCommand.php | 28 ++++++++++++++----- .../Command/Install/InstallCommandTest.php | 10 ++++++- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/bin/install b/bin/install index b563f395..2a6bb2e1 100755 --- a/bin/install +++ b/bin/install @@ -2,6 +2,7 @@ add($command); $app->setDefaultCommand($command->getName()); $app->run(); diff --git a/bin/update b/bin/update index a10ef3f1..223939b2 100755 --- a/bin/update +++ b/bin/update @@ -2,6 +2,7 @@ add($command); $app->setDefaultCommand($command->getName()); $app->run(); diff --git a/module/CLI/src/Command/Install/InstallCommand.php b/module/CLI/src/Command/Install/InstallCommand.php index d036c78e..e30785af 100644 --- a/module/CLI/src/Command/Install/InstallCommand.php +++ b/module/CLI/src/Command/Install/InstallCommand.php @@ -14,6 +14,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Filesystem; use Zend\Config\Writer\WriterInterface; class InstallCommand extends Command @@ -56,18 +58,24 @@ class InstallCommand extends Command * @var bool */ private $isUpdate; + /** + * @var Filesystem + */ + private $filesystem; /** * InstallCommand constructor. * @param WriterInterface $configWriter + * @param Filesystem $filesystem * @param bool $isUpdate * @throws LogicException */ - public function __construct(WriterInterface $configWriter, $isUpdate = false) + public function __construct(WriterInterface $configWriter, Filesystem $filesystem, $isUpdate = false) { parent::__construct(); $this->configWriter = $configWriter; $this->isUpdate = $isUpdate; + $this->filesystem = $filesystem; } public function configure() @@ -89,15 +97,19 @@ class InstallCommand extends Command ]); // Check if a cached config file exists and drop it if so - if (file_exists('data/cache/app_config.php')) { + if ($this->filesystem->exists('data/cache/app_config.php')) { $output->write('Deleting old cached config...'); - if (unlink('data/cache/app_config.php')) { + try { + $this->filesystem->remove('data/cache/app_config.php'); $output->writeln(' Success'); - } else { + } catch (IOException $e) { $output->writeln( ' Failed! You will have to manually delete the data/cache/app_config.php file to get' . ' new config applied.' ); + if ($output->isVerbose()) { + $this->getApplication()->renderException($e, $output); + } return; } } @@ -158,9 +170,11 @@ class InstallCommand extends Command // Ask the user for the older shlink path $keepAsking = true; do { - $this->importedInstallationPath = $this->ask('Previous shlink installation path from which to import config'); + $this->importedInstallationPath = $this->ask( + 'Previous shlink installation path from which to import config' + ); $configFile = $this->importedInstallationPath . '/' . self::GENERATED_CONFIG_PATH; - $configExists = file_exists($configFile); + $configExists = $this->filesystem->exists($configFile); if (! $configExists) { $keepAsking = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( @@ -191,7 +205,7 @@ class InstallCommand extends Command if ($keepConfig) { // If the user selected to keep DB config and is configured to use sqlite, copy DB file if ($config->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) { - copy( + $this->filesystem->copy( $this->importedInstallationPath . '/' . CustomizableAppConfig::SQLITE_DB_PATH, CustomizableAppConfig::SQLITE_DB_PATH ); diff --git a/module/CLI/test/Command/Install/InstallCommandTest.php b/module/CLI/test/Command/Install/InstallCommandTest.php index 4b49d7ec..8c76299f 100644 --- a/module/CLI/test/Command/Install/InstallCommandTest.php +++ b/module/CLI/test/Command/Install/InstallCommandTest.php @@ -8,6 +8,7 @@ use Shlinkio\Shlink\CLI\Command\Install\InstallCommand; use Symfony\Component\Console\Application; use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Process\Process; use Zend\Config\Writer\WriterInterface; @@ -21,6 +22,10 @@ class InstallCommandTest extends TestCase * @var ObjectProphecy */ protected $configWriter; + /** + * @var ObjectProphecy + */ + protected $filesystem; public function setUp() { @@ -31,13 +36,16 @@ class InstallCommandTest extends TestCase $processHelper->setHelperSet(Argument::any())->willReturn(null); $processHelper->run(Argument::cetera())->willReturn($processMock->reveal()); + $this->filesystem = $this->prophesize(Filesystem::class); + $this->filesystem->exists(Argument::cetera())->willReturn(false); + $app = new Application(); $helperSet = $app->getHelperSet(); $helperSet->set($processHelper->reveal()); $app->setHelperSet($helperSet); $this->configWriter = $this->prophesize(WriterInterface::class); - $command = new InstallCommand($this->configWriter->reveal()); + $command = new InstallCommand($this->configWriter->reveal(), $this->filesystem->reveal()); $app->add($command); $questionHelper = $command->getHelper('question'); From 2368b634e3c81a4482c07d6b2085a097843790da Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 5 Jul 2017 18:12:03 +0200 Subject: [PATCH 20/55] Moved command and app creation logic to a factory for install scripts --- .phpstorm.meta.php | 4 ++ bin/install | 14 +++---- bin/update | 14 +++---- .../src/Factory/InstallApplicationFactory.php | 41 +++++++++++++++++++ 4 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 module/CLI/src/Factory/InstallApplicationFactory.php diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 5acf91bd..5dc306bc 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -2,6 +2,7 @@ namespace PHPSTORM_META; use Psr\Container\ContainerInterface; +use Zend\ServiceManager\ServiceManager; /** * PhpStorm Container Interop code completion @@ -16,4 +17,7 @@ $STATIC_METHOD_TYPES = [ ContainerInterface::get('') => [ '' == '@', ], + ServiceManager::build('') => [ + '' == '@', + ], ]; diff --git a/bin/install b/bin/install index 2a6bb2e1..b4147e10 100755 --- a/bin/install +++ b/bin/install @@ -1,16 +1,14 @@ #!/usr/bin/env php add($command); -$app->setDefaultCommand($command->getName()); -$app->run(); +$container = new ServiceManager(['factories' => [ + Application::class => InstallApplicationFactory::class, +]]); +$container->build(Application::class)->run(); diff --git a/bin/update b/bin/update index 223939b2..13c2a684 100755 --- a/bin/update +++ b/bin/update @@ -1,16 +1,14 @@ #!/usr/bin/env php add($command); -$app->setDefaultCommand($command->getName()); -$app->run(); +$container = new ServiceManager(['factories' => [ + Application::class => InstallApplicationFactory::class, +]]); +$container->build(Application::class, ['isUpdate' => true])->run(); diff --git a/module/CLI/src/Factory/InstallApplicationFactory.php b/module/CLI/src/Factory/InstallApplicationFactory.php new file mode 100644 index 00000000..2c5b5236 --- /dev/null +++ b/module/CLI/src/Factory/InstallApplicationFactory.php @@ -0,0 +1,41 @@ +add($command); + $app->setDefaultCommand($command->getName()); + + return $app; + } +} From 479e69447858d404f4de88991b865563fa2e87b7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 5 Jul 2017 20:04:44 +0200 Subject: [PATCH 21/55] Moved all configuration customization steps to individual plugins --- bin/cli | 5 +- bin/install | 5 + bin/update | 5 + .../src/Command/Install/InstallCommand.php | 207 +++--------------- .../src/Factory/InstallApplicationFactory.php | 18 +- .../Install/ConfigCustomizerPluginManager.php | 10 + ...ConfigCustomizerPluginManagerInterface.php | 8 + .../Plugin/AbstractConfigCustomizerPlugin.php | 66 ++++++ .../ApplicationConfigCustomizerPlugin.php | 46 ++++ .../ConfigCustomizerPluginInterface.php | 17 ++ .../Plugin/DatabaseConfigCustomizerPlugin.php | 101 +++++++++ .../DefaultConfigCustomizerPluginFactory.php | 31 +++ .../Plugin/LanguageConfigCustomizerPlugin.php | 52 +++++ .../UrlShortenerConfigCustomizerPlugin.php | 53 +++++ .../CLI/src/Model/CustomizableAppConfig.php | 30 +++ 15 files changed, 475 insertions(+), 179 deletions(-) create mode 100644 module/CLI/src/Install/ConfigCustomizerPluginManager.php create mode 100644 module/CLI/src/Install/ConfigCustomizerPluginManagerInterface.php create mode 100644 module/CLI/src/Install/Plugin/AbstractConfigCustomizerPlugin.php create mode 100644 module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php create mode 100644 module/CLI/src/Install/Plugin/ConfigCustomizerPluginInterface.php create mode 100644 module/CLI/src/Install/Plugin/DatabaseConfigCustomizerPlugin.php create mode 100644 module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php create mode 100644 module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php create mode 100644 module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php diff --git a/bin/cli b/bin/cli index 66086b23..263df59e 100755 --- a/bin/cli +++ b/bin/cli @@ -5,7 +5,4 @@ use Symfony\Component\Console\Application as CliApp; /** @var ContainerInterface $container */ $container = include __DIR__ . '/../config/container.php'; - -/** @var CliApp $app */ -$app = $container->get(CliApp::class); -$app->run(); +$container->get(CliApp::class)->run(); diff --git a/bin/install b/bin/install index b4147e10..43a07cd3 100755 --- a/bin/install +++ b/bin/install @@ -2,6 +2,9 @@ [ Application::class => InstallApplicationFactory::class, + Filesystem::class => InvokableFactory::class, + QuestionHelper::class => InvokableFactory::class, ]]); $container->build(Application::class)->run(); diff --git a/bin/update b/bin/update index 13c2a684..164e20b0 100755 --- a/bin/update +++ b/bin/update @@ -2,6 +2,9 @@ [ Application::class => InstallApplicationFactory::class, + Filesystem::class => InvokableFactory::class, + QuestionHelper::class => InvokableFactory::class, ]]); $container->build(Application::class, ['isUpdate' => true])->run(); diff --git a/module/CLI/src/Command/Install/InstallCommand.php b/module/CLI/src/Command/Install/InstallCommand.php index e30785af..641fb552 100644 --- a/module/CLI/src/Command/Install/InstallCommand.php +++ b/module/CLI/src/Command/Install/InstallCommand.php @@ -1,9 +1,10 @@ 'pdo_mysql', - 'PostgreSQL' => 'pdo_pgsql', - 'SQLite' => 'pdo_sqlite', - ]; - const SUPPORTED_LANGUAGES = ['en', 'es']; const GENERATED_CONFIG_PATH = 'config/params/generated_config.php'; /** @@ -46,22 +38,22 @@ class InstallCommand extends Command * @var ProcessHelper */ private $processHelper; - /** - * @var string - */ - private $importedInstallationPath; /** * @var WriterInterface */ private $configWriter; - /** - * @var bool - */ - private $isUpdate; /** * @var Filesystem */ private $filesystem; + /** + * @var ConfigCustomizerPluginManagerInterface + */ + private $configCustomizers; + /** + * @var bool + */ + private $isUpdate; /** * InstallCommand constructor. @@ -70,18 +62,24 @@ class InstallCommand extends Command * @param bool $isUpdate * @throws LogicException */ - public function __construct(WriterInterface $configWriter, Filesystem $filesystem, $isUpdate = false) - { + public function __construct( + WriterInterface $configWriter, + Filesystem $filesystem, + ConfigCustomizerPluginManagerInterface $configCustomizers, + $isUpdate = false + ) { parent::__construct(); $this->configWriter = $configWriter; $this->isUpdate = $isUpdate; $this->filesystem = $filesystem; + $this->configCustomizers = $configCustomizers; } public function configure() { - $this->setName('shlink:install') - ->setDescription('Installs Shlink'); + $this + ->setName('shlink:install') + ->setDescription('Installs or updates Shlink'); } public function execute(InputInterface $input, OutputInterface $output) @@ -118,10 +116,16 @@ class InstallCommand extends Command $config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig(); // Ask for custom config params - $this->askDatabase($config); - $this->askUrlShortener($config); - $this->askLanguage($config); - $this->askApplication($config); + foreach ([ + Plugin\DatabaseConfigCustomizerPlugin::class, + Plugin\UrlShortenerConfigCustomizerPlugin::class, + Plugin\LanguageConfigCustomizerPlugin::class, + Plugin\ApplicationConfigCustomizerPlugin::class, + ] as $pluginName) { + /** @var Plugin\ConfigCustomizerPluginInterface $configCustomizer */ + $configCustomizer = $this->configCustomizers->get($pluginName); + $configCustomizer->process($input, $output, $config); + } // Generate config params files $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false); @@ -170,10 +174,10 @@ class InstallCommand extends Command // Ask the user for the older shlink path $keepAsking = true; do { - $this->importedInstallationPath = $this->ask( + $config->setImportedInstallationPath($this->ask( 'Previous shlink installation path from which to import config' - ); - $configFile = $this->importedInstallationPath . '/' . self::GENERATED_CONFIG_PATH; + )); + $configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH; $configExists = $this->filesystem->exists($configFile); if (! $configExists) { @@ -194,151 +198,6 @@ class InstallCommand extends Command return $config; } - protected function askDatabase(CustomizableAppConfig $config) - { - $this->printTitle('DATABASE'); - - if ($config->hasDatabase()) { - $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( - 'Do you want to keep imported database config? (Y/n): ' - )); - if ($keepConfig) { - // If the user selected to keep DB config and is configured to use sqlite, copy DB file - if ($config->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) { - $this->filesystem->copy( - $this->importedInstallationPath . '/' . CustomizableAppConfig::SQLITE_DB_PATH, - CustomizableAppConfig::SQLITE_DB_PATH - ); - } - - return; - } - } - - // Select database type - $params = []; - $databases = array_keys(self::DATABASE_DRIVERS); - $dbType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select database type (defaults to ' . $databases[0] . '):', - $databases, - 0 - )); - $params['DRIVER'] = self::DATABASE_DRIVERS[$dbType]; - - // Ask for connection params if database is not SQLite - if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) { - $params['NAME'] = $this->ask('Database name', 'shlink'); - $params['USER'] = $this->ask('Database username'); - $params['PASSWORD'] = $this->ask('Database password'); - $params['HOST'] = $this->ask('Database host', 'localhost'); - $params['PORT'] = $this->ask('Database port', $this->getDefaultDbPort($params['DRIVER'])); - } - - $config->setDatabase($params); - } - - protected function getDefaultDbPort($driver) - { - return $driver === 'pdo_mysql' ? '3306' : '5432'; - } - - protected function askUrlShortener(CustomizableAppConfig $config) - { - $this->printTitle('URL SHORTENER'); - - if ($config->hasUrlShortener()) { - $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( - 'Do you want to keep imported URL shortener config? (Y/n): ' - )); - if ($keepConfig) { - return; - } - } - - // Ask for URL shortener params - $config->setUrlShortener([ - 'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select schema for generated short URLs (defaults to http):', - ['http', 'https'], - 0 - )), - 'HOSTNAME' => $this->ask('Hostname for generated URLs'), - 'CHARS' => $this->ask( - 'Character set for generated short codes (leave empty to autogenerate one)', - null, - true - ) ?: str_shuffle(UrlShortener::DEFAULT_CHARS) - ]); - } - - protected function askLanguage(CustomizableAppConfig $config) - { - $this->printTitle('LANGUAGE'); - - if ($config->hasLanguage()) { - $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( - 'Do you want to keep imported language? (Y/n): ' - )); - if ($keepConfig) { - return; - } - } - - $config->setLanguage([ - 'DEFAULT' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select default language for the application in general (defaults to ' - . self::SUPPORTED_LANGUAGES[0] . '):', - self::SUPPORTED_LANGUAGES, - 0 - )), - 'CLI' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion( - 'Select default language for CLI executions (defaults to ' - . self::SUPPORTED_LANGUAGES[0] . '):', - self::SUPPORTED_LANGUAGES, - 0 - )), - ]); - } - - protected function askApplication(CustomizableAppConfig $config) - { - $this->printTitle('APPLICATION'); - - if ($config->hasApp()) { - $keepConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion( - 'Do you want to keep imported application config? (Y/n): ' - )); - if ($keepConfig) { - return; - } - } - - $config->setApp([ - 'SECRET' => $this->ask( - 'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)', - null, - true - ) ?: $this->generateRandomString(32), - ]); - } - - /** - * @param string $text - */ - protected function printTitle($text) - { - $text = trim($text); - $length = strlen($text) + 4; - $header = str_repeat('*', $length); - - $this->output->writeln([ - '', - '' . $header . '', - '* ' . strtoupper($text) . ' *', - '' . $header . '', - ]); - } - /** * @param string $text * @param string|null $default diff --git a/module/CLI/src/Factory/InstallApplicationFactory.php b/module/CLI/src/Factory/InstallApplicationFactory.php index 2c5b5236..e4d4beb9 100644 --- a/module/CLI/src/Factory/InstallApplicationFactory.php +++ b/module/CLI/src/Factory/InstallApplicationFactory.php @@ -3,10 +3,15 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Factory; +use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory; use Interop\Container\ContainerInterface; use Interop\Container\Exception\ContainerException; use Shlinkio\Shlink\CLI\Command\Install\InstallCommand; +use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManager; +use Shlinkio\Shlink\CLI\Install\Plugin; +use Shlinkio\Shlink\CLI\Install\Plugin\Factory\DefaultConfigCustomizerPluginFactory; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Filesystem\Filesystem; use Zend\Config\Writer\PhpArray; use Zend\ServiceManager\Exception\ServiceNotCreatedException; @@ -22,6 +27,7 @@ class InstallApplicationFactory implements FactoryInterface * @param string $requestedName * @param null|array $options * @return object + * @throws LogicException * @throws ServiceNotFoundException if unable to resolve the service. * @throws ServiceNotCreatedException if an exception is raised when * creating a service. @@ -32,7 +38,17 @@ class InstallApplicationFactory implements FactoryInterface $isUpdate = $options !== null && isset($options['isUpdate']) ? (bool) $options['isUpdate'] : false; $app = new Application(); - $command = new InstallCommand(new PhpArray(), new Filesystem(), $isUpdate); + $command = new InstallCommand( + new PhpArray(), + $container->get(Filesystem::class), + new ConfigCustomizerPluginManager($container, ['factories' => [ + Plugin\DatabaseConfigCustomizerPlugin::class => AnnotatedFactory::class, + Plugin\UrlShortenerConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class, + Plugin\LanguageConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class, + Plugin\ApplicationConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class, + ]]), + $isUpdate + ); $app->add($command); $app->setDefaultCommand($command->getName()); diff --git a/module/CLI/src/Install/ConfigCustomizerPluginManager.php b/module/CLI/src/Install/ConfigCustomizerPluginManager.php new file mode 100644 index 00000000..c8f0e7cb --- /dev/null +++ b/module/CLI/src/Install/ConfigCustomizerPluginManager.php @@ -0,0 +1,10 @@ +questionHelper = $questionHelper; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param string $text + * @param string|null $default + * @param bool $allowEmpty + * @return string + * @throws RuntimeException + */ + protected function ask(InputInterface $input, OutputInterface $output, $text, $default = null, $allowEmpty = false) + { + if ($default !== null) { + $text .= ' (defaults to ' . $default . ')'; + } + do { + $value = $this->questionHelper->ask($input, $output, new Question( + '' . $text . ': ', + $default + )); + if (empty($value) && ! $allowEmpty) { + $output->writeln('Value can\'t be empty'); + } + } while (empty($value) && $default === null && ! $allowEmpty); + + return $value; + } + + /** + * @param OutputInterface $output + * @param string $text + */ + protected function printTitle(OutputInterface $output, $text) + { + $text = trim($text); + $length = strlen($text) + 4; + $header = str_repeat('*', $length); + + $output->writeln([ + '', + '' . $header . '', + '* ' . strtoupper($text) . ' *', + '' . $header . '', + ]); + } +} diff --git a/module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php b/module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php new file mode 100644 index 00000000..78fda68a --- /dev/null +++ b/module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php @@ -0,0 +1,46 @@ +printTitle($output, 'APPLICATION'); + + if ($appConfig->hasApp()) { + $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion( + 'Do you want to keep imported application config? (Y/n): ' + )); + if ($keepConfig) { + return; + } + } + + $appConfig->setApp([ + 'SECRET' => $this->ask( + $input, + $output, + 'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)', + null, + true + ) ?: $this->generateRandomString(32), + ]); + } +} diff --git a/module/CLI/src/Install/Plugin/ConfigCustomizerPluginInterface.php b/module/CLI/src/Install/Plugin/ConfigCustomizerPluginInterface.php new file mode 100644 index 00000000..2f1c60e1 --- /dev/null +++ b/module/CLI/src/Install/Plugin/ConfigCustomizerPluginInterface.php @@ -0,0 +1,17 @@ + 'pdo_mysql', + 'PostgreSQL' => 'pdo_pgsql', + 'SQLite' => 'pdo_sqlite', + ]; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * DatabaseConfigCustomizerPlugin constructor. + * @param QuestionHelper $questionHelper + * @param Filesystem $filesystem + * + * @DI\Inject({QuestionHelper::class, Filesystem::class}) + */ + public function __construct(QuestionHelper $questionHelper, Filesystem $filesystem) + { + parent::__construct($questionHelper); + $this->filesystem = $filesystem; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param CustomizableAppConfig $appConfig + * @return void + * @throws IOException + * @throws RuntimeException + */ + public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig) + { + $this->printTitle($output, 'DATABASE'); + + if ($appConfig->hasDatabase()) { + $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion( + 'Do you want to keep imported database config? (Y/n): ' + )); + if ($keepConfig) { + // If the user selected to keep DB config and is configured to use sqlite, copy DB file + if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) { + try { + $this->filesystem->copy( + $appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH, + CustomizableAppConfig::SQLITE_DB_PATH + ); + } catch (IOException $e) { + $output->writeln('It wasn\'t possible to import the SQLite database'); + throw $e; + } + } + + return; + } + } + + // Select database type + $params = []; + $databases = array_keys(self::DATABASE_DRIVERS); + $dbType = $this->questionHelper->ask($input, $output, new ChoiceQuestion( + 'Select database type (defaults to ' . $databases[0] . '):', + $databases, + 0 + )); + $params['DRIVER'] = self::DATABASE_DRIVERS[$dbType]; + + // Ask for connection params if database is not SQLite + if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) { + $params['NAME'] = $this->ask($input, $output, 'Database name', 'shlink'); + $params['USER'] = $this->ask($input, $output, 'Database username'); + $params['PASSWORD'] = $this->ask($input, $output, 'Database password'); + $params['HOST'] = $this->ask($input, $output, 'Database host', 'localhost'); + $params['PORT'] = $this->ask($input, $output, 'Database port', $this->getDefaultDbPort($params['DRIVER'])); + } + + $appConfig->setDatabase($params); + } + + private function getDefaultDbPort($driver) + { + return $driver === 'pdo_mysql' ? '3306' : '5432'; + } +} diff --git a/module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php b/module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php new file mode 100644 index 00000000..6e1ea7a0 --- /dev/null +++ b/module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php @@ -0,0 +1,31 @@ +get(QuestionHelper::class)); + } +} diff --git a/module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php b/module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php new file mode 100644 index 00000000..3259883a --- /dev/null +++ b/module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php @@ -0,0 +1,52 @@ +printTitle($output, 'LANGUAGE'); + + if ($appConfig->hasLanguage()) { + $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion( + 'Do you want to keep imported language? (Y/n): ' + )); + if ($keepConfig) { + return; + } + } + + $appConfig->setLanguage([ + 'DEFAULT' => $this->questionHelper->ask($input, $output, new ChoiceQuestion( + 'Select default language for the application in general (defaults to ' + . self::SUPPORTED_LANGUAGES[0] . '):', + self::SUPPORTED_LANGUAGES, + 0 + )), + 'CLI' => $this->questionHelper->ask($input, $output, new ChoiceQuestion( + 'Select default language for CLI executions (defaults to ' + . self::SUPPORTED_LANGUAGES[0] . '):', + self::SUPPORTED_LANGUAGES, + 0 + )), + ]); + } +} diff --git a/module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php b/module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php new file mode 100644 index 00000000..7c0dec48 --- /dev/null +++ b/module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php @@ -0,0 +1,53 @@ +printTitle($output, 'URL SHORTENER'); + + if ($appConfig->hasUrlShortener()) { + $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion( + 'Do you want to keep imported URL shortener config? (Y/n): ' + )); + if ($keepConfig) { + return; + } + } + + // Ask for URL shortener params + $appConfig->setUrlShortener([ + 'SCHEMA' => $this->questionHelper->ask($input, $output, new ChoiceQuestion( + 'Select schema for generated short URLs (defaults to http):', + ['http', 'https'], + 0 + )), + 'HOSTNAME' => $this->ask($input, $output, 'Hostname for generated URLs'), + 'CHARS' => $this->ask( + $input, + $output, + 'Character set for generated short codes (leave empty to autogenerate one)', + null, + true + ) ?: str_shuffle(UrlShortener::DEFAULT_CHARS) + ]); + } +} diff --git a/module/CLI/src/Model/CustomizableAppConfig.php b/module/CLI/src/Model/CustomizableAppConfig.php index a1f48bbd..03836e0f 100644 --- a/module/CLI/src/Model/CustomizableAppConfig.php +++ b/module/CLI/src/Model/CustomizableAppConfig.php @@ -23,6 +23,10 @@ final class CustomizableAppConfig implements ArraySerializableInterface * @var array */ private $app; + /** + * @var string + */ + private $importedInstallationPath; /** * @return array @@ -128,6 +132,32 @@ final class CustomizableAppConfig implements ArraySerializableInterface return ! empty($this->app); } + /** + * @return string + */ + public function getImportedInstallationPath() + { + return $this->importedInstallationPath; + } + + /** + * @param string $importedInstallationPath + * @return $this|self + */ + public function setImportedInstallationPath($importedInstallationPath) + { + $this->importedInstallationPath = $importedInstallationPath; + return $this; + } + + /** + * @return bool + */ + public function hasImportedInstallationPath() + { + return $this->importedInstallationPath !== null; + } + /** * Exchange internal values from provided array * From 3547889ad58435fc2e7374c8b6bf64dd9902abcd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 6 Jul 2017 10:04:35 +0200 Subject: [PATCH 22/55] Fixed InstallCommandTest --- .../src/Command/Install/InstallCommand.php | 1 - .../Command/Install/InstallCommandTest.php | 94 ++++++++----------- 2 files changed, 37 insertions(+), 58 deletions(-) diff --git a/module/CLI/src/Command/Install/InstallCommand.php b/module/CLI/src/Command/Install/InstallCommand.php index 641fb552..d6ee3ea2 100644 --- a/module/CLI/src/Command/Install/InstallCommand.php +++ b/module/CLI/src/Command/Install/InstallCommand.php @@ -4,7 +4,6 @@ namespace Shlinkio\Shlink\CLI\Command\Install; use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManagerInterface; use Shlinkio\Shlink\CLI\Install\Plugin; use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig; -use Shlinkio\Shlink\Common\Util\StringUtilsTrait; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\RuntimeException; diff --git a/module/CLI/test/Command/Install/InstallCommandTest.php b/module/CLI/test/Command/Install/InstallCommandTest.php index 8c76299f..72f80cdd 100644 --- a/module/CLI/test/Command/Install/InstallCommandTest.php +++ b/module/CLI/test/Command/Install/InstallCommandTest.php @@ -5,6 +5,8 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Command\Install\InstallCommand; +use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManagerInterface; +use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerPluginInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Console\Tester\CommandTester; @@ -39,79 +41,57 @@ class InstallCommandTest extends TestCase $this->filesystem = $this->prophesize(Filesystem::class); $this->filesystem->exists(Argument::cetera())->willReturn(false); + $this->configWriter = $this->prophesize(WriterInterface::class); + + $configCustomizer = $this->prophesize(ConfigCustomizerPluginInterface::class); + $configCustomizers = $this->prophesize(ConfigCustomizerPluginManagerInterface::class); + $configCustomizers->get(Argument::cetera())->willReturn($configCustomizer->reveal()); + $app = new Application(); $helperSet = $app->getHelperSet(); $helperSet->set($processHelper->reveal()); $app->setHelperSet($helperSet); - - $this->configWriter = $this->prophesize(WriterInterface::class); - $command = new InstallCommand($this->configWriter->reveal(), $this->filesystem->reveal()); + $command = new InstallCommand( + $this->configWriter->reveal(), + $this->filesystem->reveal(), + $configCustomizers->reveal() + ); $app->add($command); $questionHelper = $command->getHelper('question'); - $questionHelper->setInputStream($this->createInputStream()); +// $questionHelper->setInputStream($this->createInputStream()); $this->commandTester = new CommandTester($command); } - protected function createInputStream() - { - $stream = fopen('php://memory', 'rb+', false); - fwrite($stream, <<configWriter->toFile(Argument::any(), [ - 'app_options' => [ - 'secret_key' => 'my_secret', - ], - 'entity_manager' => [ - 'connection' => [ - 'driver' => 'pdo_mysql', - 'dbname' => 'shlink_db', - 'user' => 'alejandro', - 'password' => '1234', - 'host' => 'localhost', - 'port' => '3306', - 'driverOptions' => [ - \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', - ] - ], - ], - 'translator' => [ - 'locale' => 'en', - ], - 'cli' => [ - 'locale' => 'es', - ], - 'url_shortener' => [ - 'domain' => [ - 'schema' => 'http', - 'hostname' => 'doma.in', - ], - 'shortcode_chars' => 'abc123BCA', - ], - ], false)->shouldBeCalledTimes(1); + $this->configWriter->toFile(Argument::any(), Argument::type('array'), false)->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shlink:install', From bb050cc1b6fb19b251c425c9b12bde6d006db50e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 6 Jul 2017 13:38:15 +0200 Subject: [PATCH 23/55] Improved InstallCommandTest coverage --- .../src/Command/Install/InstallCommand.php | 6 +- .../config/params/generated_config.php | 2 + .../Command/Install/InstallCommandTest.php | 101 ++++++++++++------ 3 files changed, 75 insertions(+), 34 deletions(-) create mode 100644 module/CLI/test-resources/config/params/generated_config.php diff --git a/module/CLI/src/Command/Install/InstallCommand.php b/module/CLI/src/Command/Install/InstallCommand.php index d6ee3ea2..93de4305 100644 --- a/module/CLI/src/Command/Install/InstallCommand.php +++ b/module/CLI/src/Command/Install/InstallCommand.php @@ -158,7 +158,7 @@ class InstallCommand extends Command * @return CustomizableAppConfig * @throws RuntimeException */ - protected function importConfig() + private function importConfig() { $config = new CustomizableAppConfig(); @@ -204,7 +204,7 @@ class InstallCommand extends Command * @return string * @throws RuntimeException */ - protected function ask($text, $default = null, $allowEmpty = false) + private function ask($text, $default = null, $allowEmpty = false) { if ($default !== null) { $text .= ' (defaults to ' . $default . ')'; @@ -227,7 +227,7 @@ class InstallCommand extends Command * @param string $errorMessage * @return bool */ - protected function runCommand($command, $errorMessage) + private function runCommand($command, $errorMessage) { $process = $this->processHelper->run($this->output, $command); if ($process->isSuccessful()) { diff --git a/module/CLI/test-resources/config/params/generated_config.php b/module/CLI/test-resources/config/params/generated_config.php new file mode 100644 index 00000000..881ab67d --- /dev/null +++ b/module/CLI/test-resources/config/params/generated_config.php @@ -0,0 +1,2 @@ +getHelperSet(); $helperSet->set($processHelper->reveal()); $app->setHelperSet($helperSet); - $command = new InstallCommand( + $this->command = new InstallCommand( $this->configWriter->reveal(), $this->filesystem->reveal(), $configCustomizers->reveal() ); - $app->add($command); + $app->add($this->command); - $questionHelper = $command->getHelper('question'); -// $questionHelper->setInputStream($this->createInputStream()); - $this->commandTester = new CommandTester($command); + $this->commandTester = new CommandTester($this->command); } -// protected function createInputStream() -// { -// $stream = fopen('php://memory', 'rb+', false); -// fwrite($stream, <<configWriter->toFile(Argument::any(), Argument::type('array'), false)->shouldBeCalledTimes(1); + $this->commandTester->execute([]); + } - $this->commandTester->execute([ - 'command' => 'shlink:install', + /** + * @test + */ + public function cachedConfigIsDeletedIfExists() + { + /** @var MethodProphecy $appConfigExists */ + $appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn(true); + /** @var MethodProphecy $appConfigRemove */ + $appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willReturn(null); + + $this->commandTester->execute([]); + + $appConfigExists->shouldHaveBeenCalledTimes(1); + $appConfigRemove->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + public function exceptionWhileDeletingCachedConfigCancelsProcess() + { + /** @var MethodProphecy $appConfigExists */ + $appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn(true); + /** @var MethodProphecy $appConfigRemove */ + $appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willThrow(IOException::class); + /** @var MethodProphecy $configToFile */ + $configToFile = $this->configWriter->toFile(Argument::cetera())->willReturn(true); + + $this->commandTester->execute([]); + + $appConfigExists->shouldHaveBeenCalledTimes(1); + $appConfigRemove->shouldHaveBeenCalledTimes(1); + $configToFile->shouldNotHaveBeenCalled(); + } + + /** + * @test + */ + public function whenCommandIsUpdatePreviousConfigCanBeImported() + { + $ref = new \ReflectionObject($this->command); + $prop = $ref->getProperty('isUpdate'); + $prop->setAccessible(true); + $prop->setValue($this->command, true); + + /** @var MethodProphecy $importedConfigExists */ + $importedConfigExists = $this->filesystem->exists( + __DIR__ . '/../../../test-resources/' . InstallCommand::GENERATED_CONFIG_PATH + )->willReturn(true); + + $this->commandTester->setInputs([ + '', + '/foo/bar/wrong_previous_shlink', + '', + __DIR__ . '/../../../test-resources', ]); + $this->commandTester->execute([]); + + $importedConfigExists->shouldHaveBeenCalled(); } } From 99ffff11c7cd3377ebed08558a7a55ba900bad9a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 6 Jul 2017 13:43:36 +0200 Subject: [PATCH 24/55] Created DefaultConfigCustomizerPluginFactoryTest --- ...faultConfigCustomizerPluginFactoryTest.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 module/CLI/test/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactoryTest.php diff --git a/module/CLI/test/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactoryTest.php b/module/CLI/test/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactoryTest.php new file mode 100644 index 00000000..509099ed --- /dev/null +++ b/module/CLI/test/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactoryTest.php @@ -0,0 +1,40 @@ +factory = new DefaultConfigCustomizerPluginFactory(); + } + + /** + * @test + */ + public function createsProperService() + { + $instance = $this->factory->__invoke(new ServiceManager(['services' => [ + QuestionHelper::class => $this->prophesize(QuestionHelper::class)->reveal(), + ]]), ApplicationConfigCustomizerPlugin::class); + $this->assertInstanceOf(ApplicationConfigCustomizerPlugin::class, $instance); + + $instance = $this->factory->__invoke(new ServiceManager(['services' => [ + QuestionHelper::class => $this->prophesize(QuestionHelper::class)->reveal(), + ]]), LanguageConfigCustomizerPlugin::class); + $this->assertInstanceOf(LanguageConfigCustomizerPlugin::class, $instance); + } +} From d56cde72a380c1945e294e9d2df3f8d9b42adaa6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 6 Jul 2017 17:12:19 +0200 Subject: [PATCH 25/55] Created ApplicationConfigCustomizerPluginTest --- .../ApplicationConfigCustomizerPluginTest.php | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 module/CLI/test/Install/Plugin/ApplicationConfigCustomizerPluginTest.php diff --git a/module/CLI/test/Install/Plugin/ApplicationConfigCustomizerPluginTest.php b/module/CLI/test/Install/Plugin/ApplicationConfigCustomizerPluginTest.php new file mode 100644 index 00000000..cd4cef42 --- /dev/null +++ b/module/CLI/test/Install/Plugin/ApplicationConfigCustomizerPluginTest.php @@ -0,0 +1,96 @@ +questionHelper = $this->prophesize(QuestionHelper::class); + $this->plugin = new ApplicationConfigCustomizerPlugin($this->questionHelper->reveal()); + } + + /** + * @test + */ + public function configIsRequestedToTheUser() + { + /** @var MethodProphecy $askSecret */ + $askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('the_secret'); + $config = new CustomizableAppConfig(); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertTrue($config->hasApp()); + $this->assertEquals([ + 'SECRET' => 'the_secret', + ], $config->getApp()); + $askSecret->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + public function overwriteIsRequestedIfValueIsAlreadySet() + { + /** @var MethodProphecy $ask */ + $ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) { + $last = array_pop($args); + return $last instanceof ConfirmationQuestion ? false : 'the_new_secret'; + }); + $config = new CustomizableAppConfig(); + $config->setApp([ + 'SECRET' => 'foo', + ]); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertEquals([ + 'SECRET' => 'the_new_secret', + ], $config->getApp()); + $ask->shouldHaveBeenCalledTimes(2); + } + + /** + * @test + */ + public function existingValueIsKeptIfRequested() + { + /** @var MethodProphecy $ask */ + $ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true); + + + $config = new CustomizableAppConfig(); + $config->setApp([ + 'SECRET' => 'foo', + ]); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertEquals([ + 'SECRET' => 'foo', + ], $config->getApp()); + $ask->shouldHaveBeenCalledTimes(1); + } +} From 69a99949e198050b84387e2b5fb107f861a61859 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 6 Jul 2017 17:20:55 +0200 Subject: [PATCH 26/55] Created LanguageConfigCustomizerPluginTest --- .../LanguageConfigCustomizerPluginTest.php | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 module/CLI/test/Install/Plugin/LanguageConfigCustomizerPluginTest.php diff --git a/module/CLI/test/Install/Plugin/LanguageConfigCustomizerPluginTest.php b/module/CLI/test/Install/Plugin/LanguageConfigCustomizerPluginTest.php new file mode 100644 index 00000000..44d82cb4 --- /dev/null +++ b/module/CLI/test/Install/Plugin/LanguageConfigCustomizerPluginTest.php @@ -0,0 +1,101 @@ +questionHelper = $this->prophesize(QuestionHelper::class); + $this->plugin = new LanguageConfigCustomizerPlugin($this->questionHelper->reveal()); + } + + /** + * @test + */ + public function configIsRequestedToTheUser() + { + /** @var MethodProphecy $askSecret */ + $askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('en'); + $config = new CustomizableAppConfig(); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertTrue($config->hasLanguage()); + $this->assertEquals([ + 'DEFAULT' => 'en', + 'CLI' => 'en', + ], $config->getLanguage()); + $askSecret->shouldHaveBeenCalledTimes(2); + } + + /** + * @test + */ + public function overwriteIsRequestedIfValueIsAlreadySet() + { + /** @var MethodProphecy $ask */ + $ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) { + $last = array_pop($args); + return $last instanceof ConfirmationQuestion ? false : 'es'; + }); + $config = new CustomizableAppConfig(); + $config->setLanguage([ + 'DEFAULT' => 'en', + 'CLI' => 'en', + ]); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertEquals([ + 'DEFAULT' => 'es', + 'CLI' => 'es', + ], $config->getLanguage()); + $ask->shouldHaveBeenCalledTimes(3); + } + + /** + * @test + */ + public function existingValueIsKeptIfRequested() + { + /** @var MethodProphecy $ask */ + $ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true); + + + $config = new CustomizableAppConfig(); + $config->setLanguage([ + 'DEFAULT' => 'es', + 'CLI' => 'es', + ]); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertEquals([ + 'DEFAULT' => 'es', + 'CLI' => 'es', + ], $config->getLanguage()); + $ask->shouldHaveBeenCalledTimes(1); + } +} From 23922f6c7b4aa2818956bd103b0c044bfa8e446b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 6 Jul 2017 17:28:32 +0200 Subject: [PATCH 27/55] Created UrlShortenerConfigCustomizerPluginTest --- .../LanguageConfigCustomizerPluginTest.php | 1 - ...UrlShortenerConfigCustomizerPluginTest.php | 105 ++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 module/CLI/test/Install/Plugin/UrlShortenerConfigCustomizerPluginTest.php diff --git a/module/CLI/test/Install/Plugin/LanguageConfigCustomizerPluginTest.php b/module/CLI/test/Install/Plugin/LanguageConfigCustomizerPluginTest.php index 44d82cb4..0edc382e 100644 --- a/module/CLI/test/Install/Plugin/LanguageConfigCustomizerPluginTest.php +++ b/module/CLI/test/Install/Plugin/LanguageConfigCustomizerPluginTest.php @@ -83,7 +83,6 @@ class LanguageConfigCustomizerPluginTest extends TestCase /** @var MethodProphecy $ask */ $ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true); - $config = new CustomizableAppConfig(); $config->setLanguage([ 'DEFAULT' => 'es', diff --git a/module/CLI/test/Install/Plugin/UrlShortenerConfigCustomizerPluginTest.php b/module/CLI/test/Install/Plugin/UrlShortenerConfigCustomizerPluginTest.php new file mode 100644 index 00000000..dc2a9f6e --- /dev/null +++ b/module/CLI/test/Install/Plugin/UrlShortenerConfigCustomizerPluginTest.php @@ -0,0 +1,105 @@ +questionHelper = $this->prophesize(QuestionHelper::class); + $this->plugin = new UrlShortenerConfigCustomizerPlugin($this->questionHelper->reveal()); + } + + /** + * @test + */ + public function configIsRequestedToTheUser() + { + /** @var MethodProphecy $askSecret */ + $askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('something'); + $config = new CustomizableAppConfig(); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertTrue($config->hasUrlShortener()); + $this->assertEquals([ + 'SCHEMA' => 'something', + 'HOSTNAME' => 'something', + 'CHARS' => 'something', + ], $config->getUrlShortener()); + $askSecret->shouldHaveBeenCalledTimes(3); + } + + /** + * @test + */ + public function overwriteIsRequestedIfValueIsAlreadySet() + { + /** @var MethodProphecy $ask */ + $ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) { + $last = array_pop($args); + return $last instanceof ConfirmationQuestion ? false : 'foo'; + }); + $config = new CustomizableAppConfig(); + $config->setUrlShortener([ + 'SCHEMA' => 'bar', + 'HOSTNAME' => 'bar', + 'CHARS' => 'bar', + ]); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertEquals([ + 'SCHEMA' => 'foo', + 'HOSTNAME' => 'foo', + 'CHARS' => 'foo', + ], $config->getUrlShortener()); + $ask->shouldHaveBeenCalledTimes(4); + } + + /** + * @test + */ + public function existingValueIsKeptIfRequested() + { + /** @var MethodProphecy $ask */ + $ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true); + + $config = new CustomizableAppConfig(); + $config->setUrlShortener([ + 'SCHEMA' => 'foo', + 'HOSTNAME' => 'foo', + 'CHARS' => 'foo', + ]); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertEquals([ + 'SCHEMA' => 'foo', + 'HOSTNAME' => 'foo', + 'CHARS' => 'foo', + ], $config->getUrlShortener()); + $ask->shouldHaveBeenCalledTimes(1); + } +} From c05aeabdeea99d58e22475fc18c298693639c5ec Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 6 Jul 2017 17:38:16 +0200 Subject: [PATCH 28/55] Improved if statements reducing indentation --- .../Plugin/ApplicationConfigCustomizerPlugin.php | 11 ++++------- .../Install/Plugin/LanguageConfigCustomizerPlugin.php | 11 ++++------- .../Plugin/UrlShortenerConfigCustomizerPlugin.php | 11 ++++------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php b/module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php index 78fda68a..b1bf6436 100644 --- a/module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php +++ b/module/CLI/src/Install/Plugin/ApplicationConfigCustomizerPlugin.php @@ -24,13 +24,10 @@ class ApplicationConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin { $this->printTitle($output, 'APPLICATION'); - if ($appConfig->hasApp()) { - $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion( - 'Do you want to keep imported application config? (Y/n): ' - )); - if ($keepConfig) { - return; - } + if ($appConfig->hasApp() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion( + 'Do you want to keep imported application config? (Y/n): ' + ))) { + return; } $appConfig->setApp([ diff --git a/module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php b/module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php index 3259883a..83fbe7ad 100644 --- a/module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php +++ b/module/CLI/src/Install/Plugin/LanguageConfigCustomizerPlugin.php @@ -25,13 +25,10 @@ class LanguageConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin { $this->printTitle($output, 'LANGUAGE'); - if ($appConfig->hasLanguage()) { - $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion( - 'Do you want to keep imported language? (Y/n): ' - )); - if ($keepConfig) { - return; - } + if ($appConfig->hasLanguage() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion( + 'Do you want to keep imported language? (Y/n): ' + ))) { + return; } $appConfig->setLanguage([ diff --git a/module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php b/module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php index 7c0dec48..30186bfb 100644 --- a/module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php +++ b/module/CLI/src/Install/Plugin/UrlShortenerConfigCustomizerPlugin.php @@ -24,13 +24,10 @@ class UrlShortenerConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin { $this->printTitle($output, 'URL SHORTENER'); - if ($appConfig->hasUrlShortener()) { - $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion( - 'Do you want to keep imported URL shortener config? (Y/n): ' - )); - if ($keepConfig) { - return; - } + if ($appConfig->hasUrlShortener() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion( + 'Do you want to keep imported URL shortener config? (Y/n): ' + ))) { + return; } // Ask for URL shortener params From dd099dc39cb8a5ea22ca5eae0d54adbb730279c9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 6 Jul 2017 17:49:05 +0200 Subject: [PATCH 29/55] Removed declare strict types added by mistake --- .../src/Factory/InstallApplicationFactory.php | 2 -- .../ApplicationConfigCustomizerPlugin.php | 2 -- .../Plugin/DatabaseConfigCustomizerPlugin.php | 33 +++++++++---------- .../DefaultConfigCustomizerPluginFactory.php | 2 -- .../Plugin/LanguageConfigCustomizerPlugin.php | 2 -- .../UrlShortenerConfigCustomizerPlugin.php | 2 -- .../ApplicationConfigCustomizerPluginTest.php | 2 -- ...faultConfigCustomizerPluginFactoryTest.php | 2 -- .../LanguageConfigCustomizerPluginTest.php | 2 -- ...UrlShortenerConfigCustomizerPluginTest.php | 2 -- ...seImplicitOptionsMiddlewareFactoryTest.php | 2 -- 11 files changed, 15 insertions(+), 38 deletions(-) diff --git a/module/CLI/src/Factory/InstallApplicationFactory.php b/module/CLI/src/Factory/InstallApplicationFactory.php index e4d4beb9..742b5b70 100644 --- a/module/CLI/src/Factory/InstallApplicationFactory.php +++ b/module/CLI/src/Factory/InstallApplicationFactory.php @@ -1,6 +1,4 @@ printTitle($output, 'DATABASE'); - if ($appConfig->hasDatabase()) { - $keepConfig = $this->questionHelper->ask($input, $output, new ConfirmationQuestion( - 'Do you want to keep imported database config? (Y/n): ' - )); - if ($keepConfig) { - // If the user selected to keep DB config and is configured to use sqlite, copy DB file - if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) { - try { - $this->filesystem->copy( - $appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH, - CustomizableAppConfig::SQLITE_DB_PATH - ); - } catch (IOException $e) { - $output->writeln('It wasn\'t possible to import the SQLite database'); - throw $e; - } + if ($appConfig->hasDatabase() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion( + 'Do you want to keep imported database config? (Y/n): ' + ))) { + // If the user selected to keep DB config and is configured to use sqlite, copy DB file + if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) { + try { + $this->filesystem->copy( + $appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH, + CustomizableAppConfig::SQLITE_DB_PATH + ); + } catch (IOException $e) { + $output->writeln('It wasn\'t possible to import the SQLite database'); + throw $e; } - - return; } + + return; } // Select database type diff --git a/module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php b/module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php index 6e1ea7a0..78e5a87a 100644 --- a/module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php +++ b/module/CLI/src/Install/Plugin/Factory/DefaultConfigCustomizerPluginFactory.php @@ -1,6 +1,4 @@ Date: Thu, 6 Jul 2017 18:00:38 +0200 Subject: [PATCH 30/55] Created DatabaseConfigCustomizerPluginTest --- .../ApplicationConfigCustomizerPluginTest.php | 1 - .../DatabaseConfigCustomizerPluginTest.php | 152 ++++++++++++++++++ 2 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 module/CLI/test/Install/Plugin/DatabaseConfigCustomizerPluginTest.php diff --git a/module/CLI/test/Install/Plugin/ApplicationConfigCustomizerPluginTest.php b/module/CLI/test/Install/Plugin/ApplicationConfigCustomizerPluginTest.php index d0a4505d..2b30a328 100644 --- a/module/CLI/test/Install/Plugin/ApplicationConfigCustomizerPluginTest.php +++ b/module/CLI/test/Install/Plugin/ApplicationConfigCustomizerPluginTest.php @@ -78,7 +78,6 @@ class ApplicationConfigCustomizerPluginTest extends TestCase /** @var MethodProphecy $ask */ $ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true); - $config = new CustomizableAppConfig(); $config->setApp([ 'SECRET' => 'foo', diff --git a/module/CLI/test/Install/Plugin/DatabaseConfigCustomizerPluginTest.php b/module/CLI/test/Install/Plugin/DatabaseConfigCustomizerPluginTest.php new file mode 100644 index 00000000..85d18225 --- /dev/null +++ b/module/CLI/test/Install/Plugin/DatabaseConfigCustomizerPluginTest.php @@ -0,0 +1,152 @@ +questionHelper = $this->prophesize(QuestionHelper::class); + $this->filesystem = $this->prophesize(Filesystem::class); + + $this->plugin = new DatabaseConfigCustomizerPlugin( + $this->questionHelper->reveal(), + $this->filesystem->reveal() + ); + } + + /** + * @test + */ + public function configIsRequestedToTheUser() + { + /** @var MethodProphecy $askSecret */ + $askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('MySQL'); + $config = new CustomizableAppConfig(); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertTrue($config->hasDatabase()); + $this->assertEquals([ + 'DRIVER' => 'pdo_mysql', + 'NAME' => 'MySQL', + 'USER' => 'MySQL', + 'PASSWORD' => 'MySQL', + 'HOST' => 'MySQL', + 'PORT' => 'MySQL', + ], $config->getDatabase()); + $askSecret->shouldHaveBeenCalledTimes(6); + } + + /** + * @test + */ + public function overwriteIsRequestedIfValueIsAlreadySet() + { + /** @var MethodProphecy $ask */ + $ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) { + $last = array_pop($args); + return $last instanceof ConfirmationQuestion ? false : 'MySQL'; + }); + $config = new CustomizableAppConfig(); + $config->setDatabase([ + 'DRIVER' => 'pdo_pgsql', + 'NAME' => 'MySQL', + 'USER' => 'MySQL', + 'PASSWORD' => 'MySQL', + 'HOST' => 'MySQL', + 'PORT' => 'MySQL', + ]); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertEquals([ + 'DRIVER' => 'pdo_mysql', + 'NAME' => 'MySQL', + 'USER' => 'MySQL', + 'PASSWORD' => 'MySQL', + 'HOST' => 'MySQL', + 'PORT' => 'MySQL', + ], $config->getDatabase()); + $ask->shouldHaveBeenCalledTimes(7); + } + + /** + * @test + */ + public function existingValueIsKeptIfRequested() + { + /** @var MethodProphecy $ask */ + $ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true); + + $config = new CustomizableAppConfig(); + $config->setDatabase([ + 'DRIVER' => 'pdo_pgsql', + 'NAME' => 'MySQL', + 'USER' => 'MySQL', + 'PASSWORD' => 'MySQL', + 'HOST' => 'MySQL', + 'PORT' => 'MySQL', + ]); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertEquals([ + 'DRIVER' => 'pdo_pgsql', + 'NAME' => 'MySQL', + 'USER' => 'MySQL', + 'PASSWORD' => 'MySQL', + 'HOST' => 'MySQL', + 'PORT' => 'MySQL', + ], $config->getDatabase()); + $ask->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + public function sqliteDatabaseIsImportedWhenRequested() + { + /** @var MethodProphecy $ask */ + $ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true); + /** @var MethodProphecy $copy */ + $copy = $this->filesystem->copy(Argument::cetera())->willReturn(null); + + $config = new CustomizableAppConfig(); + $config->setDatabase([ + 'DRIVER' => 'pdo_sqlite', + ]); + + $this->plugin->process(new ArrayInput([]), new NullOutput(), $config); + + $this->assertEquals([ + 'DRIVER' => 'pdo_sqlite', + ], $config->getDatabase()); + $ask->shouldHaveBeenCalledTimes(1); + $copy->shouldHaveBeenCalledTimes(1); + } +} From e0f18f8d1fd45709d6f8794a7a0dd786c3e89c2e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 6 Jul 2017 18:06:11 +0200 Subject: [PATCH 31/55] Created InstallApplicationFactoryTest --- .../Factory/InstallApplicationFactoryTest.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 module/CLI/test/Factory/InstallApplicationFactoryTest.php diff --git a/module/CLI/test/Factory/InstallApplicationFactoryTest.php b/module/CLI/test/Factory/InstallApplicationFactoryTest.php new file mode 100644 index 00000000..35820bba --- /dev/null +++ b/module/CLI/test/Factory/InstallApplicationFactoryTest.php @@ -0,0 +1,35 @@ +factory = new InstallApplicationFactory(); + } + + /** + * @test + */ + public function serviceIsCreated() + { + $instance = $this->factory->__invoke(new ServiceManager(['services' => [ + Filesystem::class => $this->prophesize(Filesystem::class)->reveal(), + ]]), ''); + + $this->assertInstanceOf(Application::class, $instance); + } +} From 486ea10c3c27443140c61f4c2fffabeb1b9a73eb Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 7 Jul 2017 11:45:20 +0200 Subject: [PATCH 32/55] Renamed EditTagsAction to EditShortcodeTagsAction --- module/Rest/config/dependencies.config.php | 2 +- module/Rest/config/routes.config.php | 2 +- .../{EditTagsAction.php => EditShortcodeTagsAction.php} | 4 ++-- ...TagsActionTest.php => EditShortcodeTagsActionTest.php} | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) rename module/Rest/src/Action/{EditTagsAction.php => EditShortcodeTagsAction.php} (96%) rename module/Rest/test/Action/{EditTagsActionTest.php => EditShortcodeTagsActionTest.php} (89%) diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 3fae9af7..d3bff420 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -18,7 +18,7 @@ return [ Action\ResolveUrlAction::class => AnnotatedFactory::class, Action\GetVisitsAction::class => AnnotatedFactory::class, Action\ListShortcodesAction::class => AnnotatedFactory::class, - Action\EditTagsAction::class => AnnotatedFactory::class, + Action\EditShortcodeTagsAction::class => AnnotatedFactory::class, Middleware\BodyParserMiddleware::class => AnnotatedFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 4edf4658..8344e52a 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -37,7 +37,7 @@ return [ [ 'name' => 'rest-edit-tags', 'path' => '/rest/v{version:1}/short-codes/{shortCode}/tags', - 'middleware' => Action\EditTagsAction::class, + 'middleware' => Action\EditShortcodeTagsAction::class, 'allowed_methods' => ['PUT'], ], ], diff --git a/module/Rest/src/Action/EditTagsAction.php b/module/Rest/src/Action/EditShortcodeTagsAction.php similarity index 96% rename from module/Rest/src/Action/EditTagsAction.php rename to module/Rest/src/Action/EditShortcodeTagsAction.php index 075e65f0..9eab0759 100644 --- a/module/Rest/src/Action/EditTagsAction.php +++ b/module/Rest/src/Action/EditShortcodeTagsAction.php @@ -13,7 +13,7 @@ use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; use Zend\I18n\Translator\TranslatorInterface; -class EditTagsAction extends AbstractRestAction +class EditShortcodeTagsAction extends AbstractRestAction { /** * @var ShortUrlServiceInterface @@ -25,7 +25,7 @@ class EditTagsAction extends AbstractRestAction private $translator; /** - * EditTagsAction constructor. + * EditShortcodeTagsAction constructor. * @param ShortUrlServiceInterface $shortUrlService * @param TranslatorInterface $translator * @param LoggerInterface|null $logger diff --git a/module/Rest/test/Action/EditTagsActionTest.php b/module/Rest/test/Action/EditShortcodeTagsActionTest.php similarity index 89% rename from module/Rest/test/Action/EditTagsActionTest.php rename to module/Rest/test/Action/EditShortcodeTagsActionTest.php index 518925ab..72f1dee9 100644 --- a/module/Rest/test/Action/EditTagsActionTest.php +++ b/module/Rest/test/Action/EditShortcodeTagsActionTest.php @@ -6,15 +6,15 @@ use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\ShortUrlService; -use Shlinkio\Shlink\Rest\Action\EditTagsAction; +use Shlinkio\Shlink\Rest\Action\EditShortcodeTagsAction; use ShlinkioTest\Shlink\Common\Util\TestUtils; use Zend\Diactoros\ServerRequestFactory; use Zend\I18n\Translator\Translator; -class EditTagsActionTest extends TestCase +class EditShortcodeTagsActionTest extends TestCase { /** - * @var EditTagsAction + * @var EditShortcodeTagsAction */ protected $action; /** @@ -25,7 +25,7 @@ class EditTagsActionTest extends TestCase public function setUp() { $this->shortUrlService = $this->prophesize(ShortUrlService::class); - $this->action = new EditTagsAction($this->shortUrlService->reveal(), Translator::factory([])); + $this->action = new EditShortcodeTagsAction($this->shortUrlService->reveal(), Translator::factory([])); } /** From c37660f76327d64d39cb1bfec589818152e5d570 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 7 Jul 2017 12:49:41 +0200 Subject: [PATCH 33/55] Created TagService --- module/Core/config/dependencies.config.php | 1 + module/Core/src/Service/Tag/TagService.php | 34 +++++++++++++ .../src/Service/Tag/TagServiceInterface.php | 12 +++++ .../Core/test/Service/Tag/TagServiceTest.php | 51 +++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 module/Core/src/Service/Tag/TagService.php create mode 100644 module/Core/src/Service/Tag/TagServiceInterface.php create mode 100644 module/Core/test/Service/Tag/TagServiceTest.php diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index f5c75bac..1ef7257c 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -16,6 +16,7 @@ return [ Service\VisitsTracker::class => AnnotatedFactory::class, Service\ShortUrlService::class => AnnotatedFactory::class, Service\VisitService::class => AnnotatedFactory::class, + Service\Tag\TagService::class => AnnotatedFactory::class, // Middleware Action\RedirectAction::class => AnnotatedFactory::class, diff --git a/module/Core/src/Service/Tag/TagService.php b/module/Core/src/Service/Tag/TagService.php new file mode 100644 index 00000000..91cb5a72 --- /dev/null +++ b/module/Core/src/Service/Tag/TagService.php @@ -0,0 +1,34 @@ +em = $em; + } + + /** + * @return Tag[] + * @throws \UnexpectedValueException + */ + public function listTags() + { + return $this->em->getRepository(Tag::class)->findBy([], ['name' => 'DESC']); + } +} diff --git a/module/Core/src/Service/Tag/TagServiceInterface.php b/module/Core/src/Service/Tag/TagServiceInterface.php new file mode 100644 index 00000000..cf1914f4 --- /dev/null +++ b/module/Core/src/Service/Tag/TagServiceInterface.php @@ -0,0 +1,12 @@ +em = $this->prophesize(EntityManagerInterface::class); + $this->service = new TagService($this->em->reveal()); + } + + /** + * @test + */ + public function listTagsDelegatesOnRepository() + { + $expected = [new Tag(), new Tag()]; + + $repo = $this->prophesize(EntityRepository::class); + /** @var MethodProphecy $find */ + $find = $repo->findBy(Argument::cetera())->willReturn($expected); + /** @var MethodProphecy $getRepo */ + $getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal()); + + $result = $this->service->listTags(); + + $this->assertEquals($expected, $result); + $find->shouldHaveBeenCalled(); + $getRepo->shouldHaveBeenCalled(); + } +} From 95ec7e0afae6cd3ca7c11312d44f074f214eee4a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 7 Jul 2017 13:12:45 +0200 Subject: [PATCH 34/55] Registered action to list tags --- module/Rest/config/dependencies.config.php | 1 + module/Rest/config/routes.config.php | 34 +++++++++---- module/Rest/src/Action/AuthenticateAction.php | 1 + .../Rest/src/Action/CreateShortcodeAction.php | 1 + .../src/Action/EditShortcodeTagsAction.php | 1 + module/Rest/src/Action/GetVisitsAction.php | 1 + .../Rest/src/Action/ListShortcodesAction.php | 3 +- module/Rest/src/Action/ListTagsAction.php | 51 +++++++++++++++++++ module/Rest/src/Action/ResolveUrlAction.php | 3 +- .../CheckAuthenticationMiddleware.php | 3 +- .../CheckAuthenticationMiddlewareTest.php | 3 +- 11 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 module/Rest/src/Action/ListTagsAction.php diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index d3bff420..d48d319b 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -19,6 +19,7 @@ return [ Action\GetVisitsAction::class => AnnotatedFactory::class, Action\ListShortcodesAction::class => AnnotatedFactory::class, Action\EditShortcodeTagsAction::class => AnnotatedFactory::class, + Action\ListTagsAction::class => AnnotatedFactory::class, Middleware\BodyParserMiddleware::class => AnnotatedFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 8344e52a..6212e6a8 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -5,41 +5,53 @@ return [ 'routes' => [ [ - 'name' => 'rest-authenticate', + 'name' => Action\AuthenticateAction::class, 'path' => '/rest/v{version:1}/authenticate', 'middleware' => Action\AuthenticateAction::class, 'allowed_methods' => ['POST'], ], + + // Short codes [ - 'name' => 'rest-create-shortcode', + 'name' => Action\CreateShortcodeAction::class, 'path' => '/rest/v{version:1}/short-codes', 'middleware' => Action\CreateShortcodeAction::class, 'allowed_methods' => ['POST'], ], [ - 'name' => 'rest-resolve-url', + 'name' => Action\ResolveUrlAction::class, 'path' => '/rest/v{version:1}/short-codes/{shortCode}', 'middleware' => Action\ResolveUrlAction::class, 'allowed_methods' => ['GET'], ], [ - 'name' => 'rest-list-shortened-url', + 'name' => Action\ListShortcodesAction::class, 'path' => '/rest/v{version:1}/short-codes', 'middleware' => Action\ListShortcodesAction::class, 'allowed_methods' => ['GET'], ], [ - 'name' => 'rest-get-visits', - 'path' => '/rest/v{version:1}/short-codes/{shortCode}/visits', - 'middleware' => Action\GetVisitsAction::class, - 'allowed_methods' => ['GET'], - ], - [ - 'name' => 'rest-edit-tags', + 'name' => Action\EditShortcodeTagsAction::class, 'path' => '/rest/v{version:1}/short-codes/{shortCode}/tags', 'middleware' => Action\EditShortcodeTagsAction::class, 'allowed_methods' => ['PUT'], ], + + // Visits + [ + 'name' => Action\GetVisitsAction::class, + 'path' => '/rest/v{version:1}/short-codes/{shortCode}/visits', + 'middleware' => Action\GetVisitsAction::class, + 'allowed_methods' => ['GET'], + ], + + // Tags + [ + 'name' => Action\ListTagsAction::class, + 'path' => '/rest/v{version:1}/tags', + 'middleware' => Action\ListTagsAction::class, + 'allowed_methods' => ['GET'], + ], ], ]; diff --git a/module/Rest/src/Action/AuthenticateAction.php b/module/Rest/src/Action/AuthenticateAction.php index 73d6a7df..a4b15920 100644 --- a/module/Rest/src/Action/AuthenticateAction.php +++ b/module/Rest/src/Action/AuthenticateAction.php @@ -54,6 +54,7 @@ class AuthenticateAction extends AbstractRestAction * @param Request $request * @param DelegateInterface $delegate * @return null|Response + * @throws \InvalidArgumentException */ public function process(Request $request, DelegateInterface $delegate) { diff --git a/module/Rest/src/Action/CreateShortcodeAction.php b/module/Rest/src/Action/CreateShortcodeAction.php index 5384aac9..3cea7469 100644 --- a/module/Rest/src/Action/CreateShortcodeAction.php +++ b/module/Rest/src/Action/CreateShortcodeAction.php @@ -55,6 +55,7 @@ class CreateShortcodeAction extends AbstractRestAction * @param Request $request * @param DelegateInterface $delegate * @return null|Response + * @throws \InvalidArgumentException */ public function process(Request $request, DelegateInterface $delegate) { diff --git a/module/Rest/src/Action/EditShortcodeTagsAction.php b/module/Rest/src/Action/EditShortcodeTagsAction.php index 9eab0759..19f3a29d 100644 --- a/module/Rest/src/Action/EditShortcodeTagsAction.php +++ b/module/Rest/src/Action/EditShortcodeTagsAction.php @@ -46,6 +46,7 @@ class EditShortcodeTagsAction extends AbstractRestAction * @param Request $request * @param DelegateInterface $delegate * @return null|Response + * @throws \InvalidArgumentException */ public function process(Request $request, DelegateInterface $delegate) { diff --git a/module/Rest/src/Action/GetVisitsAction.php b/module/Rest/src/Action/GetVisitsAction.php index 681c9338..0a03436c 100644 --- a/module/Rest/src/Action/GetVisitsAction.php +++ b/module/Rest/src/Action/GetVisitsAction.php @@ -47,6 +47,7 @@ class GetVisitsAction extends AbstractRestAction * @param Request $request * @param DelegateInterface $delegate * @return null|Response + * @throws \InvalidArgumentException */ public function process(Request $request, DelegateInterface $delegate) { diff --git a/module/Rest/src/Action/ListShortcodesAction.php b/module/Rest/src/Action/ListShortcodesAction.php index 43803a7f..f4bf420b 100644 --- a/module/Rest/src/Action/ListShortcodesAction.php +++ b/module/Rest/src/Action/ListShortcodesAction.php @@ -48,6 +48,7 @@ class ListShortcodesAction extends AbstractRestAction * @param Request $request * @param DelegateInterface $delegate * @return null|Response + * @throws \InvalidArgumentException */ public function process(Request $request, DelegateInterface $delegate) { @@ -66,7 +67,7 @@ class ListShortcodesAction extends AbstractRestAction /** * @param array $query - * @return string + * @return array */ public function queryToListParams(array $query) { diff --git a/module/Rest/src/Action/ListTagsAction.php b/module/Rest/src/Action/ListTagsAction.php new file mode 100644 index 00000000..afee7df5 --- /dev/null +++ b/module/Rest/src/Action/ListTagsAction.php @@ -0,0 +1,51 @@ +tagService = $tagService; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * to the next middleware component to create the response. + * + * @param ServerRequestInterface $request + * @param DelegateInterface $delegate + * + * @return ResponseInterface + * @throws \InvalidArgumentException + */ + public function process(ServerRequestInterface $request, DelegateInterface $delegate) + { + return new JsonResponse([ + 'tags' => [ + 'data' => $this->tagService->listTags(), + ], + ]); + } +} diff --git a/module/Rest/src/Action/ResolveUrlAction.php b/module/Rest/src/Action/ResolveUrlAction.php index 55280922..f8167b16 100644 --- a/module/Rest/src/Action/ResolveUrlAction.php +++ b/module/Rest/src/Action/ResolveUrlAction.php @@ -46,6 +46,7 @@ class ResolveUrlAction extends AbstractRestAction * @param Request $request * @param DelegateInterface $delegate * @return null|Response + * @throws \InvalidArgumentException */ public function process(Request $request, DelegateInterface $delegate) { @@ -53,7 +54,7 @@ class ResolveUrlAction extends AbstractRestAction try { $longUrl = $this->urlShortener->shortCodeToUrl($shortCode); - if (! isset($longUrl)) { + if ($longUrl === null) { return new JsonResponse([ 'error' => RestUtils::INVALID_ARGUMENT_ERROR, 'message' => sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode), diff --git a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php index 0304a408..d7d922ad 100644 --- a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php @@ -9,6 +9,7 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Shlinkio\Shlink\Rest\Action\AuthenticateAction; use Shlinkio\Shlink\Rest\Authentication\JWTService; use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface; use Shlinkio\Shlink\Rest\Exception\AuthenticationException; @@ -69,7 +70,7 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn $routeResult = $request->getAttribute(RouteResult::class); if (! isset($routeResult) || $routeResult->isFailure() - || $routeResult->getMatchedRouteName() === 'rest-authenticate' + || $routeResult->getMatchedRouteName() === AuthenticateAction::class || $request->getMethod() === 'OPTIONS' ) { return $delegate->process($request); diff --git a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php index 65523f62..dcb905fe 100644 --- a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php +++ b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php @@ -5,6 +5,7 @@ use Interop\Http\ServerMiddleware\DelegateInterface; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\ObjectProphecy; +use Shlinkio\Shlink\Rest\Action\AuthenticateAction; use Shlinkio\Shlink\Rest\Authentication\JWTService; use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware; use ShlinkioTest\Shlink\Common\Util\TestUtils; @@ -56,7 +57,7 @@ class CheckAuthenticationMiddlewareTest extends TestCase $request = ServerRequestFactory::fromGlobals()->withAttribute( RouteResult::class, - RouteResult::fromRoute(new Route('foo', '', Route::HTTP_METHOD_ANY, 'rest-authenticate'), []) + RouteResult::fromRoute(new Route('foo', '', Route::HTTP_METHOD_ANY, AuthenticateAction::class)) ); $delegate = $this->prophesize(DelegateInterface::class); /** @var MethodProphecy $process */ From 5c7962966d6fa1e545362c3c2f0ee7945e0ba40f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 7 Jul 2017 13:28:58 +0200 Subject: [PATCH 35/55] Created ListTagsActionTest --- .../Factory/InstallApplicationFactoryTest.php | 2 - module/Core/src/Entity/Tag.php | 5 ++ .../Core/test/Service/Tag/TagServiceTest.php | 2 - .../Rest/test/Action/ListTagsActionTest.php | 50 +++++++++++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 module/Rest/test/Action/ListTagsActionTest.php diff --git a/module/CLI/test/Factory/InstallApplicationFactoryTest.php b/module/CLI/test/Factory/InstallApplicationFactoryTest.php index 35820bba..c7e87bc7 100644 --- a/module/CLI/test/Factory/InstallApplicationFactoryTest.php +++ b/module/CLI/test/Factory/InstallApplicationFactoryTest.php @@ -1,6 +1,4 @@ name = $name; + } + /** * @return string */ diff --git a/module/Core/test/Service/Tag/TagServiceTest.php b/module/Core/test/Service/Tag/TagServiceTest.php index 2a629c4f..d111afbb 100644 --- a/module/Core/test/Service/Tag/TagServiceTest.php +++ b/module/Core/test/Service/Tag/TagServiceTest.php @@ -1,6 +1,4 @@ tagService = $this->prophesize(TagServiceInterface::class); + $this->action = new ListTagsAction($this->tagService->reveal()); + } + + /** + * @test + */ + public function returnsDataFromService() + { + /** @var MethodProphecy $listTags */ + $listTags = $this->tagService->listTags()->willReturn([new Tag('foo'), new Tag('bar')]); + + $resp = $this->action->process( + ServerRequestFactory::fromGlobals(), + $this->prophesize(DelegateInterface::class)->reveal() + ); + + $this->assertEquals([ + 'tags' => [ + 'data' => ['foo', 'bar'], + ], + ], \json_decode((string) $resp->getBody(), true)); + $listTags->shouldHaveBeenCalled(); + } +} From caf4fa7fddcc3bc412e8acb2a4219644091ee362 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 8 Jul 2017 12:55:33 +0200 Subject: [PATCH 36/55] Documented list tags endpoint --- docs/swagger/paths/v1_tags.json | 53 +++++++++++++++++++++++++++++++++ docs/swagger/swagger.json | 15 +++++++--- 2 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 docs/swagger/paths/v1_tags.json diff --git a/docs/swagger/paths/v1_tags.json b/docs/swagger/paths/v1_tags.json new file mode 100644 index 00000000..6f5be49b --- /dev/null +++ b/docs/swagger/paths/v1_tags.json @@ -0,0 +1,53 @@ +{ + "get": { + "tags": [ + "Tags" + ], + "summary": "List existing tags", + "description": "Returns the list of all tags used in any short URL, ordered by name", + "parameters": [ + { + "$ref": "../parameters/Authorization.json" + } + ], + "responses": { + "200": { + "description": "The list of tags", + "schema": { + "type": "object", + "properties": { + "tags": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "examples": { + "application/json": { + "shortUrls": { + "data": [ + "games", + "php", + "shlink", + "tech" + ] + } + } + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + } +} diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 6e9244f7..b60ab79c 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -3,7 +3,7 @@ "info": { "title": "Shlink", "description": "Shlink, the self-hosted URL shortener", - "version": "1.2.0" + "version": "1.0" }, "schemes": [ "http", @@ -22,17 +22,24 @@ "/v1/authenticate": { "$ref": "paths/v1_authenticate.json" }, + "/v1/short-codes": { "$ref": "paths/v1_short-codes.json" }, "/v1/short-codes/{shortCode}": { "$ref": "paths/v1_short-codes_{shortCode}.json" }, - "/v1/short-codes/{shortCode}/visits": { - "$ref": "paths/v1_short-codes_{shortCode}_visits.json" - }, "/v1/short-codes/{shortCode}/tags": { "$ref": "paths/v1_short-codes_{shortCode}_tags.json" + }, + + "/v1/tags": { + "$ref": "paths/v1_tags.json" + }, + + + "/v1/short-codes/{shortCode}/visits": { + "$ref": "paths/v1_short-codes_{shortCode}_visits.json" } } } From 1ba7fc81acd4144245083f4a5e0aca5c17533c36 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 8 Jul 2017 13:17:46 +0200 Subject: [PATCH 37/55] Created ListTagsCommand --- module/CLI/config/cli.config.php | 1 + module/CLI/config/dependencies.config.php | 1 + .../CLI/src/Command/Tag/ListTagsCommand.php | 69 +++++++++++++++++++ module/Core/src/Service/Tag/TagService.php | 2 +- 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 module/CLI/src/Command/Tag/ListTagsCommand.php diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 8bd88607..53a5172b 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -17,6 +17,7 @@ return [ Command\Api\GenerateKeyCommand::class, Command\Api\DisableKeyCommand::class, Command\Api\ListKeysCommand::class, + Command\Tag\ListTagsCommand::class, ] ], diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 00e56607..6795852d 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -21,6 +21,7 @@ return [ Command\Api\GenerateKeyCommand::class => AnnotatedFactory::class, Command\Api\DisableKeyCommand::class => AnnotatedFactory::class, Command\Api\ListKeysCommand::class => AnnotatedFactory::class, + Command\Tag\ListTagsCommand::class => AnnotatedFactory::class, ], ], diff --git a/module/CLI/src/Command/Tag/ListTagsCommand.php b/module/CLI/src/Command/Tag/ListTagsCommand.php new file mode 100644 index 00000000..442706fd --- /dev/null +++ b/module/CLI/src/Command/Tag/ListTagsCommand.php @@ -0,0 +1,69 @@ +tagService = $tagService; + $this->translator = $translator; + } + + protected function configure() + { + $this + ->setName('tag:list') + ->setDescription('Lists existing tags'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $table = new Table($output); + $table->setHeaders([$this->translator->translate('Name')]) + ->setRows($this->getTagsRows()); + + $table->render(); + } + + private function getTagsRows() + { + $tags = $this->tagService->listTags(); + if (empty($tags)) { + return [[$this->translator->translate('No tags yet')]]; + } + + return array_map(function (Tag $tag) { + return [$tag->getName()]; + }, $tags); + } +} diff --git a/module/Core/src/Service/Tag/TagService.php b/module/Core/src/Service/Tag/TagService.php index 91cb5a72..5bc06ec5 100644 --- a/module/Core/src/Service/Tag/TagService.php +++ b/module/Core/src/Service/Tag/TagService.php @@ -29,6 +29,6 @@ class TagService implements TagServiceInterface */ public function listTags() { - return $this->em->getRepository(Tag::class)->findBy([], ['name' => 'DESC']); + return $this->em->getRepository(Tag::class)->findBy([], ['name' => 'ASC']); } } From 6717102dd2b4c60ec1f9be326be13ec4e547d328 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Jul 2017 08:31:21 +0200 Subject: [PATCH 38/55] Updated tag actions namespace --- docs/swagger/swagger.json | 1 - module/Core/src/Util/TagManagerTrait.php | 2 +- module/Rest/config/dependencies.config.php | 2 +- module/Rest/src/Action/{ => Tag}/ListTagsAction.php | 3 ++- module/Rest/test/Action/{ => Tag}/ListTagsActionTest.php | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename module/Rest/src/Action/{ => Tag}/ListTagsAction.php (93%) rename module/Rest/test/Action/{ => Tag}/ListTagsActionTest.php (92%) diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index b60ab79c..c7df5490 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -37,7 +37,6 @@ "$ref": "paths/v1_tags.json" }, - "/v1/short-codes/{shortCode}/visits": { "$ref": "paths/v1_short-codes_{shortCode}_visits.json" } diff --git a/module/Core/src/Util/TagManagerTrait.php b/module/Core/src/Util/TagManagerTrait.php index 9ca02b92..e20e1919 100644 --- a/module/Core/src/Util/TagManagerTrait.php +++ b/module/Core/src/Util/TagManagerTrait.php @@ -26,7 +26,7 @@ trait TagManagerTrait } /** - * Tag names are trimmed, lowercased and spaces are replaced by dashes + * Tag names are trimmed, lower cased and spaces are replaced by dashes * * @param string $tagName * @return string diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index d48d319b..9428075e 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -19,7 +19,7 @@ return [ Action\GetVisitsAction::class => AnnotatedFactory::class, Action\ListShortcodesAction::class => AnnotatedFactory::class, Action\EditShortcodeTagsAction::class => AnnotatedFactory::class, - Action\ListTagsAction::class => AnnotatedFactory::class, + Action\Tag\ListTagsAction::class => AnnotatedFactory::class, Middleware\BodyParserMiddleware::class => AnnotatedFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, diff --git a/module/Rest/src/Action/ListTagsAction.php b/module/Rest/src/Action/Tag/ListTagsAction.php similarity index 93% rename from module/Rest/src/Action/ListTagsAction.php rename to module/Rest/src/Action/Tag/ListTagsAction.php index afee7df5..acfa0846 100644 --- a/module/Rest/src/Action/ListTagsAction.php +++ b/module/Rest/src/Action/Tag/ListTagsAction.php @@ -1,5 +1,5 @@ Date: Sat, 15 Jul 2017 09:00:53 +0200 Subject: [PATCH 39/55] Added Create and Delete tag actions --- docs/swagger/paths/v1_tags.json | 60 ++++++++++++++++++- module/Core/src/Entity/Tag.php | 3 +- module/Core/src/Repository/TagRepository.php | 29 +++++++++ .../src/Repository/TagRepositoryInterface.php | 17 ++++++ module/Core/src/Service/Tag/TagService.php | 30 ++++++++++ .../src/Service/Tag/TagServiceInterface.php | 15 +++++ module/Rest/config/dependencies.config.php | 2 + module/Rest/config/routes.config.php | 31 +++++++--- .../Rest/src/Action/Tag/CreateTagsAction.php | 57 ++++++++++++++++++ .../Rest/src/Action/Tag/DeleteTagsAction.php | 53 ++++++++++++++++ 10 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 module/Core/src/Repository/TagRepository.php create mode 100644 module/Core/src/Repository/TagRepositoryInterface.php create mode 100644 module/Rest/src/Action/Tag/CreateTagsAction.php create mode 100644 module/Rest/src/Action/Tag/DeleteTagsAction.php diff --git a/docs/swagger/paths/v1_tags.json b/docs/swagger/paths/v1_tags.json index 6f5be49b..92005b12 100644 --- a/docs/swagger/paths/v1_tags.json +++ b/docs/swagger/paths/v1_tags.json @@ -31,7 +31,65 @@ }, "examples": { "application/json": { - "shortUrls": { + "tags": { + "data": [ + "games", + "php", + "shlink", + "tech" + ] + } + } + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + }, + "post": { + "tags": [ + "Tags" + ], + "summary": "Create tags", + "description": "Provided a list of tags, creates all that do not yet exist", + "parameters": [ + { + "$ref": "../parameters/Authorization.json" + }, + { + "name": "tags[]", + "in": "formData", + "description": "The list of tag names to create", + "required": true, + "type": "array" + } + ], + "responses": { + "200": { + "description": "The list of tags", + "schema": { + "type": "object", + "properties": { + "tags": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "examples": { + "application/json": { + "tags": { "data": [ "games", "php", diff --git a/module/Core/src/Entity/Tag.php b/module/Core/src/Entity/Tag.php index ba212ebc..7537b8c0 100644 --- a/module/Core/src/Entity/Tag.php +++ b/module/Core/src/Entity/Tag.php @@ -3,13 +3,14 @@ namespace Shlinkio\Shlink\Core\Entity; use Doctrine\ORM\Mapping as ORM; use Shlinkio\Shlink\Common\Entity\AbstractEntity; +use Shlinkio\Shlink\Core\Repository\TagRepository; /** * Class Tag * @author * @link * - * @ORM\Entity() + * @ORM\Entity(repositoryClass=TagRepository::class) * @ORM\Table(name="tags") */ class Tag extends AbstractEntity implements \JsonSerializable diff --git a/module/Core/src/Repository/TagRepository.php b/module/Core/src/Repository/TagRepository.php new file mode 100644 index 00000000..a4254cad --- /dev/null +++ b/module/Core/src/Repository/TagRepository.php @@ -0,0 +1,29 @@ +getEntityManager()->createQueryBuilder(); + $qb->delete(Tag::class, 't') + ->where($qb->expr()->in('t.name', $names)); + + return $qb->getQuery()->execute(); + } +} diff --git a/module/Core/src/Repository/TagRepositoryInterface.php b/module/Core/src/Repository/TagRepositoryInterface.php new file mode 100644 index 00000000..23eb8128 --- /dev/null +++ b/module/Core/src/Repository/TagRepositoryInterface.php @@ -0,0 +1,17 @@ +em->getRepository(Tag::class)->findBy([], ['name' => 'ASC']); } + + /** + * @param array $tagNames + * @return void + */ + public function deleteTags(array $tagNames) + { + /** @var TagRepository $repo */ + $repo = $this->em->getRepository(Tag::class); + $repo->deleteByName($tagNames); + } + + /** + * Provided a list of tag names, creates all that do not exist yet + * + * @param string[] $tagNames + * @return Collection|Tag[] + */ + public function createTags(array $tagNames) + { + $tags = $this->tagNamesToEntities($this->em, $tagNames); + $this->em->flush(); + + return $tags; + } } diff --git a/module/Core/src/Service/Tag/TagServiceInterface.php b/module/Core/src/Service/Tag/TagServiceInterface.php index cf1914f4..996a914c 100644 --- a/module/Core/src/Service/Tag/TagServiceInterface.php +++ b/module/Core/src/Service/Tag/TagServiceInterface.php @@ -1,6 +1,7 @@ AnnotatedFactory::class, Action\EditShortcodeTagsAction::class => AnnotatedFactory::class, Action\Tag\ListTagsAction::class => AnnotatedFactory::class, + Action\Tag\DeleteTagsAction::class => AnnotatedFactory::class, + Action\Tag\CreateTagsAction::class => AnnotatedFactory::class, Middleware\BodyParserMiddleware::class => AnnotatedFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 6212e6a8..648008c5 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -1,5 +1,6 @@ Action\AuthenticateAction::class, 'path' => '/rest/v{version:1}/authenticate', 'middleware' => Action\AuthenticateAction::class, - 'allowed_methods' => ['POST'], + 'allowed_methods' => [RequestMethod::METHOD_POST], ], // Short codes @@ -16,25 +17,25 @@ return [ 'name' => Action\CreateShortcodeAction::class, 'path' => '/rest/v{version:1}/short-codes', 'middleware' => Action\CreateShortcodeAction::class, - 'allowed_methods' => ['POST'], + 'allowed_methods' => [RequestMethod::METHOD_POST], ], [ 'name' => Action\ResolveUrlAction::class, 'path' => '/rest/v{version:1}/short-codes/{shortCode}', 'middleware' => Action\ResolveUrlAction::class, - 'allowed_methods' => ['GET'], + 'allowed_methods' => [RequestMethod::METHOD_GET], ], [ 'name' => Action\ListShortcodesAction::class, 'path' => '/rest/v{version:1}/short-codes', 'middleware' => Action\ListShortcodesAction::class, - 'allowed_methods' => ['GET'], + 'allowed_methods' => [RequestMethod::METHOD_GET], ], [ 'name' => Action\EditShortcodeTagsAction::class, 'path' => '/rest/v{version:1}/short-codes/{shortCode}/tags', 'middleware' => Action\EditShortcodeTagsAction::class, - 'allowed_methods' => ['PUT'], + 'allowed_methods' => [RequestMethod::METHOD_PUT], ], // Visits @@ -42,15 +43,27 @@ return [ 'name' => Action\GetVisitsAction::class, 'path' => '/rest/v{version:1}/short-codes/{shortCode}/visits', 'middleware' => Action\GetVisitsAction::class, - 'allowed_methods' => ['GET'], + 'allowed_methods' => [RequestMethod::METHOD_GET], ], // Tags [ - 'name' => Action\ListTagsAction::class, + 'name' => Action\Tag\ListTagsAction::class, 'path' => '/rest/v{version:1}/tags', - 'middleware' => Action\ListTagsAction::class, - 'allowed_methods' => ['GET'], + 'middleware' => Action\Tag\ListTagsAction::class, + 'allowed_methods' => [RequestMethod::METHOD_GET], + ], + [ + 'name' => Action\Tag\DeleteTagsAction::class, + 'path' => '/rest/v{version:1}/tags', + 'middleware' => Action\Tag\DeleteTagsAction::class, + 'allowed_methods' => [RequestMethod::METHOD_DELETE], + ], + [ + 'name' => Action\Tag\CreateTagsAction::class, + 'path' => '/rest/v{version:1}/tags', + 'middleware' => Action\Tag\CreateTagsAction::class, + 'allowed_methods' => [RequestMethod::METHOD_POST], ], ], diff --git a/module/Rest/src/Action/Tag/CreateTagsAction.php b/module/Rest/src/Action/Tag/CreateTagsAction.php new file mode 100644 index 00000000..b5496f75 --- /dev/null +++ b/module/Rest/src/Action/Tag/CreateTagsAction.php @@ -0,0 +1,57 @@ +tagService = $tagService; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * to the next middleware component to create the response. + * + * @param ServerRequestInterface $request + * @param DelegateInterface $delegate + * + * @return ResponseInterface + * @throws \InvalidArgumentException + */ + public function process(ServerRequestInterface $request, DelegateInterface $delegate) + { + $body = $request->getParsedBody(); + $tags = isset($body['tags']) ? $body['tags'] : []; + + return new JsonResponse([ + 'tags' => [ + 'data' => $this->tagService->createTags($tags)->toArray(), + ], + ]); + } +} diff --git a/module/Rest/src/Action/Tag/DeleteTagsAction.php b/module/Rest/src/Action/Tag/DeleteTagsAction.php new file mode 100644 index 00000000..f579e9b8 --- /dev/null +++ b/module/Rest/src/Action/Tag/DeleteTagsAction.php @@ -0,0 +1,53 @@ +tagService = $tagService; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * to the next middleware component to create the response. + * + * @param ServerRequestInterface $request + * @param DelegateInterface $delegate + * + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, DelegateInterface $delegate) + { + $query = $request->getQueryParams(); + $tags = isset($query['tags']) ? $query['tags'] : []; + + $this->tagService->deleteTags($tags); + return new EmptyResponse(); + } +} From 3e268e2012aaedd6edf73eb45f8c075e2c7afdbf Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Jul 2017 09:05:02 +0200 Subject: [PATCH 40/55] Improved TagServiceTest --- .../Core/test/Service/Tag/TagServiceTest.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/module/Core/test/Service/Tag/TagServiceTest.php b/module/Core/test/Service/Tag/TagServiceTest.php index d111afbb..206964a8 100644 --- a/module/Core/test/Service/Tag/TagServiceTest.php +++ b/module/Core/test/Service/Tag/TagServiceTest.php @@ -7,6 +7,7 @@ use Prophecy\Argument; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\Tag; +use Shlinkio\Shlink\Core\Repository\TagRepository; use Shlinkio\Shlink\Core\Service\Tag\TagService; use PHPUnit\Framework\TestCase; @@ -46,4 +47,45 @@ class TagServiceTest extends TestCase $find->shouldHaveBeenCalled(); $getRepo->shouldHaveBeenCalled(); } + + /** + * @test + */ + public function deleteTagsDelegatesOnRepository() + { + $repo = $this->prophesize(TagRepository::class); + /** @var MethodProphecy $delete */ + $delete = $repo->deleteByName(['foo', 'bar'])->willReturn(4); + /** @var MethodProphecy $getRepo */ + $getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal()); + + $this->service->deleteTags(['foo', 'bar']); + + $delete->shouldHaveBeenCalled(); + $getRepo->shouldHaveBeenCalled(); + } + + /** + * @test + */ + public function createTagsPersistsEntities() + { + $repo = $this->prophesize(TagRepository::class); + /** @var MethodProphecy $find */ + $find = $repo->findOneBy(Argument::cetera())->willReturn(new Tag()); + /** @var MethodProphecy $getRepo */ + $getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal()); + /** @var MethodProphecy $persist */ + $persist = $this->em->persist(Argument::type(Tag::class))->willReturn(null); + /** @var MethodProphecy $flush */ + $flush = $this->em->flush()->willReturn(null); + + $result = $this->service->createTags(['foo', 'bar']); + + $this->assertCount(2, $result); + $find->shouldHaveBeenCalled(); + $getRepo->shouldHaveBeenCalled(); + $persist->shouldHaveBeenCalledTimes(2); + $flush->shouldHaveBeenCalled(); + } } From 563e654b998acb4133f24cc2dd47fc3e213e4f43 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Jul 2017 09:10:09 +0200 Subject: [PATCH 41/55] Created DeleteTagsActionTest --- .../test/Action/Tag/DeleteTagsActionTest.php | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 module/Rest/test/Action/Tag/DeleteTagsActionTest.php diff --git a/module/Rest/test/Action/Tag/DeleteTagsActionTest.php b/module/Rest/test/Action/Tag/DeleteTagsActionTest.php new file mode 100644 index 00000000..0a0657bf --- /dev/null +++ b/module/Rest/test/Action/Tag/DeleteTagsActionTest.php @@ -0,0 +1,57 @@ +tagService = $this->prophesize(TagServiceInterface::class); + $this->action = new DeleteTagsAction($this->tagService->reveal()); + } + + /** + * @test + * @dataProvider provideTags + * @param array|null $tags + */ + public function processDelegatesIntoService($tags) + { + $request = ServerRequestFactory::fromGlobals()->withQueryParams(['tags' => $tags]); + /** @var MethodProphecy $deleteTags */ + $deleteTags = $this->tagService->deleteTags($tags ?: []); + + $response = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); + + $this->assertEquals(204, $response->getStatusCode()); + $deleteTags->shouldHaveBeenCalled(); + } + + public function provideTags() + { + return [ + [['foo', 'bar', 'baz']], + [['some', 'thing']], + [null], + [[]], + ]; + } +} From 575509c45b797be79a011d16ac2643b9d3965749 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Jul 2017 09:12:07 +0200 Subject: [PATCH 42/55] Created CreateTagsActiontest --- .../test/Action/Tag/CreateTagsActionTest.php | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 module/Rest/test/Action/Tag/CreateTagsActionTest.php diff --git a/module/Rest/test/Action/Tag/CreateTagsActionTest.php b/module/Rest/test/Action/Tag/CreateTagsActionTest.php new file mode 100644 index 00000000..795827fa --- /dev/null +++ b/module/Rest/test/Action/Tag/CreateTagsActionTest.php @@ -0,0 +1,58 @@ +tagService = $this->prophesize(TagServiceInterface::class); + $this->action = new CreateTagsAction($this->tagService->reveal()); + } + + /** + * @test + * @dataProvider provideTags + * @param array|null $tags + */ + public function processDelegatesIntoService($tags) + { + $request = ServerRequestFactory::fromGlobals()->withParsedBody(['tags' => $tags]); + /** @var MethodProphecy $deleteTags */ + $deleteTags = $this->tagService->createTags($tags ?: [])->willReturn(new ArrayCollection()); + + $response = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); + + $this->assertEquals(200, $response->getStatusCode()); + $deleteTags->shouldHaveBeenCalled(); + } + + public function provideTags() + { + return [ + [['foo', 'bar', 'baz']], + [['some', 'thing']], + [null], + [[]], + ]; + } +} From e07c464de8f417ccb0ee097cf8f58dfc132b05e7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Jul 2017 09:15:45 +0200 Subject: [PATCH 43/55] Removed strict declarations --- module/CLI/src/Command/Tag/ListTagsCommand.php | 2 -- module/Core/src/Repository/TagRepository.php | 2 -- module/Core/src/Repository/TagRepositoryInterface.php | 2 -- module/Rest/src/Action/Tag/CreateTagsAction.php | 2 -- module/Rest/src/Action/Tag/DeleteTagsAction.php | 2 -- module/Rest/test/Action/Tag/CreateTagsActionTest.php | 2 -- module/Rest/test/Action/Tag/DeleteTagsActionTest.php | 2 -- 7 files changed, 14 deletions(-) diff --git a/module/CLI/src/Command/Tag/ListTagsCommand.php b/module/CLI/src/Command/Tag/ListTagsCommand.php index 442706fd..201b09ae 100644 --- a/module/CLI/src/Command/Tag/ListTagsCommand.php +++ b/module/CLI/src/Command/Tag/ListTagsCommand.php @@ -1,6 +1,4 @@ Date: Sat, 15 Jul 2017 12:04:12 +0200 Subject: [PATCH 44/55] Created UpdateTagAction --- .../Exception/EntityDoesNotExistException.php | 26 ++++++ module/Core/src/Service/Tag/TagService.php | 22 +++++ .../src/Service/Tag/TagServiceInterface.php | 9 ++ module/Rest/config/dependencies.config.php | 1 + module/Rest/config/routes.config.php | 6 ++ .../Rest/src/Action/Tag/UpdateTagAction.php | 83 +++++++++++++++++ .../test/Action/Tag/UpdateTagActionTest.php | 89 +++++++++++++++++++ 7 files changed, 236 insertions(+) create mode 100644 module/Core/src/Exception/EntityDoesNotExistException.php create mode 100644 module/Rest/src/Action/Tag/UpdateTagAction.php create mode 100644 module/Rest/test/Action/Tag/UpdateTagActionTest.php diff --git a/module/Core/src/Exception/EntityDoesNotExistException.php b/module/Core/src/Exception/EntityDoesNotExistException.php new file mode 100644 index 00000000..27825690 --- /dev/null +++ b/module/Core/src/Exception/EntityDoesNotExistException.php @@ -0,0 +1,26 @@ + $value) { + $result[] = sprintf('"%s" => "%s"', $key, $value); + } + + return implode(', ', $result); + } +} diff --git a/module/Core/src/Service/Tag/TagService.php b/module/Core/src/Service/Tag/TagService.php index 897cf552..52708a39 100644 --- a/module/Core/src/Service/Tag/TagService.php +++ b/module/Core/src/Service/Tag/TagService.php @@ -5,6 +5,7 @@ use Acelaya\ZsmAnnotatedServices\Annotation as DI; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; use Shlinkio\Shlink\Core\Entity\Tag; +use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Repository\TagRepository; use Shlinkio\Shlink\Core\Util\TagManagerTrait; @@ -61,4 +62,25 @@ class TagService implements TagServiceInterface return $tags; } + + /** + * @param string $oldName + * @param string $newName + * @return Tag + * @throws EntityDoesNotExistException + */ + public function renameTag($oldName, $newName) + { + $criteria = ['name' => $oldName]; + /** @var Tag|null $tag */ + $tag = $this->em->getRepository(Tag::class)->findOneBy($criteria); + if ($tag === null) { + throw EntityDoesNotExistException::createFromEntityAndConditions(Tag::class, $criteria); + } + + $tag->setName($newName); + $this->em->flush($tag); + + return $tag; + } } diff --git a/module/Core/src/Service/Tag/TagServiceInterface.php b/module/Core/src/Service/Tag/TagServiceInterface.php index 996a914c..48714309 100644 --- a/module/Core/src/Service/Tag/TagServiceInterface.php +++ b/module/Core/src/Service/Tag/TagServiceInterface.php @@ -3,6 +3,7 @@ namespace Shlinkio\Shlink\Core\Service\Tag; use Doctrine\Common\Collections\Collection; use Shlinkio\Shlink\Core\Entity\Tag; +use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; interface TagServiceInterface { @@ -24,4 +25,12 @@ interface TagServiceInterface * @return Collection|Tag[] */ public function createTags(array $tagNames); + + /** + * @param string $oldName + * @param string $newName + * @return Tag + * @throws EntityDoesNotExistException + */ + public function renameTag($oldName, $newName); } diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 3219a057..a36f7590 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -22,6 +22,7 @@ return [ Action\Tag\ListTagsAction::class => AnnotatedFactory::class, Action\Tag\DeleteTagsAction::class => AnnotatedFactory::class, Action\Tag\CreateTagsAction::class => AnnotatedFactory::class, + Action\Tag\UpdateTagAction::class => AnnotatedFactory::class, Middleware\BodyParserMiddleware::class => AnnotatedFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 648008c5..0922e18a 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -65,6 +65,12 @@ return [ 'middleware' => Action\Tag\CreateTagsAction::class, 'allowed_methods' => [RequestMethod::METHOD_POST], ], + [ + 'name' => Action\Tag\UpdateTagAction::class, + 'path' => '/rest/v{version:1}/tags', + 'middleware' => Action\Tag\UpdateTagAction::class, + 'allowed_methods' => [RequestMethod::METHOD_PUT], + ], ], ]; diff --git a/module/Rest/src/Action/Tag/UpdateTagAction.php b/module/Rest/src/Action/Tag/UpdateTagAction.php new file mode 100644 index 00000000..40595691 --- /dev/null +++ b/module/Rest/src/Action/Tag/UpdateTagAction.php @@ -0,0 +1,83 @@ +tagService = $tagService; + $this->translator = $translator; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * to the next middleware component to create the response. + * + * @param ServerRequestInterface $request + * @param DelegateInterface $delegate + * + * @return ResponseInterface + * @throws \InvalidArgumentException + */ + public function process(ServerRequestInterface $request, DelegateInterface $delegate) + { + $body = $request->getParsedBody(); + if (! isset($body['oldName'], $body['newName'])) { + return new JsonResponse([ + 'error' => RestUtils::INVALID_ARGUMENT_ERROR, + 'message' => $this->translator->translate( + 'You have to provide both \'oldName\' and \'newName\' params in order to properly rename the tag' + ), + ], self::STATUS_BAD_REQUEST); + } + + try { + $this->tagService->renameTag($body['oldName'], $body['newName']); + return new EmptyResponse(); + } catch (EntityDoesNotExistException $e) { + return new JsonResponse([ + 'error' => RestUtils::NOT_FOUND_ERROR, + 'message' => sprintf( + $this->translator->translate('It wasn\'t possible to find a tag with name \'%s\''), + $body['oldName'] + ), + ], self::STATUS_NOT_FOUND); + } + } +} diff --git a/module/Rest/test/Action/Tag/UpdateTagActionTest.php b/module/Rest/test/Action/Tag/UpdateTagActionTest.php new file mode 100644 index 00000000..52b2274b --- /dev/null +++ b/module/Rest/test/Action/Tag/UpdateTagActionTest.php @@ -0,0 +1,89 @@ +tagService = $this->prophesize(TagServiceInterface::class); + $this->action = new UpdateTagAction($this->tagService->reveal(), Translator::factory([])); + } + + /** + * @test + * @dataProvider provideParams + * @param array $bodyParams + */ + public function whenInvalidParamsAreProvidedAnErrorIsReturned(array $bodyParams) + { + $request = ServerRequestFactory::fromGlobals()->withParsedBody($bodyParams); + $resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); + + $this->assertEquals(400, $resp->getStatusCode()); + } + + public function provideParams() + { + return [ + [['oldName' => 'foo']], + [['newName' => 'foo']], + [[]], + ]; + } + + /** + * @test + */ + public function requestingInvalidTagReturnsError() + { + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'oldName' => 'foo', + 'newName' => 'bar', + ]); + /** @var MethodProphecy $rename */ + $rename = $this->tagService->renameTag('foo', 'bar')->willThrow(EntityDoesNotExistException::class); + + $resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); + + $this->assertEquals(404, $resp->getStatusCode()); + $rename->shouldHaveBeenCalled(); + } + + /** + * @test + */ + public function correctInvocationRenamesTag() + { + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'oldName' => 'foo', + 'newName' => 'bar', + ]); + /** @var MethodProphecy $rename */ + $rename = $this->tagService->renameTag('foo', 'bar')->willReturn(new Tag()); + + $resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); + + $this->assertEquals(204, $resp->getStatusCode()); + $rename->shouldHaveBeenCalled(); + } +} From 286c24f8c02efe660c5eb382bf582435db5197fb Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Jul 2017 12:09:25 +0200 Subject: [PATCH 45/55] Improved TagServiceTest --- .../Core/test/Service/Tag/TagServiceTest.php | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/module/Core/test/Service/Tag/TagServiceTest.php b/module/Core/test/Service/Tag/TagServiceTest.php index 206964a8..fcae18d0 100644 --- a/module/Core/test/Service/Tag/TagServiceTest.php +++ b/module/Core/test/Service/Tag/TagServiceTest.php @@ -7,6 +7,7 @@ use Prophecy\Argument; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\Tag; +use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Repository\TagRepository; use Shlinkio\Shlink\Core\Service\Tag\TagService; use PHPUnit\Framework\TestCase; @@ -88,4 +89,46 @@ class TagServiceTest extends TestCase $persist->shouldHaveBeenCalledTimes(2); $flush->shouldHaveBeenCalled(); } + + /** + * @test + */ + public function renameInvalidTagThrowsException() + { + $repo = $this->prophesize(TagRepository::class); + /** @var MethodProphecy $find */ + $find = $repo->findOneBy(Argument::cetera())->willReturn(null); + /** @var MethodProphecy $getRepo */ + $getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal()); + + $find->shouldBeCalled(); + $getRepo->shouldBeCalled(); + $this->expectException(EntityDoesNotExistException::class); + + $this->service->renameTag('foo', 'bar'); + } + + /** + * @test + */ + public function renameValidTagChangesItsName() + { + $expected = new Tag(); + + $repo = $this->prophesize(TagRepository::class); + /** @var MethodProphecy $find */ + $find = $repo->findOneBy(Argument::cetera())->willReturn($expected); + /** @var MethodProphecy $getRepo */ + $getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal()); + /** @var MethodProphecy $flush */ + $flush = $this->em->flush($expected)->willReturn(null); + + $tag = $this->service->renameTag('foo', 'bar'); + + $this->assertSame($expected, $tag); + $this->assertEquals('bar', $tag->getName()); + $find->shouldHaveBeenCalled(); + $getRepo->shouldHaveBeenCalled(); + $flush->shouldHaveBeenCalled(); + } } From 8d0bac9478090ede2a102c2175726e5b8cc5e92c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Jul 2017 12:13:59 +0200 Subject: [PATCH 46/55] Documented delete and edit tags endpoints --- docs/swagger/paths/v1_tags.json | 82 +++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/docs/swagger/paths/v1_tags.json b/docs/swagger/paths/v1_tags.json index 92005b12..9f7cefb2 100644 --- a/docs/swagger/paths/v1_tags.json +++ b/docs/swagger/paths/v1_tags.json @@ -50,6 +50,7 @@ } } }, + "post": { "tags": [ "Tags" @@ -107,5 +108,86 @@ } } } + }, + + "put": { + "tags": [ + "Tags" + ], + "summary": "Rename tag", + "description": "Renames one existing tag", + "parameters": [ + { + "$ref": "../parameters/Authorization.json" + }, + { + "name": "oldName", + "in": "formData", + "description": "Current name of the tag", + "required": true, + "type": "string" + }, + { + "name": "newName", + "in": "formData", + "description": "New name of the tag", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": "The tag has been properly renamed" + }, + "400": { + "description": "You have not provided either the oldName or the newName params.", + "schema": { + "$ref": "../definitions/Error.json" + } + }, + "404": { + "description": "There's no tag found with the name provided in oldName param.", + "schema": { + "$ref": "../definitions/Error.json" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + }, + + "delete": { + "tags": [ + "Tags" + ], + "summary": "Delete tags", + "description": "Deletes provided list of tags", + "parameters": [ + { + "$ref": "../parameters/Authorization.json" + }, + { + "name": "tags[]", + "in": "query", + "description": "The names of the tags to delete", + "required": true, + "type": "array" + } + ], + "responses": { + "204": { + "description": "Tags properly deleted" + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } } } From c8368c90985fa665d70d1f1ee28125f9036d5a76 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Jul 2017 12:16:15 +0200 Subject: [PATCH 47/55] Updated language files --- module/CLI/lang/es.mo | Bin 6739 -> 6782 bytes module/CLI/lang/es.po | 13 ++++++++++--- module/Rest/lang/es.mo | Bin 2283 -> 2634 bytes module/Rest/lang/es.po | 17 ++++++++++++++--- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index 3365694abb1e2e8bd51fda869dbb18fc3c7e86c0..2684b41b1ceb41eb5b9b4ef1e1507baccd9d037c 100644 GIT binary patch delta 1481 zcmYk+OGs2v9LMpaj#`?pQA_jD9baWOdS`TejA@!0^t6ZcLO}~rA$6fef;6=#Y!*?7 zAhgLv5N!;kO%yF6gjR*sqQV{&2rU#0+C|^roe4U4?&r*zJLmlG|9`GaHGgVC%Wl_< zp|#NS)nko0BVAl*SL2P@jMs1r&Z3Qva1VaPljun>W*=V0c6^Rn&y^U_gZh0lw&F=_ zGbUta8Kg4t3ElV;wf7&~gmD{;$-;DGFH?$XSb=%ih}!5BX5ct#y&E`%Ggyd)8;v=D zO{f5-Fqi$!D+WnSe8)Ik#Y|jB4<_;@LsO0xR%0@Dp#tnhZ8(g5IEj_Gj47DH&3b@5 ztV0{Ca1ayO-^?=50{2iUoyQ%xiu{=zc3~}3g33%47GVwg@f4~=VN?m`uoG8sC)N{1 zHl9b7U;;IN2SZx;DFf~`i>QU);#T~PX`BZ)(dF>DC_R#qMx=J82U~Cs&*C>cgiX|; z1H-rtUtW8JM`L^i%epIHqu^g|X zo^&3G-MmDd?jP?>s&L41t5e+`wn z81kH5TYQwDdr-Q9W#l?@D}>;3u<9EQEDL%>M&NJ zQrdufu^)BHAK+cO5x=R+SDXyXhDu2dJJQdNr<_FxREwa|61 zGNe`?!O1{$=wyo}or7A9e!2qHLl)6Vm}#c(rgJu?2V5!HDk1SYt(dFg*gk#Jl{c*S zM1>~^Cwx7kT7YiRsjE{56~B)Duf;M@ewFiv=%fdO(Xrn1wCGrAR%@Ehh|lb>BOjb= zBUdGh|3pp_OVCSKD~;g1=@ipxHC%Vm%NB;>Kf02Sj9TM;17p@i|M)^%(n?&Q&TH3s zeF4ko5BdTXw%@jsj&^#F_g@+s8yX$)w)c(q2Q9zt`~SY$>E6`$asEnsp)u`lLVD|q UCnMI#s5RI(;Vi!JF8hk>FA_|Pg#Z8m delta 1432 zcmYk+OGs2v9LMp$I%784n9nq=MlH*1#+mE0v4{B#G0a|65Gf@jl_`}}C~lHmMuUs6 zg{w9$Tv$;pqeYRMFoY0lA>=N|%3MVC{mm$Jm~%hJ!@cK!?*D(Te=24xhCijd#*B85 zo=?w=H#>tVE-th_w^=s!<0c%%AWop3d5@d^0wf z4cjOKFB3DEj`OJ4Uoi#0Va2L)ZKl{yvXDx|n%18zlVh%c3iz-boD)l4Sjx)F$^VvlfwxCLP z4mE!hweAECbFWP@(8AAgGtOfg=U^Fg_&aH1EE5Gt&8z|I@E)GUH`s|Jf=j3p zoTnbzz+)taJ;6GBiz=~)I`BbQE0PrJ7-rDH-~xv5DQe+G)I!Us!{=hVN~IU;F^oFZ zLwFh=;Sv0aRoIXltN9(&3q3&1PoV;TL1nUMg@IC(OyPa#U>vGx$GTaWQLBy-&A{p~ zntqmu9#GBcC6qb6Wg(r!*nWC#q~Dd1wUZG$jJ6`K+Cp*Up)2pIz8saB@X^Dnms6{t z`{-0MS}B!XFS6F+862Q1+cm3`1-N&0tS?&sF4UYDwl7lP&S=uNxYkr#rAgmqB|S*r zLs#1#!~94Yqph5)%CTemwtL<+-RW6OsI2mbf_}&GIpyJyQyO%FLC?{4|FQ0Yp6fl= mdi{-EH@d?\n" "Language-Team: \n" "Language: es_ES\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 1.8.7.1\n" +"X-Generator: Poedit 2.0.1\n" "X-Poedit-Basepath: ..\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Poedit-SourceCharset: UTF-8\n" @@ -223,6 +223,13 @@ msgstr "URL larga:" msgid "Provided short code \"%s\" has an invalid format." msgstr "El código corto proporcionado \"%s\" tiene un formato inválido." +#, fuzzy +msgid "Name" +msgstr "Nombre" + +msgid "No tags yet" +msgstr "Aún no hay tags" + msgid "Processes visits where location is not set yet" msgstr "Procesa las visitas donde la localización no ha sido establecida aún" diff --git a/module/Rest/lang/es.mo b/module/Rest/lang/es.mo index 070394c048e2f126405912a9b4245104227e4eb2..139dbf7e3c6ef2149c6c6effb01d27e7e1d0403b 100644 GIT binary patch delta 702 zcmY+AyKfUg5QoPPOn{h(h=&lbZK6x#IJV+gN-7Wy4Fv^3LKJIzW1n>1HS0|*Bq00& zbOuq-K#iy(8YHSzKtn}AK|=?kq@&4u9;QFBU`=$T#45_y}GT*^w6n zcRBb5W8-QOF*IfHGWo-jNCm!xb@&l}fPdi{e1BA=3ID*Wurf29=U~h~!`NRwHeGNX z-XgyPQ|XGlAz0zy%yAKxkun}FzDP9Y$_e}=K8sT>lnQeNoq~>up;y@9z>XY51mj9> zy?P_4ksjJ4sDT>}(^h6waB4HP9i>&!T{TKWPg&hJDp?pNyWQMeexs+iCuIZgw$qML zEk{k_vd&HF5~XQQtQk$Sf%dvTRH;?YcZ`qY=^GfIJy70ECWjv5ba#Bekd>R2J*ZEV}w;I&s%Ixz{U^Td`lN@{b43)6*f@#{=#5X)cHWzIU}qc5_}$FvKTXw+ A)&Kwi delta 377 zcmZ9|Juk&z7{>9d^|Xh>kyJP#-cBSEOMA{EXj2nG%vPfavytH}MqA@Mm>8NEB*J1b znVML90TPp$|5GM+a^?5D_HO$;{vP$;#>}Ot7MUUEBqATgRnL>!XmK8Q@dA&rgI}0n z+m}{x8@KQb-TEUg;wMhw#DKJkUF3XUCk!GsuW*iyyFiNZ74NYTN;y7ZifTw3kl{X_ z{>>|N_a+B_KV%2@m>=K@-XiZ>7wzh5sw<5!q<3BP)G%>>gXf7m8X|_c3X4N;y?pUT s{USF\n" "Language-Team: \n" "Language: es_ES\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 1.8.7.1\n" +"X-Generator: Poedit 2.0.1\n" "X-Poedit-Basepath: ..\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Poedit-SourceCharset: UTF-8\n" @@ -50,6 +50,17 @@ msgstr "El código corto \"%s\" proporcionado no existe" msgid "Provided short code \"%s\" has an invalid format" msgstr "El código corto proporcionado \"%s\" tiene un formato no inválido" +msgid "" +"You have to provide both 'oldName' and 'newName' params in order to properly " +"rename the tag" +msgstr "" +"Debes proporcionar tanto el parámetro 'oldName' como 'newName' para poder " +"renombrar el tag correctamente" + +#, php-format +msgid "It wasn't possible to find a tag with name '%s'" +msgstr "No fue posible encontrar un tag con el nombre '%s'" + #, php-format msgid "You need to provide the Bearer type in the %s header." msgstr "Debes proporcionar el typo Bearer en la cabecera %s." From b37f303e760403c9934d89a5b0dc3e6d88a47122 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Jul 2017 09:09:11 +0200 Subject: [PATCH 48/55] Created CreateTagCommand --- module/CLI/config/cli.config.php | 1 + module/CLI/config/dependencies.config.php | 1 + .../CLI/src/Command/Api/DisableKeyCommand.php | 2 +- .../CLI/src/Command/Tag/CreateTagCommand.php | 69 +++++++++++++++++++ .../CLI/src/Command/Tag/ListTagsCommand.php | 4 +- .../test/Command/Tag/CreateTagCommandTest.php | 69 +++++++++++++++++++ 6 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 module/CLI/src/Command/Tag/CreateTagCommand.php create mode 100644 module/CLI/test/Command/Tag/CreateTagCommandTest.php diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 53a5172b..14893d3d 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -18,6 +18,7 @@ return [ Command\Api\DisableKeyCommand::class, Command\Api\ListKeysCommand::class, Command\Tag\ListTagsCommand::class, + Command\Tag\CreateTagCommand::class, ] ], diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 6795852d..11f0ce77 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -22,6 +22,7 @@ return [ Command\Api\DisableKeyCommand::class => AnnotatedFactory::class, Command\Api\ListKeysCommand::class => AnnotatedFactory::class, Command\Tag\ListTagsCommand::class => AnnotatedFactory::class, + Command\Tag\CreateTagCommand::class => AnnotatedFactory::class, ], ], diff --git a/module/CLI/src/Command/Api/DisableKeyCommand.php b/module/CLI/src/Command/Api/DisableKeyCommand.php index 738b8b43..48d9d564 100644 --- a/module/CLI/src/Command/Api/DisableKeyCommand.php +++ b/module/CLI/src/Command/Api/DisableKeyCommand.php @@ -32,7 +32,7 @@ class DisableKeyCommand extends Command { $this->apiKeyService = $apiKeyService; $this->translator = $translator; - parent::__construct(null); + parent::__construct(); } public function configure() diff --git a/module/CLI/src/Command/Tag/CreateTagCommand.php b/module/CLI/src/Command/Tag/CreateTagCommand.php new file mode 100644 index 00000000..97501847 --- /dev/null +++ b/module/CLI/src/Command/Tag/CreateTagCommand.php @@ -0,0 +1,69 @@ +tagService = $tagService; + $this->translator = $translator; + parent::__construct(); + } + + protected function configure() + { + $this + ->setName('tag:create') + ->setDescription($this->translator->translate('Creates one or more tags')) + ->addOption( + 'name', + 't', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + $this->translator->translate('The name of the tags to create') + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $tagNames = $input->getOption('name'); + if (empty($tagNames)) { + $output->writeln(sprintf( + '%s', + $this->translator->translate('You have to provide at least one tag name') + )); + return; + } + + $this->tagService->createTags($tagNames); + $output->writeln($this->translator->translate('Created tags') . sprintf(': ["%s"]', implode( + '", "', + $tagNames + ))); + } +} diff --git a/module/CLI/src/Command/Tag/ListTagsCommand.php b/module/CLI/src/Command/Tag/ListTagsCommand.php index 201b09ae..3c781cb6 100644 --- a/module/CLI/src/Command/Tag/ListTagsCommand.php +++ b/module/CLI/src/Command/Tag/ListTagsCommand.php @@ -32,16 +32,16 @@ class ListTagsCommand extends Command */ public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator) { - parent::__construct(); $this->tagService = $tagService; $this->translator = $translator; + parent::__construct(); } protected function configure() { $this ->setName('tag:list') - ->setDescription('Lists existing tags'); + ->setDescription($this->translator->translate('Lists existing tags')); } protected function execute(InputInterface $input, OutputInterface $output) diff --git a/module/CLI/test/Command/Tag/CreateTagCommandTest.php b/module/CLI/test/Command/Tag/CreateTagCommandTest.php new file mode 100644 index 00000000..413f0970 --- /dev/null +++ b/module/CLI/test/Command/Tag/CreateTagCommandTest.php @@ -0,0 +1,69 @@ +tagService = $this->prophesize(TagServiceInterface::class); + + $command = new CreateTagCommand($this->tagService->reveal(), Translator::factory([])); + $app = new Application(); + $app->add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function errorIsReturnedWhenNoTagsAreProvided() + { + $this->commandTester->execute([]); + + $output = $this->commandTester->getDisplay(); + $this->assertContains('You have to provide at least one tag name', $output); + } + + /** + * @test + */ + public function serviceIsInvokedOnSuccess() + { + $tagNames = ['foo', 'bar']; + $this->commandTester->execute([ + '--name' => $tagNames, + ]); + /** @var MethodProphecy $createTags */ + $createTags = $this->tagService->createTags($tagNames)->willReturn([]); + + $output = $this->commandTester->getDisplay(); + + $this->assertContains(sprintf('Created tags: ["%s"]', implode('", "', $tagNames)), $output); + $createTags->shouldHaveBeenCalled(); + } +} From 095d8e73b855eeb77b8570dd74142722194afc79 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Jul 2017 09:13:25 +0200 Subject: [PATCH 49/55] Created ListTagsCommand --- .../test/Command/Tag/CreateTagCommandTest.php | 6 +- .../test/Command/Tag/ListTagsCommandTest.php | 75 +++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 module/CLI/test/Command/Tag/ListTagsCommandTest.php diff --git a/module/CLI/test/Command/Tag/CreateTagCommandTest.php b/module/CLI/test/Command/Tag/CreateTagCommandTest.php index 413f0970..6dab8d86 100644 --- a/module/CLI/test/Command/Tag/CreateTagCommandTest.php +++ b/module/CLI/test/Command/Tag/CreateTagCommandTest.php @@ -55,12 +55,12 @@ class CreateTagCommandTest extends TestCase public function serviceIsInvokedOnSuccess() { $tagNames = ['foo', 'bar']; - $this->commandTester->execute([ - '--name' => $tagNames, - ]); /** @var MethodProphecy $createTags */ $createTags = $this->tagService->createTags($tagNames)->willReturn([]); + $this->commandTester->execute([ + '--name' => $tagNames, + ]); $output = $this->commandTester->getDisplay(); $this->assertContains(sprintf('Created tags: ["%s"]', implode('", "', $tagNames)), $output); diff --git a/module/CLI/test/Command/Tag/ListTagsCommandTest.php b/module/CLI/test/Command/Tag/ListTagsCommandTest.php new file mode 100644 index 00000000..2c3a0a90 --- /dev/null +++ b/module/CLI/test/Command/Tag/ListTagsCommandTest.php @@ -0,0 +1,75 @@ +tagService = $this->prophesize(TagServiceInterface::class); + + $command = new ListTagsCommand($this->tagService->reveal(), Translator::factory([])); + $app = new Application(); + $app->add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function noTagsPrintsEmptyMessage() + { + /** @var MethodProphecy $listTags */ + $listTags = $this->tagService->listTags()->willReturn([]); + + $this->commandTester->execute([]); + $output = $this->commandTester->getDisplay(); + + $this->assertContains('No tags yet', $output); + $listTags->shouldHaveBeenCalled(); + } + + /** + * @test + */ + public function listOfTagsIsPrinted() + { + /** @var MethodProphecy $listTags */ + $listTags = $this->tagService->listTags()->willReturn([ + (new Tag())->setName('foo'), + (new Tag())->setName('bar'), + ]); + + $this->commandTester->execute([]); + $output = $this->commandTester->getDisplay(); + + $this->assertContains('foo', $output); + $this->assertContains('bar', $output); + $listTags->shouldHaveBeenCalled(); + } +} From 3cd14153ca806a37b7b61b2319d5763db69497bf Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Jul 2017 09:24:21 +0200 Subject: [PATCH 50/55] Created command to rename tag --- module/CLI/config/cli.config.php | 1 + module/CLI/config/dependencies.config.php | 1 + .../CLI/src/Command/Tag/CreateTagCommand.php | 2 +- .../CLI/src/Command/Tag/ListTagsCommand.php | 2 +- .../CLI/src/Command/Tag/RenameTagCommand.php | 65 +++++++++++++++ .../test/Command/Tag/RenameTagCommandTest.php | 82 +++++++++++++++++++ 6 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 module/CLI/src/Command/Tag/RenameTagCommand.php create mode 100644 module/CLI/test/Command/Tag/RenameTagCommandTest.php diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 14893d3d..bd7161c1 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -19,6 +19,7 @@ return [ Command\Api\ListKeysCommand::class, Command\Tag\ListTagsCommand::class, Command\Tag\CreateTagCommand::class, + Command\Tag\RenameTagCommand::class, ] ], diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 11f0ce77..b02fa1c6 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -23,6 +23,7 @@ return [ Command\Api\ListKeysCommand::class => AnnotatedFactory::class, Command\Tag\ListTagsCommand::class => AnnotatedFactory::class, Command\Tag\CreateTagCommand::class => AnnotatedFactory::class, + Command\Tag\RenameTagCommand::class => AnnotatedFactory::class, ], ], diff --git a/module/CLI/src/Command/Tag/CreateTagCommand.php b/module/CLI/src/Command/Tag/CreateTagCommand.php index 97501847..8a06d38c 100644 --- a/module/CLI/src/Command/Tag/CreateTagCommand.php +++ b/module/CLI/src/Command/Tag/CreateTagCommand.php @@ -40,7 +40,7 @@ class CreateTagCommand extends Command { $this ->setName('tag:create') - ->setDescription($this->translator->translate('Creates one or more tags')) + ->setDescription($this->translator->translate('Creates one or more tags.')) ->addOption( 'name', 't', diff --git a/module/CLI/src/Command/Tag/ListTagsCommand.php b/module/CLI/src/Command/Tag/ListTagsCommand.php index 3c781cb6..eb120226 100644 --- a/module/CLI/src/Command/Tag/ListTagsCommand.php +++ b/module/CLI/src/Command/Tag/ListTagsCommand.php @@ -41,7 +41,7 @@ class ListTagsCommand extends Command { $this ->setName('tag:list') - ->setDescription($this->translator->translate('Lists existing tags')); + ->setDescription($this->translator->translate('Lists existing tags.')); } protected function execute(InputInterface $input, OutputInterface $output) diff --git a/module/CLI/src/Command/Tag/RenameTagCommand.php b/module/CLI/src/Command/Tag/RenameTagCommand.php new file mode 100644 index 00000000..7fd97b5e --- /dev/null +++ b/module/CLI/src/Command/Tag/RenameTagCommand.php @@ -0,0 +1,65 @@ +tagService = $tagService; + $this->translator = $translator; + parent::__construct(); + } + + protected function configure() + { + $this + ->setName('tag:rename') + ->setDescription($this->translator->translate('Renames one existing tag.')) + ->addArgument('oldName', InputArgument::REQUIRED, $this->translator->translate('Current name of the tag.')) + ->addArgument('newName', InputArgument::REQUIRED, $this->translator->translate('New name of the tag.')); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $oldName = $input->getArgument('oldName'); + $newName = $input->getArgument('newName'); + + try { + $this->tagService->renameTag($oldName, $newName); + $output->writeln(sprintf('%s', $this->translator->translate('Tag properly renamed.'))); + } catch (EntityDoesNotExistException $e) { + $output->writeln('' . sprintf($this->translator->translate( + 'A tag with name "%s" was not found' + ), $oldName) . ''); + } + } +} diff --git a/module/CLI/test/Command/Tag/RenameTagCommandTest.php b/module/CLI/test/Command/Tag/RenameTagCommandTest.php new file mode 100644 index 00000000..032bfcc7 --- /dev/null +++ b/module/CLI/test/Command/Tag/RenameTagCommandTest.php @@ -0,0 +1,82 @@ +tagService = $this->prophesize(TagServiceInterface::class); + + $command = new RenameTagCommand($this->tagService->reveal(), Translator::factory([])); + $app = new Application(); + $app->add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function errorIsPrintedIfExceptionIsThrown() + { + $oldName = 'foo'; + $newName = 'bar'; + /** @var MethodProphecy $renameTag */ + $renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(EntityDoesNotExistException::class); + + $this->commandTester->execute([ + 'oldName' => $oldName, + 'newName' => $newName, + ]); + $output = $this->commandTester->getDisplay(); + + $this->assertContains('A tag with name "foo" was not found', $output); + $renameTag->shouldHaveBeenCalled(); + } + + /** + * @test + */ + public function successIsPrintedIfNoErrorOccurs() + { + $oldName = 'foo'; + $newName = 'bar'; + /** @var MethodProphecy $renameTag */ + $renameTag = $this->tagService->renameTag($oldName, $newName)->willReturn(new Tag()); + + $this->commandTester->execute([ + 'oldName' => $oldName, + 'newName' => $newName, + ]); + $output = $this->commandTester->getDisplay(); + + $this->assertContains('Tag properly renamed', $output); + $renameTag->shouldHaveBeenCalled(); + } +} From 602e11d5e770a50c2f4402b283b615d89d949081 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Jul 2017 09:28:40 +0200 Subject: [PATCH 51/55] Added namespace to functions --- config/autoload/app_options.global.php | 4 +- config/autoload/entity-manager.global.php | 8 ++- config/autoload/translator.global.php | 4 +- config/autoload/url-shortener.global.php | 7 +- module/CLI/config/cli.config.php | 3 +- .../CLI/src/Command/Tag/RenameTagCommand.php | 2 - .../test/Command/Tag/CreateTagCommandTest.php | 2 - .../test/Command/Tag/ListTagsCommandTest.php | 2 - .../test/Command/Tag/RenameTagCommandTest.php | 2 - module/Common/functions/functions.php | 64 +++++++++---------- module/Common/src/Factory/CacheFactory.php | 8 +-- 11 files changed, 53 insertions(+), 53 deletions(-) diff --git a/config/autoload/app_options.global.php b/config/autoload/app_options.global.php index e3f8fbdd..fd7147a1 100644 --- a/config/autoload/app_options.global.php +++ b/config/autoload/app_options.global.php @@ -1,10 +1,12 @@ [ 'name' => 'Shlink', 'version' => '1.2.0', - 'secret_key' => env('SECRET_KEY'), + 'secret_key' => Common\env('SECRET_KEY'), ], ]; diff --git a/config/autoload/entity-manager.global.php b/config/autoload/entity-manager.global.php index 6bdcae77..803eb8ff 100644 --- a/config/autoload/entity-manager.global.php +++ b/config/autoload/entity-manager.global.php @@ -1,4 +1,6 @@ [ @@ -6,9 +8,9 @@ return [ 'proxies_dir' => 'data/proxies', ], 'connection' => [ - 'user' => env('DB_USER'), - 'password' => env('DB_PASSWORD'), - 'dbname' => env('DB_NAME', 'shlink'), + 'user' => Common\env('DB_USER'), + 'password' => Common\env('DB_PASSWORD'), + 'dbname' => Common\env('DB_NAME', 'shlink'), 'charset' => 'utf8', ], ], diff --git a/config/autoload/translator.global.php b/config/autoload/translator.global.php index d0561cbe..2ce6bb44 100644 --- a/config/autoload/translator.global.php +++ b/config/autoload/translator.global.php @@ -1,8 +1,10 @@ [ - 'locale' => env('DEFAULT_LOCALE', 'en'), + 'locale' => Common\env('DEFAULT_LOCALE', 'en'), ], ]; diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 5b0d95c9..f17d192d 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -1,14 +1,15 @@ [ 'domain' => [ - 'schema' => env('SHORTENED_URL_SCHEMA', 'http'), - 'hostname' => env('SHORTENED_URL_HOSTNAME'), + 'schema' => Common\env('SHORTENED_URL_SCHEMA', 'http'), + 'hostname' => Common\env('SHORTENED_URL_HOSTNAME'), ], - 'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS), + 'shortcode_chars' => Common\env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS), ], ]; diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index bd7161c1..a7a4fb9b 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -1,10 +1,11 @@ [ - 'locale' => env('CLI_LOCALE', 'en'), + 'locale' => Common\env('CLI_LOCALE', 'en'), 'commands' => [ Command\Shortcode\GenerateShortcodeCommand::class, Command\Shortcode\ResolveUrlCommand::class, diff --git a/module/CLI/src/Command/Tag/RenameTagCommand.php b/module/CLI/src/Command/Tag/RenameTagCommand.php index 7fd97b5e..e3ee678e 100644 --- a/module/CLI/src/Command/Tag/RenameTagCommand.php +++ b/module/CLI/src/Command/Tag/RenameTagCommand.php @@ -1,6 +1,4 @@ get('config'); - if (isset($config['cache']) - && isset($config['cache']['adapter']) + if (isset($config['cache'], $config['cache']['adapter']) && in_array($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS) ) { return $this->resolveCacheAdapter($config['cache']); } // If the adapter has not been set in config, create one based on environment - return env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache(); + return Common\env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache(); } /** @@ -80,7 +80,7 @@ class CacheFactory implements FactoryInterface if (! isset($server['host'])) { continue; } - $port = isset($server['port']) ? intval($server['port']) : 11211; + $port = isset($server['port']) ? (int) $server['port'] : 11211; $memcached->addServer($server['host'], $port); } From a138f4153dae260de5520dccc28737f9f3d82045 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Jul 2017 09:35:24 +0200 Subject: [PATCH 52/55] Created DeleteTagsCommand --- module/CLI/config/cli.config.php | 1 + module/CLI/config/dependencies.config.php | 1 + .../CLI/src/Command/Tag/DeleteTagsCommand.php | 69 +++++++++++++++++++ .../Command/Tag/DeleteTagsCommandTest.php | 68 ++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 module/CLI/src/Command/Tag/DeleteTagsCommand.php create mode 100644 module/CLI/test/Command/Tag/DeleteTagsCommandTest.php diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index a7a4fb9b..a1a34c16 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -21,6 +21,7 @@ return [ Command\Tag\ListTagsCommand::class, Command\Tag\CreateTagCommand::class, Command\Tag\RenameTagCommand::class, + Command\Tag\DeleteTagsCommand::class, ] ], diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index b02fa1c6..565ab8bc 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -24,6 +24,7 @@ return [ Command\Tag\ListTagsCommand::class => AnnotatedFactory::class, Command\Tag\CreateTagCommand::class => AnnotatedFactory::class, Command\Tag\RenameTagCommand::class => AnnotatedFactory::class, + Command\Tag\DeleteTagsCommand::class => AnnotatedFactory::class, ], ], diff --git a/module/CLI/src/Command/Tag/DeleteTagsCommand.php b/module/CLI/src/Command/Tag/DeleteTagsCommand.php new file mode 100644 index 00000000..0a4e271b --- /dev/null +++ b/module/CLI/src/Command/Tag/DeleteTagsCommand.php @@ -0,0 +1,69 @@ +tagService = $tagService; + $this->translator = $translator; + parent::__construct(); + } + + protected function configure() + { + $this + ->setName('tag:delete') + ->setDescription($this->translator->translate('Deletes one or more tags.')) + ->addOption( + 'name', + 't', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + $this->translator->translate('The name of the tags to delete') + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $tagNames = $input->getOption('name'); + if (empty($tagNames)) { + $output->writeln(sprintf( + '%s', + $this->translator->translate('You have to provide at least one tag name') + )); + return; + } + + $this->tagService->deleteTags($tagNames); + $output->writeln($this->translator->translate('Deleted tags') . sprintf(': ["%s"]', implode( + '", "', + $tagNames + ))); + } +} diff --git a/module/CLI/test/Command/Tag/DeleteTagsCommandTest.php b/module/CLI/test/Command/Tag/DeleteTagsCommandTest.php new file mode 100644 index 00000000..9498a450 --- /dev/null +++ b/module/CLI/test/Command/Tag/DeleteTagsCommandTest.php @@ -0,0 +1,68 @@ +tagService = $this->prophesize(TagServiceInterface::class); + + $command = new DeleteTagsCommand($this->tagService->reveal(), Translator::factory([])); + $app = new Application(); + $app->add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function errorIsReturnedWhenNoTagsAreProvided() + { + $this->commandTester->execute([]); + + $output = $this->commandTester->getDisplay(); + $this->assertContains('You have to provide at least one tag name', $output); + } + + /** + * @test + */ + public function serviceIsInvokedOnSuccess() + { + $tagNames = ['foo', 'bar']; + /** @var MethodProphecy $deleteTags */ + $deleteTags = $this->tagService->deleteTags($tagNames)->will(function () { + }); + + $this->commandTester->execute([ + '--name' => $tagNames, + ]); + $output = $this->commandTester->getDisplay(); + + $this->assertContains(sprintf('Deleted tags: ["%s"]', implode('", "', $tagNames)), $output); + $deleteTags->shouldHaveBeenCalled(); + } +} From f3389d3738469701853b3a628269c7ec52212296 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Jul 2017 09:40:34 +0200 Subject: [PATCH 53/55] Updated language files --- module/CLI/lang/es.mo | Bin 6782 -> 7812 bytes module/CLI/lang/es.po | 46 ++++++++++++++++++++++++++++++++++++++--- module/Rest/lang/es.mo | Bin 2634 -> 2645 bytes module/Rest/lang/es.po | 8 +++---- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index 2684b41b1ceb41eb5b9b4ef1e1507baccd9d037c..93bf65593ba92f90696353bc8fe4caab31896c14 100644 GIT binary patch delta 2574 zcmZwHU2GIp7{>9l1=XrT7(YhVD={YET6jJY45 z#SZ)g8?mY~oOgL-H)@^~-fK+39AwbW7w51VD{eAo2F}JRY{t3Rg7vr!6Aq?!q;#LzKb{G2RIKuM{V>n*0R1SpBgS)kE2{zf(!6%+>95HNKM1EaDko3S7rz& z;X#~BpMOUFObxrMVtq58fp*@E4z9&GzJ$~9Q`Cgt;2OMw z%dv@zWZCqgp5KYez+O~o4&hS#7`5O}sKEckI-J5o1*LKx1J%l60^3nFA41jiDE8o2 zxD1=frzYq@Wy*~lLDlvo>b|q61%E(dHov0QxrPn6u!j6MGuTi=jW~F_Q8gao04SBm zP-o*E+=##7@smb7=8vdreVf^T9A{((AFE6B4Z>cU017rA7PqwYI}3haAS39h0t zJC(^Tu0#dci!oGNp!ffda2&iMicqaA#H@`}Nt9mgNNH3c#BFY)t5L<^H$ppCyOXXg zHqrHKsep+mXgUu|=qhI=>j!CFj%qsXDu$EkH(ZYXex!;dOBw-_yyEK9BX$o z;3x&9(2tYO4zIgv9lWAo?xMG8ph|UygW#vOmGRQjGgC%m3$5=A*x{7F$7Y;C*ETN7 zHQHe(XEUB}cYFCvvN);YuR?p)b$mCen{y`okh5OKwO-Z^dRaHP5I61lY}U>A;Z)vk z>+cD!#7zfJ2mfq;IFspc)9$}Bh97P;V@E3I>`IqrSI#O+q;h`Fx=%tXGZ3xV?J?ZZ z+udrmy2Jllx0IM#h|ST)!nr3t950@lHZsHR%X<6VZ2Bon;M|vtm*Q1Dv9&=m#&?jj zz88gV`qb^;d9T~^Ee(H(5>pQmJ%TSTQU1U0_P{v~cn@c4<>t?v8vHnSlyKAoFR z4W=@zlQW&+@Wzvtnr1#%wyB#kIZQWUxv+Me4>uP#)Qqdy0f)-v-63ymd;g8$bX`~F zSiq&f>yE{2uN!2-305DwG(LB{cy!Ji4P=a?*zZ$P)>Cn{Q-Y_y_0PxJqhtoFO$?D# vb3n7~@VbDmza`8>@yy)wi4J!cXG@33%l4 delta 1563 zcmX}sSx8h-9LMo9YHGR6IBBV)He*_umHRd>SyogUQCU_fG?iIk24YYkOhT5;Lkl;- z$9#yE7o$sWEf7S|Q&CS5woniRDHujC()V{~>QCo>PRE(E{Qu|9#pZ?T@U%B!+z_p_ zJheDu4#@-_#Dzu1EW;68juTjp4{#%X#se5gG-ex~!!7s>HD5wf)Bx)HEw~8}V4E>v zGeIYvfe+}#AE>n#FcssIjmbn0vX&`EH_lzw2x`I87{X!PfYX?UX?$1*@L?U6 zV;P>rB-S?*bTq*&R7$6C4bC9{Og5`9mnlMJrVI( zQ^?-U3)Jm?i>vSyi*(|3)JDH$l7B5&$+y_NX-6&4jk;7vQK<@}GIbw=_z3m+Sybj+ zL zp{(dN(!F4h&gql^MGehQqwG$ZbT{f~|00f#(x0jI8)BmY495EU9dO6`Dube#rqnC+ zS}Mw;IL}6&N-e)zPRfgLJxx&@#ra*J^iEXsT&I&M-9CFXam;0vC;!RV5*q3?diakG z_QG*{akFj#wu=jfpk>ippSxb;nbf z6z$g9uRWddc2{PT%bL#0vDYpeja$Mb{{GNlUnpqJXK#-m3ia9Ba;~|o`P_T\n" "Language-Team: \n" "Language: es_ES\n" @@ -223,12 +223,52 @@ msgstr "URL larga:" msgid "Provided short code \"%s\" has an invalid format." msgstr "El código corto proporcionado \"%s\" tiene un formato inválido." +msgid "Creates one or more tags." +msgstr "Crea una o más etiquetas." + +msgid "The name of the tags to create" +msgstr "El nombre de las etiquetas a crear" + +msgid "You have to provide at least one tag name" +msgstr "Debes proporcionar al menos un nombre de etiqueta" + +msgid "Created tags" +msgstr "Etiquetas creadas" + +msgid "Deletes one or more tags." +msgstr "Elimina una o más etiquetas." + +msgid "The name of the tags to delete" +msgstr "El nombre de las etiquetas a eliminar" + +msgid "Deleted tags" +msgstr "Etiquetas eliminadas" + +msgid "Lists existing tags." +msgstr "Lista las etiquetas existentes." + #, fuzzy msgid "Name" msgstr "Nombre" msgid "No tags yet" -msgstr "Aún no hay tags" +msgstr "Aún no hay etiquetas" + +msgid "Renames one existing tag." +msgstr "Renombra una etiqueta existente." + +msgid "Current name of the tag." +msgstr "Nombre actual de la etiqueta." + +msgid "New name of the tag." +msgstr "Nuevo nombre de la etiqueta." + +msgid "Tag properly renamed." +msgstr "Etiqueta correctamente renombrada." + +#, php-format +msgid "A tag with name \"%s\" was not found" +msgstr "Una etiqueta con nombre \"%s\" no ha sido encontrada" msgid "Processes visits where location is not set yet" msgstr "Procesa las visitas donde la localización no ha sido establecida aún" diff --git a/module/Rest/lang/es.mo b/module/Rest/lang/es.mo index 139dbf7e3c6ef2149c6c6effb01d27e7e1d0403b..a7b89c77959117bbacb4d479354ffb959de3734a 100644 GIT binary patch delta 162 zcmX>la#du)npg`428NSt3=DgLG!r`mgB6fI0Hn=<^lu>T2&9cU7#N~~bSaPq@h<{t zT_7#Y$-p23q#c2D5Ri@o(s@97Bak)$(h8e78Rs#xn<*GrS{YkTUdN&cVVf9izQ)4G d#FeO!T9R2$`30Kvnph(S28NSt3=DgL^dBH?1*CVeGccF~>90WA5lHKDFfc>|>3kp!;-3c6 zx\n" "Language-Team: \n" "Language: es_ES\n" @@ -55,11 +55,11 @@ msgid "" "rename the tag" msgstr "" "Debes proporcionar tanto el parámetro 'oldName' como 'newName' para poder " -"renombrar el tag correctamente" +"renombrar la etiqueta correctamente" #, php-format msgid "It wasn't possible to find a tag with name '%s'" -msgstr "No fue posible encontrar un tag con el nombre '%s'" +msgstr "No fue posible encontrar una etiqueta con el nombre '%s'" #, php-format msgid "You need to provide the Bearer type in the %s header." From ff98e1fb3dd2949347655b97a5dc08a696a8fc38 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Jul 2017 09:43:35 +0200 Subject: [PATCH 54/55] Added v1.5.0 to CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96fca031..7aa00a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ ## CHANGELOG +### 1.5.0 + +**Enhancements:** + +* [95: Add tags CRUD to CLI](https://github.com/shlinkio/shlink/issues/95) +* [59: Add tags CRUD to REST](https://github.com/shlinkio/shlink/issues/59) +* [66: Allow to import certain information from older app directory when updating](https://github.com/shlinkio/shlink/issues/66) + +**Tasks** + +* [96: Add namespace to functions](https://github.com/shlinkio/shlink/issues/96) +* [76: Add response examples to swagger docs](https://github.com/shlinkio/shlink/issues/76) +* [93: Improve cross domain management by using the ImplicitOptionsMiddleware](https://github.com/shlinkio/shlink/issues/93) + +**Bugs** + +* [92: Fix formatted dates, using an ISO compliant format](https://github.com/shlinkio/shlink/issues/92) + ### 1.4.0 **Enhancements:** From 9260b3ac6b9ee3ce3656481d3f72515065311995 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Jul 2017 09:58:03 +0200 Subject: [PATCH 55/55] Fixed coding styles --- .../CLI/src/Install/Plugin/DatabaseConfigCustomizerPlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/CLI/src/Install/Plugin/DatabaseConfigCustomizerPlugin.php b/module/CLI/src/Install/Plugin/DatabaseConfigCustomizerPlugin.php index 88bceb47..abb2e192 100644 --- a/module/CLI/src/Install/Plugin/DatabaseConfigCustomizerPlugin.php +++ b/module/CLI/src/Install/Plugin/DatabaseConfigCustomizerPlugin.php @@ -19,7 +19,7 @@ class DatabaseConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin 'PostgreSQL' => 'pdo_pgsql', 'SQLite' => 'pdo_sqlite', ]; - + /** * @var Filesystem */