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(); + } +}