diff --git a/.travis.yml b/.travis.yml index 467be0e..d54afb3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ php: - 5.6 - 7.0 - hhvm - + sudo: false cache: diff --git a/Controller/ResourceController.php b/Controller/ResourceController.php index 2226226..f68d8e1 100644 --- a/Controller/ResourceController.php +++ b/Controller/ResourceController.php @@ -11,10 +11,14 @@ namespace Symfony\Cmf\Bundle\ResourceRestBundle\Controller; +use Puli\Repository\Api\EditableRepository; +use Puli\Repository\Api\ResourceRepository; use Symfony\Cmf\Component\Resource\RepositoryRegistryInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use JMS\Serializer\SerializerInterface; use JMS\Serializer\SerializationContext; +use Symfony\Component\Routing\Exception\RouteNotFoundException; class ResourceController { @@ -29,21 +33,125 @@ class ResourceController private $serializer; /** - * @param RepositoryInterface + * @param SerializerInterface $serializer + * @param RepositoryRegistryInterface $registry */ - public function __construct( - SerializerInterface $serializer, - RepositoryRegistryInterface $registry - ) { + public function __construct(SerializerInterface $serializer, RepositoryRegistryInterface $registry) + { $this->serializer = $serializer; $this->registry = $registry; } - public function resourceAction($repositoryName, $path) + /** + * Provides resource information. + * + * @param string $repositoryName + * @param string $path + */ + public function getResourceAction($repositoryName, $path) { $repository = $this->registry->get($repositoryName); $resource = $repository->get('/'.$path); + return $this->createResponse($resource); + } + + /** + * Changes the current resource. + * + * The request body should contain a JSON list of operations + * like: + * + * [{"operation": "move", "target": "/cms/new/id"}] + * + * Currently supported operations: + * + * - move (options: target) + * + * changing payload properties isn't supported yet. + * + * @param string $repositoryName + * @param string $path + * @param Request $request + * + * @return Response + */ + public function patchResourceAction($repositoryName, $path, Request $request) + { + $repository = $this->registry->get($repositoryName); + $this->failOnNotEditable($repository, $repositoryName); + + $path = '/'.ltrim($path, '/'); + + $requestContent = json_decode($request->getContent(), true); + if (!$requestContent) { + return $this->badRequestResponse('Only JSON request bodies are supported.'); + } + + foreach ($requestContent as $action) { + if (!isset($action['operation'])) { + return $this->badRequestResponse('Malformed request body. It should contain a list of operations.'); + } + + switch ($action['operation']) { + case 'move': + $targetPath = $action['target']; + $repository->move($path, $targetPath); + + break; + default: + return $this->badRequestResponse(sprintf('Only operation "%s" is not supported, supported operations: move.', $action['operation'])); + } + } + + return $this->createResponse('', Response::HTTP_NO_CONTENT); + } + + /** + * Deletes the resource. + * + * @param string $repositoryName + * @param string $path + * + * @return Response + */ + public function deleteResourceAction($repositoryName, $path) + { + $repository = $this->registry->get($repositoryName); + $this->failOnNotEditable($repository, $repositoryName); + + $path = '/'.ltrim($path, '/'); + + $repository->remove($path); + + return $this->createResponse('', Response::HTTP_NO_CONTENT); + } + + private function failOnNotEditable(ResourceRepository $repository, $repositoryName) + { + if (!$repository instanceof EditableRepository) { + throw new RouteNotFoundException(sprintf('Repository "%s" is not editable.', $repositoryName)); + } + } + + /** + * @param string $message + * + * @return Response + */ + private function badRequestResponse($message) + { + return $this->createResponse(['message' => $message], Response::HTTP_BAD_REQUEST); + } + + /** + * @param mixed $resource + * @param int $httpStatusCode + * + * @return Response + */ + private function createResponse($resource, $httpStatusCode = Response::HTTP_OK) + { $context = SerializationContext::create(); $context->enableMaxDepthChecks(); $context->setSerializeNull(true); @@ -53,7 +161,7 @@ public function resourceAction($repositoryName, $path) $context ); - $response = new Response($json); + $response = new Response($json, $httpStatusCode); $response->headers->set('Content-Type', 'application/json'); return $response; diff --git a/Enhancer/EnhancerInterface.php b/Enhancer/EnhancerInterface.php index 5c5a1a5..0746183 100644 --- a/Enhancer/EnhancerInterface.php +++ b/Enhancer/EnhancerInterface.php @@ -11,7 +11,6 @@ namespace Symfony\Cmf\Bundle\ResourceRestBundle\Enhancer; -use JMS\Serializer\Context; use Puli\Repository\Api\Resource\PuliResource; /** @@ -28,8 +27,8 @@ interface EnhancerInterface * * $context->addData('foobar', 'Some value'); * - * @param Context Serialization context - * @param resource The resource being serialized + * @param array $data Context Serialization context + * @param PuliResource $resource The resource being serialized */ public function enhance(array $data, PuliResource $resource); } diff --git a/README.md b/README.md index 629700a..b7e04df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Symfony CMF Resource REST API Bundle -[![Build Status](https://secure.travis-ci.org/symfony-cmf/ResourceRestBundle.png?branch=master)](http://travis-ci.org/symfony-cmf/ResourceRestBundle) +[![Build Status](https://travis-ci.org/symfony-cmf/resource-rest-bundle.svg?branch=master)](https://travis-ci.org/symfony-cmf/resource-rest-bundle) [![StyleCI](https://styleci.io/repos/29090266/shield)](https://styleci.io/repos/29090266) [![Latest Stable Version](https://poser.pugx.org/symfony-cmf/resource-rest-bundle/version.png)](https://packagist.org/packages/symfony-cmf/resource-rest-bundle) [![Total Downloads](https://poser.pugx.org/symfony-cmf/resource-rest-bundle/d/total.png)](https://packagist.org/packages/symfony-cmf/resource-rest-bundle) diff --git a/Registry/EnhancerRegistry.php b/Registry/EnhancerRegistry.php index 5283601..aee2bc8 100644 --- a/Registry/EnhancerRegistry.php +++ b/Registry/EnhancerRegistry.php @@ -11,6 +11,7 @@ namespace Symfony\Cmf\Bundle\ResourceRestBundle\Registry; +use Symfony\Cmf\Bundle\ResourceRestBundle\Enhancer\EnhancerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -40,11 +41,8 @@ class EnhancerRegistry * @param array $enhancerMap Map of enhancer aliases to repository names * @param array $aliasMap Serice ID map for enhancer aliases */ - public function __construct( - ContainerInterface $container, - $enhancerMap = array(), - $aliasMap = array() - ) { + public function __construct(ContainerInterface $container, $enhancerMap = array(), $aliasMap = array()) + { $this->container = $container; $this->enhancerMap = $enhancerMap; $this->aliasMap = $aliasMap; @@ -65,6 +63,7 @@ public function getEnhancers($repositoryAlias) } $aliases = $this->enhancerMap[$repositoryAlias]; + $enhancers = []; foreach ($aliases as $alias) { if (!isset($this->aliasMap[$alias])) { diff --git a/Registry/PayloadAliasRegistry.php b/Registry/PayloadAliasRegistry.php index c8db6ab..048c76c 100644 --- a/Registry/PayloadAliasRegistry.php +++ b/Registry/PayloadAliasRegistry.php @@ -11,9 +11,9 @@ namespace Symfony\Cmf\Bundle\ResourceRestBundle\Registry; +use Puli\Repository\Api\Resource\PuliResource; use Symfony\Cmf\Component\Resource\RepositoryRegistryInterface; use Symfony\Cmf\Component\Resource\Repository\Resource\CmfResource; -use Puli\Repository\Api\Resource\PuliResource; /** * Registry for resource payload aliases. @@ -36,10 +36,8 @@ class PayloadAliasRegistry * @param RepositoryRegistryInterface $repositoryRegistry * @param array $aliases */ - public function __construct( - RepositoryRegistryInterface $repositoryRegistry, - array $aliases = array() - ) { + public function __construct(RepositoryRegistryInterface $repositoryRegistry, array $aliases = array()) + { $this->repositoryRegistry = $repositoryRegistry; foreach ($aliases as $alias => $config) { @@ -54,7 +52,7 @@ public function __construct( /** * Return the alias for the given PHPCR resource. * - * @param resource $resource + * @param PuliResource $resource * * @return string */ diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index cd0619d..8ad4faf 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -1,7 +1,23 @@ -_cmf_resource: +_cmf_delete_resource: path: /api/{repositoryName}/{path} + methods: ['delete'] requirements: path: .* defaults: - _controller: cmf_resource_rest.controller.resource:resourceAction - _format: json + _controller: cmf_resource_rest.controller.resource:deleteResourceAction + +_cmf_patch_resource: + path: /api/{repositoryName}/{path} + methods: ['patch'] + requirements: + path: .* + defaults: + _controller: cmf_resource_rest.controller.resource:patchResourceAction + +_cmf_get_resource: + path: /api/{repositoryName}/{path} + methods: ['get'] + requirements: + path: .* + defaults: + _controller: cmf_resource_rest.controller.resource:getResourceAction diff --git a/Serializer/Jms/EventSubscriber/ResourceSubscriber.php b/Serializer/Jms/EventSubscriber/ResourceSubscriber.php index 17bbb94..f0d7bb6 100644 --- a/Serializer/Jms/EventSubscriber/ResourceSubscriber.php +++ b/Serializer/Jms/EventSubscriber/ResourceSubscriber.php @@ -14,7 +14,6 @@ use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\PreSerializeEvent; -use Puli\Repository\Api\ResourceCollection; use Puli\Repository\Api\Resource\PuliResource; /** diff --git a/Serializer/Jms/Handler/ResourceHandler.php b/Serializer/Jms/Handler/ResourceHandler.php index 65f064c..e8f7593 100644 --- a/Serializer/Jms/Handler/ResourceHandler.php +++ b/Serializer/Jms/Handler/ResourceHandler.php @@ -18,11 +18,11 @@ use PHPCR\NodeInterface; use PHPCR\Util\PathHelper; use Puli\Repository\Api\Resource\BodyResource; +use Puli\Repository\Api\Resource\PuliResource; use Symfony\Cmf\Component\Resource\RepositoryRegistryInterface; use Symfony\Cmf\Bundle\ResourceRestBundle\Registry\PayloadAliasRegistry; use Symfony\Cmf\Bundle\ResourceRestBundle\Registry\EnhancerRegistry; use Symfony\Cmf\Component\Resource\Repository\Resource\CmfResource; -use Puli\Repository\Api\Resource\PuliResource; /** * Handle PHPCR resource serialization. diff --git a/Tests/Features/Context/ResourceContext.php b/Tests/Features/Context/ResourceContext.php index 7d2af6b..3b38457 100644 --- a/Tests/Features/Context/ResourceContext.php +++ b/Tests/Features/Context/ResourceContext.php @@ -21,6 +21,7 @@ use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; +use Webmozart\Assert\Assert; class ResourceContext implements Context, KernelAwareContext { @@ -72,6 +73,14 @@ public function beforeScenario(BeforeScenarioScope $scope) } } + /** + * @AfterScenario + */ + public function refreshSession() + { + $this->session->refresh(true); + } + /** * @Given the test application has the following configuration: */ @@ -93,7 +102,7 @@ public function createFile($filename, PyStringNode $content) } /** - * @Given there exists a :class document at :path: + * @Given there exists a/an :class document at :path: */ public function createDocument($class, $path, TableNode $fields) { @@ -122,6 +131,59 @@ public function createDocument($class, $path, TableNode $fields) $this->manager->persist($document); $this->manager->flush(); + $this->manager->clear(); + } + + /** + * @Then there is a/an :class document at :path + * @Then there is a/an :class document at :path: + */ + public function thereIsADocumentAt($class, $path, TableNode $fields = null) + { + $class = 'Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Resources\\TestBundle\\Document\\'.$class; + $path = '/tests'.$path; + + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf( + 'Class "%s" does not exist', + $class + )); + } + + $document = $this->manager->find($class, $path); + + Assert::notNull($document, sprintf('No "%s" document exists at "%s"', $class, $path)); + + if (null === $fields) { + return; + } + + foreach ($fields->getRowsHash() as $field => $value) { + Assert::eq($document->$field, $value); + } + } + + /** + * @Then there is no :class document at :path + */ + public function thereIsNoDocumentAt($class, $path) + { + $class = 'Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Resources\\TestBundle\\Document\\'.$class; + $path = '/tests'.$path; + + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf( + 'Class "%s" does not exist', + $class + )); + } + + $this->session->refresh(true); + $this->manager->clear(); + + $document = $this->manager->find($class, $path); + + Assert::null($document, sprintf('A "%s" document does exist at "%s".', $class, $path)); } private function clearDiCache() diff --git a/Tests/Features/resource_api_phpcr.feature b/Tests/Features/resource_api_phpcr.feature index b9a7a4e..abf5e4d 100644 --- a/Tests/Features/resource_api_phpcr.feature +++ b/Tests/Features/resource_api_phpcr.feature @@ -21,9 +21,9 @@ Feature: PHPCR resource repository Scenario: Retrieve PHPCR resource with children - Given there exists a "Article" document at "/cmf/articles/foo": - | title | Article 1 | - | body | This is my article | + Given there exists an "Article" document at "/cmf/articles/foo": + | title | Article 1 | + | body | This is my article | When I send a GET request to "/api/phpcr_repo/foo" Then the response should contain json: """ @@ -39,3 +39,40 @@ Feature: PHPCR resource repository "children": [] } """ + + Scenario: Rename a PHPCR resource + Given there exists an "Article" document at "/cmf/articles/foo": + | title | Article 1 | + | body | This is my article | + When I send a PATCH request to "/api/phpcr_repo/foo" with body: + """ + [{"operation": "move", "target": "/foo-bar"}] + """ + Then the response code should be 204 + And there is an "Article" document at "/cmf/articles/foo-bar" + | title | Article 1 | + | body | This is my article | + + Scenario: Move a PHPCR resource + Given there exists an "Article" document at "/cmf/articles/foo": + | title | Article 1 | + | body | This is my article | + And there exists a "Article" document at "/cmf/articles/bar": + | title | Article 2 | + | body | Another one | + When I send a PATCH request to "/api/phpcr_repo/foo" with body: + """ + [{"operation": "move", "target": "/bar/foo"}] + """ + Then the response code should be 204 + And there is an "Article" document at "/cmf/articles/bar/foo" + | title | Article 1 | + | body | This is my article | + + Scenario: Remove a PHPCR resource + Given there exists an "Article" document at "/cmf/articles/foo": + | title | Article 1 | + | body | This is my article | + When I send a DELETE request to "/api/phpcr_repo/foo" + Then the response code should be 204 + And there is no "Article" document at "/api/phpcr_repo/bar/foo" diff --git a/Tests/Features/resource_api_phpcr_odm.feature b/Tests/Features/resource_api_phpcr_odm.feature index 2a01da5..e706760 100644 --- a/Tests/Features/resource_api_phpcr_odm.feature +++ b/Tests/Features/resource_api_phpcr_odm.feature @@ -21,9 +21,9 @@ Feature: PHPCR-ODM resource repository Scenario: Retrieve a PHPCR-ODM resource - Given there exists a "Article" document at "/cmf/articles/foo": - | title | Article 1 | - | body | This is my article | + Given there exists an "Article" document at "/cmf/articles/foo": + | title | Article 1 | + | body | This is my article | When I send a GET request to "/api/phpcrodm_repo/foo" Then the response code should be 200 And the response should contain json: @@ -42,15 +42,15 @@ Feature: PHPCR-ODM resource repository """ Scenario: Retrieve a PHPCR-ODM resource with children - Given there exists a "Article" document at "/cmf/articles/foo": - | title | Article 1 | - | body | This is my article | - And there exists a "Article" document at "/cmf/articles/foo/bar": - | title | Article child | - | body | There are many like it | - And there exists a "Article" document at "/cmf/articles/foo/boo": - | title | Article child | - | body | But this one is mine | + Given there exists an "Article" document at "/cmf/articles/foo": + | title | Article 1 | + | body | This is my article | + And there exists an "Article" document at "/cmf/articles/foo/bar": + | title | Article child | + | body | There are many like it | + And there exists an "Article" document at "/cmf/articles/foo/boo": + | title | Article child | + | body | But this one is mine | When I send a GET request to "/api/phpcrodm_repo/foo" Then the response code should be 200 And the response should contain json: @@ -90,3 +90,40 @@ Feature: PHPCR-ODM resource repository } } """ + + Scenario: Rename a PHPCR-ODM resource + Given there exists an "Article" document at "/cmf/articles/foo": + | title | Article 1 | + | body | This is my article | + When I send a PATCH request to "/api/phpcrodm_repo/foo" with body: + """ + [{"operation": "move", "target": "/foo-bar"}] + """ + Then the response code should be 204 + And there is an "Article" document at "/cmf/articles/foo-bar": + | title | Article 1 | + | body | This is my article | + + Scenario: Move a PHPCR-ODM resource + Given there exists an "Article" document at "/cmf/articles/foo": + | title | Article 1 | + | body | This is my article | + And there exists an "Article" document at "/cmf/articles/bar": + | title | Article 2 | + | body | Another one | + When I send a PATCH request to "/api/phpcrodm_repo/foo" with body: + """ + [{"operation": "move", "target": "/bar/foo"}] + """ + Then the response code should be 204 + And there is an "Article" document at "/cmf/articles/bar/foo": + | title | Article 1 | + | body | This is my article | + + Scenario: Remove a PHPCR-ODM resource + Given there exists an "Article" document at "/cmf/articles/foo": + | title | Article 1 | + | body | This is my article | + When I send a DELETE request to "/api/phpcrodm_repo/foo" + Then the response code should be 204 + And there is no "Article" document at "/cmf/articles/foo" diff --git a/Tests/Unit/Registry/PayloadAliasRegistryTest.php b/Tests/Unit/Registry/PayloadAliasRegistryTest.php index ccb4f83..44d2f91 100644 --- a/Tests/Unit/Registry/PayloadAliasRegistryTest.php +++ b/Tests/Unit/Registry/PayloadAliasRegistryTest.php @@ -15,7 +15,9 @@ class PayloadAliasRegistryTest extends \PHPUnit_Framework_TestCase { - private $registry; + private $repositoryRegistry; + private $resource; + private $repository; public function setUp() { diff --git a/composer.json b/composer.json index 93c8f77..ad711b5 100644 --- a/composer.json +++ b/composer.json @@ -9,11 +9,10 @@ "homepage": "https://github.com/symfony-cmf/symfony-cmf/contributors" } ], - "minimum-stability": "dev", - "prefer-stable": true, "require": { "php": "^5.5.6|^7.0", - "symfony-cmf/resource-bundle": "1.*", + "symfony-cmf/resource-bundle": "^1.0", + "puli/repository": "@beta", "jms/serializer-bundle": "1.*" }, "require-dev": { @@ -26,6 +25,8 @@ "matthiasnoback/symfony-config-test": "~0.1", "sonata-project/doctrine-phpcr-admin-bundle": "^1.2" }, + "minimum-stability": "dev", + "prefer-stable": true, "suggest": { "doctrine/phpcr-odm": "To enable support for the PHPCR ODM documents (^1.2)", "doctrine/phpcr-bundle": "To enable support for the PHPCR ODM documents"