From 51838cc9ea6396d54e36d78010cd6442480c09b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Fri, 11 Oct 2013 12:19:00 +0200 Subject: [PATCH 01/19] Refactor branch --- CHANGELOG.md | 4 + README.md | 33 +- UPGRADE.md | 5 - composer.json | 2 +- config/module.config.php | 104 +----- docs/annotation-mapping-reference.md | 139 -------- docs/cook-book.md | 114 ------- docs/img/zfr-rest-error-access-resource.png | Bin 259606 -> 0 bytes docs/quick-start/01-introduction.md | 38 --- docs/quick-start/02-define-entity.md | 114 ------- docs/quick-start/03-configuring-router.md | 55 --- docs/quick-start/04-creating-controller.md | 169 --------- docs/quick-start/05-associations.md | 211 ------------ src/ZfrRest/Controller/CacheController.php | 52 --- src/ZfrRest/Exception/ExceptionInterface.php | 29 -- .../CreateResourcePayloadListenerFactory.php | 43 --- .../Factory/DecoderPluginManagerFactory.php | 47 --- .../Factory/Exception/RuntimeException.php | 113 ------- src/ZfrRest/Factory/JsonDecoderFactory.php | 40 --- .../Factory/ModelPluginManagerFactory.php | 47 --- src/ZfrRest/Factory/ModuleOptionsFactory.php | 42 --- .../Factory/PaginatorHydratorFactory.php | 50 --- .../Factory/ResourceGraphRouteFactory.php | 82 ----- .../Factory/ResourceMetadataCacheFactory.php | 44 --- .../ResourceMetadataFactoryFactory.php | 101 ------ .../Factory/SelectModelListenerFactory.php | 43 --- .../Http/Exception/AbstractHttpException.php | 122 ------- .../Exception/Client/BadRequestException.php | 56 --- .../Exception/Client/ConflictException.php | 46 --- .../Exception/Client/ForbiddenException.php | 45 --- .../Http/Exception/Client/GoneException.php | 45 --- .../Client/MethodNotAllowedException.php | 45 --- .../Exception/Client/NotFoundException.php | 45 --- .../Client/UnauthorizedException.php | 96 ------ .../Http/Exception/ClientException.php | 54 --- .../Server/InternalServerErrorException.php | 92 ----- .../Server/NotImplementedException.php | 45 --- .../Server/ServiceUnavailableException.php | 46 --- .../Http/Exception/ServerException.php | 54 --- src/ZfrRest/Module.php | 28 +- .../Controller/AbstractRestfulController.php | 297 ---------------- .../Mvc/Exception/BadMethodCallException.php | 32 -- src/ZfrRest/Mvc/Exception/DomainException.php | 32 -- .../Mvc/Exception/RuntimeException.php | 62 ---- src/ZfrRest/Mvc/HttpExceptionListener.php | 70 ---- .../Mvc/HttpMethodOverrideListener.php | 67 ---- .../Mvc/Router/Http/ResourceGraphRoute.php | 320 ------------------ .../Http/CreateResourcePayloadListener.php | 105 ------ .../Mvc/View/Http/SelectModelListener.php | 155 --------- .../Options/ControllerBehavioursOptions.php | 83 ----- .../Options/Exception/RuntimeException.php | 32 -- src/ZfrRest/Options/ListenersOptions.php | 133 -------- src/ZfrRest/Options/ModuleOptions.php | 180 ---------- .../Options/ResourceMetadataOptions.php | 89 ----- src/ZfrRest/Paginator/ResourcePaginator.php | 53 --- .../Paginator/ResourcePaginatorInterface.php | 37 -- .../Exception/InvalidResourceException.php | 47 --- .../Resource/Exception/RuntimeException.php | 32 -- .../Annotation/AnnotationInterface.php | 40 --- .../Metadata/Annotation/Association.php | 47 --- .../Metadata/Annotation/Collection.php | 61 ---- .../Resource/Metadata/Annotation/Resource.php | 61 ---- .../Metadata/CollectionResourceMetadata.php | 69 ---- .../CollectionResourceMetadataInterface.php | 49 --- .../Metadata/Driver/AnnotationDriver.php | 194 ----------- .../ResourceMetadataDriverInterface.php | 39 --- .../Resource/Metadata/ResourceMetadata.php | 142 -------- .../Metadata/ResourceMetadataInterface.php | 92 ----- src/ZfrRest/Resource/Resource.php | 82 ----- src/ZfrRest/Resource/ResourceInterface.php | 50 --- .../Serializer/DecoderPluginManager.php | 68 ---- .../Serializer/Exception/RuntimeException.php | 46 --- .../Stdlib/Hydrator/PaginatorHydrator.php | 76 ----- src/ZfrRest/Version.php | 29 -- .../View/Exception/RuntimeException.php | 46 --- src/ZfrRest/View/Model/ModelPluginManager.php | 62 ---- .../Asset/Annotation/AbstractPage.php | 44 --- tests/ZfrRestTest/Asset/Annotation/Page.php | 54 --- tests/ZfrRestTest/Asset/Annotation/Tweet.php | 108 ------ tests/ZfrRestTest/Asset/Annotation/User.php | 111 ------ .../Asset/Controller/UserController.php | 37 -- .../Asset/Hydrator/UserHydrator.php | 56 --- .../Asset/InputFilter/UserInputFilter.php | 38 --- tests/ZfrRestTest/Asset/Request/Request.php | 36 -- .../Client/UnauthorizedExceptionTest.php | 32 -- .../Http/Exception/ClientExceptionTest.php | 52 --- .../Http/Exception/ServerExceptionTest.php | 51 --- .../ZfrRestTest/Mvc/Asset/DummyController.php | 48 --- ...bstractRestfulControllerFunctionalTest.php | 112 ------ .../AbstractRestfulControllerTest.php | 106 ------ .../Mvc/HttpExceptionListenerTest.php | 96 ------ .../Mvc/HttpMethodOverrideListenerTest.php | 64 ---- .../Http/ResourceGraphRouteFunctionalTest.php | 80 ----- .../Router/Http/ResourceGraphRouteTest.php | 170 ---------- .../CreateResourcePayloadListenerTest.php | 113 ------- .../Mvc/View/Http/SelectModelListenerTest.php | 148 -------- .../CollectionResourceMetadataTest.php | 62 ---- .../Metadata/Driver/AnnotationDriverTest.php | 66 ---- .../Metadata/ResourceMetadataTest.php | 112 ------ tests/ZfrRestTest/Resource/ResourceTest.php | 98 ------ .../Serializer/DecoderPluginManagerTest.php | 70 ---- .../View/Model/ModelPluginManagerTest.php | 67 ---- 102 files changed, 12 insertions(+), 7638 deletions(-) delete mode 100644 UPGRADE.md delete mode 100644 docs/annotation-mapping-reference.md delete mode 100644 docs/cook-book.md delete mode 100644 docs/img/zfr-rest-error-access-resource.png delete mode 100644 docs/quick-start/01-introduction.md delete mode 100644 docs/quick-start/02-define-entity.md delete mode 100644 docs/quick-start/03-configuring-router.md delete mode 100644 docs/quick-start/04-creating-controller.md delete mode 100644 docs/quick-start/05-associations.md delete mode 100644 src/ZfrRest/Controller/CacheController.php delete mode 100644 src/ZfrRest/Exception/ExceptionInterface.php delete mode 100644 src/ZfrRest/Factory/CreateResourcePayloadListenerFactory.php delete mode 100644 src/ZfrRest/Factory/DecoderPluginManagerFactory.php delete mode 100644 src/ZfrRest/Factory/Exception/RuntimeException.php delete mode 100644 src/ZfrRest/Factory/JsonDecoderFactory.php delete mode 100644 src/ZfrRest/Factory/ModelPluginManagerFactory.php delete mode 100644 src/ZfrRest/Factory/ModuleOptionsFactory.php delete mode 100644 src/ZfrRest/Factory/PaginatorHydratorFactory.php delete mode 100644 src/ZfrRest/Factory/ResourceGraphRouteFactory.php delete mode 100644 src/ZfrRest/Factory/ResourceMetadataCacheFactory.php delete mode 100644 src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php delete mode 100644 src/ZfrRest/Factory/SelectModelListenerFactory.php delete mode 100644 src/ZfrRest/Http/Exception/AbstractHttpException.php delete mode 100644 src/ZfrRest/Http/Exception/Client/BadRequestException.php delete mode 100644 src/ZfrRest/Http/Exception/Client/ConflictException.php delete mode 100644 src/ZfrRest/Http/Exception/Client/ForbiddenException.php delete mode 100644 src/ZfrRest/Http/Exception/Client/GoneException.php delete mode 100644 src/ZfrRest/Http/Exception/Client/MethodNotAllowedException.php delete mode 100644 src/ZfrRest/Http/Exception/Client/NotFoundException.php delete mode 100644 src/ZfrRest/Http/Exception/Client/UnauthorizedException.php delete mode 100644 src/ZfrRest/Http/Exception/ClientException.php delete mode 100644 src/ZfrRest/Http/Exception/Server/InternalServerErrorException.php delete mode 100644 src/ZfrRest/Http/Exception/Server/NotImplementedException.php delete mode 100644 src/ZfrRest/Http/Exception/Server/ServiceUnavailableException.php delete mode 100644 src/ZfrRest/Http/Exception/ServerException.php delete mode 100644 src/ZfrRest/Mvc/Controller/AbstractRestfulController.php delete mode 100644 src/ZfrRest/Mvc/Exception/BadMethodCallException.php delete mode 100644 src/ZfrRest/Mvc/Exception/DomainException.php delete mode 100644 src/ZfrRest/Mvc/Exception/RuntimeException.php delete mode 100644 src/ZfrRest/Mvc/HttpExceptionListener.php delete mode 100644 src/ZfrRest/Mvc/HttpMethodOverrideListener.php delete mode 100644 src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php delete mode 100644 src/ZfrRest/Mvc/View/Http/CreateResourcePayloadListener.php delete mode 100644 src/ZfrRest/Mvc/View/Http/SelectModelListener.php delete mode 100644 src/ZfrRest/Options/ControllerBehavioursOptions.php delete mode 100644 src/ZfrRest/Options/Exception/RuntimeException.php delete mode 100644 src/ZfrRest/Options/ListenersOptions.php delete mode 100644 src/ZfrRest/Options/ModuleOptions.php delete mode 100644 src/ZfrRest/Options/ResourceMetadataOptions.php delete mode 100644 src/ZfrRest/Paginator/ResourcePaginator.php delete mode 100644 src/ZfrRest/Paginator/ResourcePaginatorInterface.php delete mode 100644 src/ZfrRest/Resource/Exception/InvalidResourceException.php delete mode 100644 src/ZfrRest/Resource/Exception/RuntimeException.php delete mode 100644 src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php delete mode 100644 src/ZfrRest/Resource/Metadata/Annotation/Association.php delete mode 100644 src/ZfrRest/Resource/Metadata/Annotation/Collection.php delete mode 100644 src/ZfrRest/Resource/Metadata/Annotation/Resource.php delete mode 100644 src/ZfrRest/Resource/Metadata/CollectionResourceMetadata.php delete mode 100644 src/ZfrRest/Resource/Metadata/CollectionResourceMetadataInterface.php delete mode 100644 src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php delete mode 100644 src/ZfrRest/Resource/Metadata/Driver/ResourceMetadataDriverInterface.php delete mode 100644 src/ZfrRest/Resource/Metadata/ResourceMetadata.php delete mode 100644 src/ZfrRest/Resource/Metadata/ResourceMetadataInterface.php delete mode 100644 src/ZfrRest/Resource/Resource.php delete mode 100644 src/ZfrRest/Resource/ResourceInterface.php delete mode 100644 src/ZfrRest/Serializer/DecoderPluginManager.php delete mode 100644 src/ZfrRest/Serializer/Exception/RuntimeException.php delete mode 100644 src/ZfrRest/Stdlib/Hydrator/PaginatorHydrator.php delete mode 100644 src/ZfrRest/Version.php delete mode 100644 src/ZfrRest/View/Exception/RuntimeException.php delete mode 100644 src/ZfrRest/View/Model/ModelPluginManager.php delete mode 100644 tests/ZfrRestTest/Asset/Annotation/AbstractPage.php delete mode 100644 tests/ZfrRestTest/Asset/Annotation/Page.php delete mode 100644 tests/ZfrRestTest/Asset/Annotation/Tweet.php delete mode 100644 tests/ZfrRestTest/Asset/Annotation/User.php delete mode 100644 tests/ZfrRestTest/Asset/Controller/UserController.php delete mode 100644 tests/ZfrRestTest/Asset/Hydrator/UserHydrator.php delete mode 100644 tests/ZfrRestTest/Asset/InputFilter/UserInputFilter.php delete mode 100644 tests/ZfrRestTest/Asset/Request/Request.php delete mode 100644 tests/ZfrRestTest/Http/Exception/Client/UnauthorizedExceptionTest.php delete mode 100644 tests/ZfrRestTest/Http/Exception/ClientExceptionTest.php delete mode 100644 tests/ZfrRestTest/Http/Exception/ServerExceptionTest.php delete mode 100644 tests/ZfrRestTest/Mvc/Asset/DummyController.php delete mode 100644 tests/ZfrRestTest/Mvc/Controller/AbstractRestfulControllerFunctionalTest.php delete mode 100644 tests/ZfrRestTest/Mvc/Controller/AbstractRestfulControllerTest.php delete mode 100644 tests/ZfrRestTest/Mvc/HttpExceptionListenerTest.php delete mode 100644 tests/ZfrRestTest/Mvc/HttpMethodOverrideListenerTest.php delete mode 100644 tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteFunctionalTest.php delete mode 100644 tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php delete mode 100644 tests/ZfrRestTest/Mvc/View/Http/CreateResourcePayloadListenerTest.php delete mode 100644 tests/ZfrRestTest/Mvc/View/Http/SelectModelListenerTest.php delete mode 100644 tests/ZfrRestTest/Resource/Metadata/CollectionResourceMetadataTest.php delete mode 100644 tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverTest.php delete mode 100644 tests/ZfrRestTest/Resource/Metadata/ResourceMetadataTest.php delete mode 100644 tests/ZfrRestTest/Resource/ResourceTest.php delete mode 100644 tests/ZfrRestTest/Serializer/DecoderPluginManagerTest.php delete mode 100644 tests/ZfrRestTest/View/Model/ModelPluginManagerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d07617..bca949e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 1.0.0 + +* Complete refactor, initial release (code cleaning, documentation...) + ## 0.0.2 * Exclude request base path from path diff --git a/README.md b/README.md index 8ec4c97..1069ff1 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,10 @@ ZfrRest ======= -[![Build Status](https://travis-ci.org/zf-fr/zfr-rest.png?branch=master)](https://travis-ci.org/zf-fr/zfr-rest) [![Coverage Status](https://coveralls.io/repos/zf-fr/zfr-rest/badge.png?branch=master)](https://coveralls.io/r/zf-fr/zfr-rest?branch=master) [![Dependency Status](https://www.versioneye.com/package/php--zfr--zfr-rest/badge.png)](https://www.versioneye.com/package/php--zfr--zfr-rest) - -Version 0.1.0 ([changelog](/CHANGELOG.md)) - -**[READ MORE ABOUT CURRENT STATUS](https://github.com/zf-fr/ZfrRest/issues/41)** - -## Is ZfrRest usable ? - -Current features of ZfrRest work well and ZfrRest is definitely usable for simple cases. However, please note that -we have many more features to come for more complex applications. Those changes may (well, for sure they will) break -the API at some point, but we will keep track of all the changes in the [UPGRADE](UPGRADE.md) guide. - -Ultimately, you are strongly encouraged to test the module and report feedbacks, PR... - -## Milestone - -Here is a basic roadmap of ZfrRest, by priority: - -1. Finish the work on the ResourceGraphRoute, especially the assemble method. -2. Better architecture for extracting/hydrating data, to especially allow to configure key names, payload structure... -(so that integrate ZfrRest with a MVC framework that has specific conventions to be as easy as write an adapter). -3. Add support for HATEOAS links. -4. Improve the performance (using proxy maybe useful in some places) -5. Cleanup the code +[![Build Status](https://travis-ci.org/zf-fr/zfr-rest.png?branch=master)](https://travis-ci.org/zf-fr/zfr-rest) +[![Coverage Status](https://coveralls.io/repos/zf-fr/zfr-rest/badge.png?branch=master)](https://coveralls.io/r/zf-fr/zfr-rest?branch=master) +[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/zf-fr/zfr-rest/badges/quality-score.png?s=78ed408c927e01cb27ab7f3cc04349a770132550)](https://scrutinizer-ci.com/g/zf-fr/zfr-rest/) +[![Dependency Status](https://www.versioneye.com/package/php--zfr--zfr-rest/badge.png)](https://www.versioneye.com/package/php--zfr--zfr-rest) ## Installation @@ -36,8 +16,3 @@ Then, add the keys "ZfrRest" to your modules list in `application.config.php` fi `zfr_rest.global.php.dist` into your `autoload` folder (don't forget to remove the .dist extension at the end!). For more details about how to use ZfrRest, please follow the [quick start](/docs/quick-start/01-introduction.md). -## Documentation - -* [Quick Start](/docs/quick-start/01-introduction.md) -* [Annotation mapping reference](/docs/annotation-mapping-reference.md) -* [Cook book](/docs/cook-book.md) diff --git a/UPGRADE.md b/UPGRADE.md deleted file mode 100644 index 7d39bf0..0000000 --- a/UPGRADE.md +++ /dev/null @@ -1,5 +0,0 @@ -# UPGRADE GUIDE - -## 0.1.0 - -* This is the first version, what do you want to upgrade? =) diff --git a/composer.json b/composer.json index 5909795..cc535ab 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "ZfrRest\\": "src/" }, "classmap": [ - "./Module.php" + "./" ] } } diff --git a/config/module.config.php b/config/module.config.php index 0f239d0..d1f3680 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -17,107 +17,5 @@ */ return array( - 'service_manager' => array( - 'invokables' => array( - 'ZfrRest\Mvc\HttpExceptionListener' => 'ZfrRest\Mvc\HttpExceptionListener', - 'ZfrRest\Mvc\HttpMethodOverrideListener' => 'ZfrRest\Mvc\HttpMethodOverrideListener', - ), - - 'factories' => array( - 'ZfrRest\Mvc\View\Http\SelectModelListener' => 'ZfrRest\Factory\SelectModelListenerFactory', - 'ZfrRest\Options\ModuleOptions' => 'ZfrRest\Factory\ModuleOptionsFactory', - 'ZfrRest\Resource\Metadata\CacheProvider' => 'ZfrRest\Factory\ResourceMetadataCacheFactory', - 'ZfrRest\Resource\Metadata\MetadataFactory' => 'ZfrRest\Factory\ResourceMetadataFactoryFactory', - 'ZfrRest\Serializer\DecoderPluginManager' => 'ZfrRest\Factory\DecoderPluginManagerFactory', - 'ZfrRest\View\Model\ModelPluginManager' => 'ZfrRest\Factory\ModelPluginManagerFactory', - 'ZfrRest\Mvc\View\Http\CreateResourcePayloadListener' - => 'ZfrRest\Factory\CreateResourcePayloadListenerFactory', - ) - ), - - 'console' => array( - 'router' => array( - 'routes' => array( - 'clear-cache' => array( - 'type' => 'Simple', - 'options' => array( - 'route' => 'rest clear metadata cache', - 'defaults' => array( - 'controller' => 'ZfrRest\Controller\Cache', - 'action' => 'clear-cache' - ) - ), - ), - ) - ) - ), - - 'controllers' => array( - 'invokables' => array( - 'ZfrRest\Controller\Cache' => 'ZfrRest\Controller\CacheController', - ) - ), - - 'hydrators' => array( - 'factories' => array( - 'ZfrRest\Stdlib\Hydrator\PaginatorHydrator' => 'ZfrRest\Factory\PaginatorHydratorFactory' - ), - 'aliases' => array( - 'PaginatorHydrator' => 'ZfrRest\Stdlib\Hydrator\PaginatorHydrator' - ) - ), - - 'route_manager' => array( - 'factories' => array( - 'ZfrRest\Mvc\Router\Http\ResourceGraphRoute' => 'ZfrRest\Factory\ResourceGraphRouteFactory' - ), - - 'aliases' => array( - 'ResourceGraphRoute' => 'ZfrRest\Mvc\Router\Http\ResourceGraphRoute' - ), - ), - - 'view_manager' => array( - 'strategies' => array( - 'ViewJsonStrategy' - ) - ), - - 'zfr_rest' => array( - /** - * Listeners options - */ - 'listeners' => array( - 'register_http_exception' => true, - 'register_create_resource_payload' => true, - 'register_select_model' => true, - 'register_http_method_override' => false - ), - - /** - * Which behaviours each controller should automatically do for us? - */ - 'controller_behaviours' => array( - 'auto_validate' => true, - 'auto_hydrate' => true - ), - - /** - * Resource metadata options - */ - 'resource_metadata' => array( - 'cache' => 'Doctrine\Common\Cache\ArrayCache', - 'drivers' => array() - ), - - /** - * Set DecoderPluginManager - */ - 'decoders' => array(), - - /** - * Set ModelPluginManager - */ - 'models' => array() - ) + 'service_manager' => array() ); diff --git a/docs/annotation-mapping-reference.md b/docs/annotation-mapping-reference.md deleted file mode 100644 index 05e4f47..0000000 --- a/docs/annotation-mapping-reference.md +++ /dev/null @@ -1,139 +0,0 @@ -# Annotations reference - -In this chapter a reference of ZfrRest annotations is given. For the examples to work, you must import a namespace: - -```php -use ZfrRest\Resource\Metadata\Annotation as REST; -``` - -## Index - -## Reference - -### Association - -This annotation is used to mark an association between two resources. This annotation can only be used at property level. - -*Optional attributes:* - -* **allowTraversal**: (default to false) If this attribute is set to true, then the association is exposed by the router, -hence allowing dispatching to the associated resource. - -*Example:* - -```php -/** - * @var Collection - * @REST\Association(allowTraversal=true, serializationStrategy="NONE") - */ -protected $tweets; -``` - -### Collection - -This annotation is used to define mapping about a collection of a given resource. This annotation basically defines -the same information as a Resource annotation, but in a collection context. This annotation can be used at class level -and property level. - -*Required attributes:* - -* **controller**: FQCN of the controller to use. - -*Optional attributes:* - -* **inputFilter**: FQCN of the input filter to use. If not set, it will reuse the input filter set in the `Resource` annotation. -* **hydrator** : FQCN of the hydrator to use. If not set, it will reuse the hydrator set in the `Resource` annotation. - -*Example:* - -```php -/** - * @REST\Collection( - * controller="Application\Controller\UserListController" - * ) - */ -class User -{ - // ... -} -``` - -### Resource - -This annotation is used to define the resource's mapping. This annotation can be used at class level and property -level. - -*Optional attributes:* - -* **controller**: FQCN of the controller to use. The controller must be added to the controllers plugin manager, -like any other Zend Framework 2 controllers. It must be a subclass of `ZfrRest\Mvc\Controller\AbstractRestfulController`. -* **inputFilter**: FQCN of the input filter to use. The input filter must be added to the input -filter plugin manager. This input filter is used to validate data for POST and PUT verbs. Note that this -attribute is **required** if you activate the *auto_validate* option (which is true by default). -* **hydrator**: FQCN of the hydrator to use. The hydrator must be added to the hydrator plugin manager. By default, -it uses the DoctrineModule hydrator (`DoctrineModule\Stdlib\Hydrator\DoctrineObject`) - -*Example:* - -```php -/** - * @REST\Resource( - * controller="Application\Controller\UserController", - * inputFilter="Application\InputFilter\UserInputFilter", - * hydrator="DoctrineModule\Stdlib\Hydrator\DoctrineObject" - * ) - */ -class User -{ - // ... -} -``` - -## Complete example - -Here is a complete example: - -```php -/** - * This example demonstrates a mapping for a User class. - * - * @REST\Resource( - * controller="Application\Controller\UserController", - * inputFilter="Application\InputFilter\UserInputFilter", - * hydrator="DoctrineModule\Stdlib\Hydrator\DoctrineObject" - * ) - * - * The mapping defined in Collection is used when we reach a URL that - * represent a collection (for instance /users) - * - * @REST\Collection( - * controller="Application\Controller\UserListController" - * ) - */ -class User -{ - /** - * @var int - */ - protected $id; - - /** - * @var string - */ - protected $firstName; - - /** - * @var Collection - * - * This will allow the following route: /users/:id/tweets. We also override the controller used. By default, it - * will reuse the mapping defined in the associated class (in this case, Tweet entity), but you may want to override - * some attributes based on the context - * - * @REST\Assocation(allowTraversal=true, serializationStrategy="NONE") - * @REST\Resource( - * controller="Application\Controller\UserTweetController") - * ) - */ - protected $tweets; -} -``` diff --git a/docs/cook-book.md b/docs/cook-book.md deleted file mode 100644 index 81f4946..0000000 --- a/docs/cook-book.md +++ /dev/null @@ -1,114 +0,0 @@ -# Cook-book - -## How to override and add new verbs - -Internally, when a request is dispatched to a controller, ZfrRest does two things: - -1. It calls a "handler". Handler method are like this: handle$verbMethod (handlePutMethod, handlePostMethod...). For -instance, handlePutMethod defined in AbstractRestfulController create the input filter, the hydrator... Each handler -receives the resource, and the resource metadata. -2. It then calls the method in your controller, like put, get... Most of the time, your controller only defines those -functions. - -If you want to add support for new verbs or override the default handler, create a new abstract controller: - -```php -namespace Application\Controller; - -use ZfrRest\Mvc\Controller\AbstractRestfulController; - -class AbstractCustomController extends AbstractRestfulController -{ - /** - * Add support for a new method not handled by default - */ - public function handlePatchMethod($resource, ResourceMetadataInterface $metadata) - { - // Do stuff... - return $this->patch($parameters); - } - - /** - * Override put handler. - */ - public function handlePutMethod($resource, ResourceMetadataInterface $metadata) - { - - } -} -``` - -Now, you can extend this new controller, and simply define the "patch" method: - -```php -namespace Application\Controller; - -use ZfrRest\Mvc\Controller\AbstractRestfulController; - -class UserController extends AbstractCustomController -{ - public function patch($parameters) - { - } -} -``` - -## How to prevent ZfrRest from auto-validating and/or auto-hydrating - -By default when POSTing or PUTing, ZfrRest automatically validate the data using the input filter defined in the -mapping, and hydrate it using the hydrator defined in the mapping (it defaults to `DoctrineModule\Stdlib\Hydrator\DoctrineObject` -for single resource, or `ZfrRest\Stdlib\Hydrator\PaginatorHydrator` for collections). - -However, you may want to do it yourself because you have very specify requirements. You can do it by disabling the -option `auto_hydrate` and `auto_validate` options in the config file (the keys lie under the `controller_behaviours` key). - -## Listeners - -ZfrRest registers various listeners that can be activated/deactivated in the config file. Just uncomment specific -lines in the `zfr_rest.global.php` file. - -## How to deal with custom Accept/Content-Type mime types ? - -By default, ZfrRest comes bundled with native support for JSON. However, you may want to use your own mime types, like -"application/vnd-user-json". For this to work, you need to do two things: - -* Add a decoder that will map a mime-type to a decoder. It is used among other to extract data from body in -POST and PUT methods. -* Add a model that will map a mime-type to a view model instance. It is used for ZfrRest to automatically output -an object (eg. a User instance). - -Here is how you would do it: - -```php -return array( - 'zfr_rest' => array( - 'models' => array( - 'invokables' => array( - 'application/vnd-user-json' => 'Zend\View\Model\ViewModel' - ) - ), - - 'decoders' => array( - 'invokables' => array( - 'application/vnd-user-json' => 'ZfrRest\Factory\JsonDecoderFactory' - ) - ) - ) -); -``` - -By default, ZfrRest map the following models: - -* 'text/html' to 'Zend\View\Model\ViewModel' -* 'application/xhtml+xml' to 'Zend\View\Model\ViewModel' -* 'application/json' to 'Zend\View\Model\JsonModel' -* 'application/javascript' to 'Zend\View\Model\JsonModel' - -And the following decoders: - -* 'application/xml' to 'Symfony\Component\Serializer\Encoder\XmlEncoder' -* 'application/json' to 'ZfrRest\Factory\JsonDecoderFactory' -* 'application/javascript' to 'ZfrRest\Factory\JsonDecoderFactory' - -> Note that although we have a decoder for Xml, ZfrRest currently does not fully support Xml because Zend Framework 2 -does not have any XmlModel as of today. diff --git a/docs/img/zfr-rest-error-access-resource.png b/docs/img/zfr-rest-error-access-resource.png deleted file mode 100644 index cff4989eabea7c448f8cda82092af650026c17b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 259606 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 array( - 'object_manager' => 'doctrine.entitymanager.orm_default' - ) -); -``` - -Users of DoctrineMongoODMModule: - -```php -return array( - 'zfr_rest' => array( - 'object_manager' => 'doctrine.documentmanager.odm_default' - ) -); -``` - -Users that are not using Doctrine can [learn more in the cook-book](../cook-book.md) about how to use with other -persistence layer like Zend\Db. - -[In next part](02-define-entity.md), you are going to define your entity. diff --git a/docs/quick-start/02-define-entity.md b/docs/quick-start/02-define-entity.md deleted file mode 100644 index f962f94..0000000 --- a/docs/quick-start/02-define-entity.md +++ /dev/null @@ -1,114 +0,0 @@ -# Quick start - -## Entity - -We are going to create a very simple REST applications that interact with users. We want to be able to create, read, -delete and update users. Let's first create the entity: - -```php -id; - } - - /** - * @param string $name - * @return void - */ - public function setName($name) - { - $this->name = $name; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } -} -``` - -This entity contains the minimal mapping we need for ZfrRest, but we are going to add more data as soon as we -progress. The ORM annotations are specific to Doctrine, so please read their documentation for more information. - -Notice the @REST\Resource annotation. This mapping is specific to ZfrRest. This annotation can accept multiple -attributes, but here we only use the "controller" attribute. This is to say to ZfrRest to map the user resource to -a specific controller. The @REST\Collection is used in cases when the resource is a collection of objects. Indeed, -in a REST model, an user and a list of users are two distinct resources. That's why we provide different mappings. - -Said otherwise, typing "/users" will be dispatched to the `UserListController` (it is a collection of users) while -typing "/users/4" will be dispatched to the `UserController`. - -As any Zend Framework 2 controllers, we need to add them to the controllers plugin manager. In your `module.config.php` -file, add the following: - -```php -'controllers' => array( - 'invokables' => array( - 'Application\Controller\UserController' => 'Application\Controller\UserController', - 'Application\Controller\UserListController' => 'Application\Controller\UserListController', - ), -), -``` - -### Configurate the mapping - -In those examples, we are using annotations, so in order to ZfrRest to "read" the mapping, we must add a driver. -Drivers in ZfrRest work pretty the same way as for Doctrine. In your `module.config.php`, add the following: - -```php -return array( - 'zfr_rest' => array( - 'object_manager' => 'doctrine.entitymanager.orm_default', - - 'resource_metadata' => array( - 'drivers' => array( - 'annotation_driver' => array( - 'class' => 'ZfrRest\Resource\Metadata\Driver\AnnotationDriver' - ) - ) - ) - ), -); -``` - -> In DoctrineORMModule for instance, you need to add one driver per module. In ZfrRest, the annotation driver is -"global" to the whole application. - -[In next part](03-configuring-router.md), you are going to configure the router. diff --git a/docs/quick-start/03-configuring-router.md b/docs/quick-start/03-configuring-router.md deleted file mode 100644 index 21d8542..0000000 --- a/docs/quick-start/03-configuring-router.md +++ /dev/null @@ -1,55 +0,0 @@ -# Quick start - -## Configuring the router - -If we type "/users" in our browser, nothing happen (yet). This is because we need to create a route. The route is an -entry-point to a specific resource. What is nice, however, is that associated resources are automatically discovered. -For instance, if your User entity has an association with a collection of Tweet resources, you don't need to write two -routes! More on that later. - -In your `module.config.php` file, add the following route: - -```php -return array( - 'router' => array( - 'routes' => array( - 'users' => array( - 'type' => 'ResourceGraphRoute', - 'options' => array( - 'route' => '/users', - 'resource' => 'Application\Repository\UserRepository' - ) - ) - ) - ), -); -``` - -The type of the route, `ResourceGraphRoute`, is a route provided by ZfrRest. Notice the `resource` option. The object -identified by the `resource` option is an object that is used to retrieve resources of a specific type. Everything in -ZfrRest is retrieved using the service locator for easier dependencies handling. Therefore, we need to add a factory -in the service manager to tell Zend Framework how to retrieve it: - -```php -return array( - 'service_manager' => array( - 'factories' => array( - 'Application\Repository\UserRepository' => function($sl) { - $objectManager = $sl->get('doctrine.entitymanager.orm_default'); - return $objectManager->getRepository('Application\Entity\User'); - } - ) - ), -); -``` - -If writing a factory for each repository is too tiring, you can always write an abstract factory for that. - -Now, let's type "/users" in our browser. Here is what happen: - -![ZfrRest](../img/zfr-rest-error-access-resource.png) - -It throws an exception, which is normal. This is because we didn't write our controller. We are going to do that -in next part. - -[In next part](04-creating-controller.md), you are going to create the controllers. diff --git a/docs/quick-start/04-creating-controller.md b/docs/quick-start/04-creating-controller.md deleted file mode 100644 index b1fa2aa..0000000 --- a/docs/quick-start/04-creating-controller.md +++ /dev/null @@ -1,169 +0,0 @@ -# Quick start - -## Controllers - -Let's make this query work by writing the `get` method in the `UserListController`. This is because "/users" returns a -collection of users, so it belongs to the controller defined in the Collection mapping. Please notice that all -controllers must extend from `ZfrRest\Mvc\Controller\AbstractRestfulController`: - -```php -setCurrentPageNumber(1); - - return $users; - } -} -``` - -As you can see, ZfrRest automatically created a paginator for us. This is because it knows it is a collection. Notice -also that we directly return the paginator. In fact, ZfrRest is smart enough to choose the appropriate view model type -based on the Accept header. For instance, if the Accept header is `application/json`, a JsonModel will be created for -you. Here is the output if the database contains one user: - -```json -{ - "current_page": 1, - "count_per_page": 10, - "items": [{ - "id": 1, - "name": "Michael" - }] -} -``` - -You can also "force" ZfrRest to always return a specific type by returning a ViewModel instance. - -Now, let's write the POST method. It belongs to the `UserListController` because we are adding a new resource to a -collection! Hopefully, it's damn simple: - -```php -serviceLocator->get('doctrine.entitymanager.orm_default'); - $em->persist($user); - $em->flush(); - } -} -``` - -ZfrRest did a lot of us here. It extracted the data from the body, validated it, hydrated it and gave us the result. -The only thing we need to do is our true logic (persist the object, send an email…). - -However, you may ask *how* ZfrRest can validate data. In fact, for this to work, you need to add an attribute to the -Resource mapping, as shown below: - -```php -/** - * @ORM\Entity - * @ORM\Table(name="Users") - * @REST\Resource( - * controller="Application\Controller\UserController", - * inputFilter="Application\InputFilter\UserInputFilter" - * ) - */ -class User -{ -``` - -The UserInputFilter is fetched from the input filter plugin manager. So if you have complex dependencies for an input -filter, you can create a factory: - -```php -// in module.config.php -return array( - 'input_filters' => array( - 'factories' => array( - 'MyComplexInputFilter' => 'Application\InputFilter\MyComplexInputFilterFactory' - ) - ) -); -``` - -If the input filter does not validate, ZfrRest will automatically returns a 400 error, with an "errors" key that -contains the fields that failed. - -Note that for POST requests, ZfrRest automatically returns a 201 answer (Created), and add a Location header with the -link to the newly created resource. - -> Note: if you don't want ZfrRest to auto-validate and auto-hydrate values for you, you can! Instead of objects, you -will receive plain array, and it's up to you to create the input filter and hydrate the data. See the cook-book for -learning how to do it. - -The GET for a single item, DELETE and PUT are then written in the `UserController`: - -```php -serviceLocator->get('doctrine.entitymanager.orm_default'); - $em->flush(); - return $user; - } - - public function delete(User $user) - { - $em = $this->serviceLocator->get('doctrine.entitymanager.orm_default'); - $em->remove($user); - $em->flush(); - } -} -``` - -### Throw exceptions - -In your controller actions, you may want to return specific HTTP status code (this is a good practice for nice APIs!). -ZfrRest make it really simple. For instance, if you want to disallow access to a specific resource: - -```php -use ZfrRest\Http\Exception\Client; - -public function get(User $user) -{ - if ($this->request->fromQuery('token') !== 'valid') { - throw new Client\UnauthorizedException(); - } - - return $user; -} -``` - -ZfrRest has built-in exceptions for the most common `4xx` and `5xx` HTTP response codes. - -[In next part](05-associations.md), you are going to learn how to use associations with ZfrRest. diff --git a/docs/quick-start/05-associations.md b/docs/quick-start/05-associations.md deleted file mode 100644 index ffca3c3..0000000 --- a/docs/quick-start/05-associations.md +++ /dev/null @@ -1,211 +0,0 @@ -# Quick start - -## Associations - -ZfrRest can also be used to traverse associations. To illustrate this, let's add an assocation from the User entity -to a new Tweet entity. - -Here is the updated User entity: - -```php -tweets = new ArrayCollection(); - } - - /** - * @return int - */ - public function getId() - { - return $this->id; - } - - /** - * @param string $name - * @return void - */ - public function setName($name) - { - $this->name = $name; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * @return Array|ArrayCollection - */ - public function getTweets() - { - return $this->tweets; - } -} -``` - -And the Tweet entity: - -```php -id; - } - - /** - * @param \Application\Entity\User $user - * @return void - */ - public function setUser($user) - { - $this->user = $user; - } - - /** - * @return \Application\Entity\User - */ - public function getUser() - { - return $this->user; - } - - /** - * @param string $content - * @return void - */ - public function setContent($content) - { - $this->content = $content; - } - - /** - * @return string - */ - public function getContent() - { - return $this->content; - } -} -``` - -As you can see, the "tweets" properties on the User entity has the annotation "Association". This indicate to the -router that it can recursively traverses a new resource. It therefore opens the following URLs: "/users/:user_id/tweets" and -"/users/:user_id/tweets/:tweet_id". - -However, the routes "/tweets" and "/tweets/:tweet_id" are still not accessible (for this to work, you would need to add -a new root route in the config file, like we did earlier for the user. - -The new routes use the mapping described in the Tweet entity. For example, the URL GET "/users/4/tweets" will be -dispatched to the controller defined in the Collection mapping of the Tweet entity (in this case, the TweetListController). -However, in some cases, you may want to change the mapping based on the context. This is why you can use both the -Collection and Resource annotations at the association level to override mapping. For instance: - -```php -class User -{ - // ... - - /** - * @var Array - * - * @ORM\OneToMany(targetEntity="Tweet", mappedBy="user") - * @REST\Association - * @REST\Collection(controller="Application\Controller\UserTweetListController") - */ - protected $tweets; -} -``` - -Now, the URL GET "/users/4/tweets" will be dispatched to `Application\Controller\UserTweetListController`. - -We're done with the quick start ! For more advanced features, please refer to the [cook-book](../cook-book.md). diff --git a/src/ZfrRest/Controller/CacheController.php b/src/ZfrRest/Controller/CacheController.php deleted file mode 100644 index 58fdce1..0000000 --- a/src/ZfrRest/Controller/CacheController.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ -class CacheController extends AbstractActionController -{ - /** - * Clear the cache (for now, only for metadata) - * - * @return string - */ - public function clearCacheAction() - { - try { - /** @var $cache \Doctrine\Common\Cache\CacheProvider */ - $cache = $this->serviceLocator->get('ZfrRest\Resource\Metadata\CacheProvider'); - } catch (Exception $e) { - return "\nNo cache to clear. Are you sure you set ZfrRest cache correctly?\n\n"; - } - - if ($cache->flushAll()) { - return "\nThe cache were successfully cleared\n\n"; - } - - return "\nImpossible to clear the cache\n\n"; - } -} diff --git a/src/ZfrRest/Exception/ExceptionInterface.php b/src/ZfrRest/Exception/ExceptionInterface.php deleted file mode 100644 index d3a1885..0000000 --- a/src/ZfrRest/Exception/ExceptionInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -interface ExceptionInterface -{ -} diff --git a/src/ZfrRest/Factory/CreateResourcePayloadListenerFactory.php b/src/ZfrRest/Factory/CreateResourcePayloadListenerFactory.php deleted file mode 100644 index 82ed562..0000000 --- a/src/ZfrRest/Factory/CreateResourcePayloadListenerFactory.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class CreateResourcePayloadListenerFactory implements FactoryInterface -{ - /** - * {@inheritDoc} - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - /* @var $hydratorManager \Zend\Stdlib\Hydrator\HydratorPluginManager */ - $hydratorManager = $serviceLocator->get('HydratorManager'); - - return new CreateResourcePayloadListener($hydratorManager); - } -} diff --git a/src/ZfrRest/Factory/DecoderPluginManagerFactory.php b/src/ZfrRest/Factory/DecoderPluginManagerFactory.php deleted file mode 100644 index e89e2e1..0000000 --- a/src/ZfrRest/Factory/DecoderPluginManagerFactory.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ -class DecoderPluginManagerFactory implements FactoryInterface -{ - /** - * {@inheritDoc} - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - /* @var $options \ZfrRest\Options\ModuleOptions */ - $options = $serviceLocator->get('ZfrRest\Options\ModuleOptions'); - - $decoderPluginManager = new DecoderPluginManager(new Config($options->getDecoders())); - $decoderPluginManager->setServiceLocator($serviceLocator); - - return $decoderPluginManager; - } -} diff --git a/src/ZfrRest/Factory/Exception/RuntimeException.php b/src/ZfrRest/Factory/Exception/RuntimeException.php deleted file mode 100644 index 4b72196..0000000 --- a/src/ZfrRest/Factory/Exception/RuntimeException.php +++ /dev/null @@ -1,113 +0,0 @@ - - */ -class RuntimeException extends BaseRuntimeException implements ExceptionInterface -{ - /** - * @param ServiceLocatorInterface $serviceLocator - * - * @return self - */ - public static function pluginManagerExpected(ServiceLocatorInterface $serviceLocator) - { - return new self( - sprintf('A hydrator plugin manager was expected, but "%s" was given', get_class($serviceLocator)) - ); - } - - /** - * @param string $resourceName - * @param Exception|null $previous - * - * @return self - */ - public static function missingResource($resourceName, Exception $previous = null) - { - return new self(sprintf('Resource "%s" cannot be found in the service locator', $resourceName), 0, $previous); - } - - /** - * @param string $serviceName - * @param Exception|null $previous - * - * @return self - */ - public static function missingObjectManager($serviceName, Exception $previous = null) - { - return new self(sprintf('The object manager key is not valid, "%s" given', $serviceName), 0, $previous); - } - - /** - * @param string $serviceName - * @param mixed $objectManager - * - * @return self - */ - public static function invalidObjectManager($serviceName, $objectManager) - { - return new self( - sprintf( - 'Invalid ObjectManager retrieved for service "%s", instance of "%s" found', - $serviceName, - is_object($objectManager) ? get_class($objectManager) : gettype($objectManager) - ) - ); - } - - /** - * @param string $serviceName - * @param mixed $cache - * - * @return self - */ - public static function invalidCache($serviceName, $cache) - { - return new self( - sprintf( - 'Invalid CacheInterface retrieved for service "%s", instance of "%s" found', - $serviceName, - is_object($cache) ? get_class($cache) : gettype($cache) - ) - ); - } - - /** - * @param string $driverClass - * - * @return self - */ - public static function invalidDriverClass($driverClass) - { - return new self( - sprintf('Unrecognized driver class "%s" given', $driverClass) - ); - } -} diff --git a/src/ZfrRest/Factory/JsonDecoderFactory.php b/src/ZfrRest/Factory/JsonDecoderFactory.php deleted file mode 100644 index aaa8f6a..0000000 --- a/src/ZfrRest/Factory/JsonDecoderFactory.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ -class JsonDecoderFactory implements FactoryInterface -{ - /** - * {@inheritDoc} - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - return new JsonDecode(true); - } -} diff --git a/src/ZfrRest/Factory/ModelPluginManagerFactory.php b/src/ZfrRest/Factory/ModelPluginManagerFactory.php deleted file mode 100644 index 4608d4b..0000000 --- a/src/ZfrRest/Factory/ModelPluginManagerFactory.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ -class ModelPluginManagerFactory implements FactoryInterface -{ - /** - * {@inheritDoc} - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - /* @var $options \ZfrRest\Options\ModuleOptions */ - $options = $serviceLocator->get('ZfrRest\Options\ModuleOptions'); - - $modelPluginManager = new ModelPluginManager(new Config($options->getModels())); - $modelPluginManager->setServiceLocator($serviceLocator); - - return $modelPluginManager; - } -} diff --git a/src/ZfrRest/Factory/ModuleOptionsFactory.php b/src/ZfrRest/Factory/ModuleOptionsFactory.php deleted file mode 100644 index 0b7c7f0..0000000 --- a/src/ZfrRest/Factory/ModuleOptionsFactory.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -class ModuleOptionsFactory implements FactoryInterface -{ - /** - * {@inheritDoc} - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - $config = $serviceLocator->get('Config'); - - return new ModuleOptions($config['zfr_rest']); - } -} diff --git a/src/ZfrRest/Factory/PaginatorHydratorFactory.php b/src/ZfrRest/Factory/PaginatorHydratorFactory.php deleted file mode 100644 index f529906..0000000 --- a/src/ZfrRest/Factory/PaginatorHydratorFactory.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ -class PaginatorHydratorFactory implements FactoryInterface -{ - /** - * {@inheritDoc} - * - * @return PaginatorHydrator - * - * @throws RuntimeException - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - if (! $serviceLocator instanceof HydratorPluginManager) { - throw RuntimeException::pluginManagerExpected($serviceLocator); - } - - return new PaginatorHydrator($serviceLocator); - } -} diff --git a/src/ZfrRest/Factory/ResourceGraphRouteFactory.php b/src/ZfrRest/Factory/ResourceGraphRouteFactory.php deleted file mode 100644 index aacbc39..0000000 --- a/src/ZfrRest/Factory/ResourceGraphRouteFactory.php +++ /dev/null @@ -1,82 +0,0 @@ -creationOptions = $creationOptions; - } - - /** - * {@inheritDoc} - * - * @return ResourceGraphRoute - * - * @throws RuntimeException - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - if (! $serviceLocator instanceof AbstractPluginManager) { - throw RuntimeException::pluginManagerExpected($serviceLocator); - } - - $parentLocator = $serviceLocator->getServiceLocator(); - - try { - $resource = $parentLocator->get($this->creationOptions['resource']); - } catch (ServiceNotFoundException $exception) { - throw RuntimeException::missingResource($this->creationOptions['resource'], $exception); - } - - /* @var $metadataFactory \Metadata\MetadataFactoryInterface */ - $metadataFactory = $parentLocator->get('ZfrRest\Resource\Metadata\MetadataFactory'); - - return new ResourceGraphRoute($metadataFactory, $resource, $this->creationOptions['route']); - } -} diff --git a/src/ZfrRest/Factory/ResourceMetadataCacheFactory.php b/src/ZfrRest/Factory/ResourceMetadataCacheFactory.php deleted file mode 100644 index 7132b91..0000000 --- a/src/ZfrRest/Factory/ResourceMetadataCacheFactory.php +++ /dev/null @@ -1,44 +0,0 @@ -get('ZfrRest\Options\ModuleOptions'); - $cacheClass = $moduleOptions->getResourceMetadata()->getCache(); - - if (!$cacheClass) { - return null; - } - - return new $cacheClass(); - } -} diff --git a/src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php b/src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php deleted file mode 100644 index 0e08848..0000000 --- a/src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php +++ /dev/null @@ -1,101 +0,0 @@ - - */ -class ResourceMetadataFactoryFactory implements FactoryInterface -{ - /** - * {@inheritDoc} - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - /** @var $moduleOptions \ZfrRest\Options\ModuleOptions */ - $moduleOptions = $serviceLocator->get('ZfrRest\Options\ModuleOptions'); - $resourceOptions = $moduleOptions->getResourceMetadata(); - - try { - /* @var $objectManager \Doctrine\Common\Persistence\ObjectManager */ - $objectManager = $serviceLocator->get($moduleOptions->getObjectManager()); - } catch (ServiceNotFoundException $exception) { - throw RuntimeException::missingObjectManager($moduleOptions->getObjectManager(), $exception); - } - - if (! $objectManager instanceof ObjectManager) { - throw RuntimeException::invalidObjectManager($moduleOptions->getObjectManager(), $objectManager); - } - - $doctrineMetadataFactory = $objectManager->getMetadataFactory(); - $driversOptions = $resourceOptions->getDrivers(); - /* @var $metadataDrivers ResourceMetadataDriverInterface[] */ - $metadataDrivers = array(); - - // The annotation driver does not need to be added twice - foreach ($driversOptions as $driverOptions) { - switch ($driverOptions['class']) { - case 'ZfrRest\Resource\Metadata\Driver\AnnotationDriver': - //AnnotationRegistry::registerAutoloadNamespace('ZfrRest\Resource\Annotation', __DIR__ . '/../..'); - $metadataDrivers[] = new AnnotationDriver(new AnnotationReader(), $doctrineMetadataFactory); - break; - default: - throw RuntimeException::invalidDriverClass($driverOptions['class']); - } - } - - $resourceMetadataFactory = new MetadataFactory(new DriverChain($metadataDrivers)); - - // We need to inject the resource metadata factory into each driver to allow them to retrieve - // metadata from one driver to another - // @todo this part should be removed - foreach ($metadataDrivers as $metadataDriver) { - if ($metadataDriver instanceof ResourceMetadataDriverInterface) { - $metadataDriver->setResourceMetadataFactory($resourceMetadataFactory); - } - } - - // Also add a cache if one is set - $cache = $serviceLocator->get('ZfrRest\Resource\Metadata\CacheProvider'); - - if ($cache instanceof CacheInterface) { - $resourceMetadataFactory->setCache($cache); - } - - - return $resourceMetadataFactory; - } -} diff --git a/src/ZfrRest/Factory/SelectModelListenerFactory.php b/src/ZfrRest/Factory/SelectModelListenerFactory.php deleted file mode 100644 index a917ec9..0000000 --- a/src/ZfrRest/Factory/SelectModelListenerFactory.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class SelectModelListenerFactory implements FactoryInterface -{ - /** - * {@inheritDoc} - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - /* @var $modelPluginManager \ZfrRest\View\Model\ModelPluginManager */ - $modelPluginManager = $serviceLocator->get('ZfrRest\View\Model\ModelPluginManager'); - - return new SelectModelListener($modelPluginManager); - } -} diff --git a/src/ZfrRest/Http/Exception/AbstractHttpException.php b/src/ZfrRest/Http/Exception/AbstractHttpException.php deleted file mode 100644 index b6aaab7..0000000 --- a/src/ZfrRest/Http/Exception/AbstractHttpException.php +++ /dev/null @@ -1,122 +0,0 @@ - - */ -abstract class AbstractHttpException extends Exception implements ExceptionInterface, HttpExceptionInterface -{ - /** - * @var int - */ - protected $statusCode; - - /** - * @var string - */ - protected $message = 'An error occurred'; - - /** - * @var mixed - */ - protected $errors; - - - /** - * @param null|int $statusCode - * @param string $message - * @param string $errors - */ - public function __construct($statusCode = null, $message = '', $errors = '') - { - if ($statusCode !== null) { - $this->setStatusCode($statusCode); - } - - if ($message !== '') { - $this->message = $message; - } - - $this->errors = $errors; - } - - /** - * Set the status code of the HTTP error - * - * @param int $statusCode - * @return AbstractHttpException - */ - public function setStatusCode($statusCode) - { - $this->statusCode = (int) $statusCode; - } - - /** - * Get the status code of the HTTP error - * - * @return int - */ - public function getStatusCode() - { - return $this->statusCode; - } - - /** - * Set errors (this is a generic placeholder for more errors) - * - * @param mixed $errors - * @return AbstractHttpException - */ - public function setErrors($errors) - { - $this->errors = $errors; - return $this; - } - - /** - * Get errors - * - * @return mixed - */ - public function getErrors() - { - return $this->errors; - } - - /** - * Prepare the response for the exception - * - * @param HttpResponse $response - * @return void - */ - public function prepareResponse(HttpResponse $response) - { - $response->setStatusCode($this->getStatusCode()); - $response->setReasonPhrase($this->getMessage()); - } -} diff --git a/src/ZfrRest/Http/Exception/Client/BadRequestException.php b/src/ZfrRest/Http/Exception/Client/BadRequestException.php deleted file mode 100644 index c31e7a5..0000000 --- a/src/ZfrRest/Http/Exception/Client/BadRequestException.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ -class BadRequestException extends ClientException -{ - /** - * @var string - */ - protected $message = 'The request cannot be fulfilled due to bad syntax'; - - - /** - * @param string $message - * @param mixed $errors - */ - public function __construct($message = '', $errors = '') - { - parent::__construct(400, $message, $errors); - } - - /** - * @param InputFilterInterface $inputFilter - * - * @return self - */ - public static function invalidInput(InputFilterInterface $inputFilter) - { - return new self('Invalid data provided', $inputFilter->getMessages()); - } -} diff --git a/src/ZfrRest/Http/Exception/Client/ConflictException.php b/src/ZfrRest/Http/Exception/Client/ConflictException.php deleted file mode 100644 index 655d4d8..0000000 --- a/src/ZfrRest/Http/Exception/Client/ConflictException.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ -class ConflictException extends ClientException -{ - /** - * @var string - */ - protected $message = - 'The request could not be processed because of conflict in the request, such as an edit conflict'; - - - /** - * @param string $message - * @param mixed $errors - */ - public function __construct($message = '', $errors = '') - { - parent::__construct(409, $message, $errors); - } -} diff --git a/src/ZfrRest/Http/Exception/Client/ForbiddenException.php b/src/ZfrRest/Http/Exception/Client/ForbiddenException.php deleted file mode 100644 index c84c5d5..0000000 --- a/src/ZfrRest/Http/Exception/Client/ForbiddenException.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class ForbiddenException extends ClientException -{ - /** - * @var string - */ - protected $message = 'The request was a valid request, but the server is refusing to respond to it'; - - - /** - * @param string $message - * @param mixed $errors - */ - public function __construct($message = '', $errors = '') - { - parent::__construct(403, $message, $errors); - } -} diff --git a/src/ZfrRest/Http/Exception/Client/GoneException.php b/src/ZfrRest/Http/Exception/Client/GoneException.php deleted file mode 100644 index ad6cafb..0000000 --- a/src/ZfrRest/Http/Exception/Client/GoneException.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class GoneException extends ClientException -{ - /** - * @var string - */ - protected $message = 'The requested resource is no longer available and will not be available again'; - - - /** - * @param string $message - * @param mixed $errors - */ - public function __construct($message = '', $errors = '') - { - parent::__construct(410, $message, $errors); - } -} diff --git a/src/ZfrRest/Http/Exception/Client/MethodNotAllowedException.php b/src/ZfrRest/Http/Exception/Client/MethodNotAllowedException.php deleted file mode 100644 index 606fa4f..0000000 --- a/src/ZfrRest/Http/Exception/Client/MethodNotAllowedException.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class MethodNotAllowedException extends ClientException -{ - /** - * @var string - */ - protected $message = 'A request was made using a HTTP method not supported by that resource'; - - - /** - * @param string $message - * @param mixed $errors - */ - public function __construct($message = '', $errors = '') - { - parent::__construct(405, $message, $errors); - } -} diff --git a/src/ZfrRest/Http/Exception/Client/NotFoundException.php b/src/ZfrRest/Http/Exception/Client/NotFoundException.php deleted file mode 100644 index c4cce3e..0000000 --- a/src/ZfrRest/Http/Exception/Client/NotFoundException.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class NotFoundException extends ClientException -{ - /** - * @var string - */ - protected $message = 'The requested resource could not be found but may be available again in the future'; - - - /** - * @param string $message - * @param mixed $errors - */ - public function __construct($message = '', $errors = '') - { - parent::__construct(404, $message, $errors); - } -} diff --git a/src/ZfrRest/Http/Exception/Client/UnauthorizedException.php b/src/ZfrRest/Http/Exception/Client/UnauthorizedException.php deleted file mode 100644 index d8c80e8..0000000 --- a/src/ZfrRest/Http/Exception/Client/UnauthorizedException.php +++ /dev/null @@ -1,96 +0,0 @@ - - */ -class UnauthorizedException extends ClientException -{ - /** - * @var string - */ - protected $message = 'You are not authorized to access to the requested resource'; - - /** - * @var string - */ - protected $challenge = 'Basic'; - - - /** - * @param string $message - * @param mixed $errors - * @param string $challenge - */ - public function __construct($message = '', $errors = '', $challenge = '') - { - if (!empty($challenge)) { - $this->setChallenge($challenge); - } - - parent::__construct(401, $message, $errors); - } - - /** - * Set the challenge method used for authentication that will appear in the "WWW-Authenticate" header - * - * @param string $challenge - * @return UnauthorizedException - */ - public function setChallenge($challenge) - { - $this->challenge = $challenge; - return $this; - } - - /** - * Get the challenge method used for authentication that will appear in the "WWW-Authenticate" header - * - * @return string - */ - public function getChallenge() - { - return $this->challenge; - } - - /** - * According to RFC 2617 (http://www.ietf.org/rfc/rfc2617.txt), the 401 response message MUST - * contain a WWW-Authenticate header - * - * {@inheritDoc} - */ - public function prepareResponse(HttpResponse $response) - { - parent::prepareResponse($response); - - $headers = $response->getHeaders(); - $challenge = $this->getChallenge(); - $authenticateHeader = Header\WWWAuthenticate::fromString("WWW-Authenticate: $challenge"); - - $headers->addHeader($authenticateHeader); - } -} diff --git a/src/ZfrRest/Http/Exception/ClientException.php b/src/ZfrRest/Http/Exception/ClientException.php deleted file mode 100644 index 9343039..0000000 --- a/src/ZfrRest/Http/Exception/ClientException.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ -class ClientException extends AbstractHttpException -{ - /** - * @var string - */ - protected $message = 'A client error occurred'; - - - /** - * @param null|int $statusCode - * @param string $message - * @param mixed $errors - * @throws \InvalidArgumentException If status code is not 4xx - */ - public function __construct($statusCode, $message = '', $errors = '') - { - // Client errors code are 4xx - if ($statusCode < 400 || $statusCode > 499) { - throw new InvalidArgumentException( - sprintf('Status code for client errors must be between 400 and 499, %s given', $statusCode) - ); - } - - parent::__construct($statusCode, $message, $errors); - } -} diff --git a/src/ZfrRest/Http/Exception/Server/InternalServerErrorException.php b/src/ZfrRest/Http/Exception/Server/InternalServerErrorException.php deleted file mode 100644 index 603018a..0000000 --- a/src/ZfrRest/Http/Exception/Server/InternalServerErrorException.php +++ /dev/null @@ -1,92 +0,0 @@ - - */ -class InternalServerErrorException extends ServerException -{ - /** - * @var string - */ - protected $message = 'An internal server error occurred'; - - - /** - * @param string $message - * @param mixed $errors - */ - public function __construct($message = '', $errors = '') - { - parent::__construct(500, $message, $errors); - } - - /** - * @return self - */ - public static function missingInputFilter() - { - return new self('No input filter class was given, although controller is configured to auto validate'); - } - - /** - * @param string $inputFilterName - * @param Exception|null $previous - * - * @return self - * - * @todo should handle also $previous exception - */ - public static function invalidInputFilter($inputFilterName, Exception $previous = null) - { - return new self( - sprintf('An invalid input filter class name was given when validating data ("%s" given)', $inputFilterName) - ); - } - - /** - * @return self - */ - public static function missingHydrator() - { - return new self('No hydrator was given, although controller is configured to auto hydrate'); - } - - /** - * @param string $hydratorName - * @param Exception|null $previous - * - * @return self - * - * @todo should handle also $previous exception - */ - public static function invalidHydrator($hydratorName, Exception $previous = null) - { - return new self( - sprintf('An invalid hydrator class name was given when hydrating data ("%s" given)', $hydratorName) - ); - } -} diff --git a/src/ZfrRest/Http/Exception/Server/NotImplementedException.php b/src/ZfrRest/Http/Exception/Server/NotImplementedException.php deleted file mode 100644 index bd0e21a..0000000 --- a/src/ZfrRest/Http/Exception/Server/NotImplementedException.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class NotImplementedException extends ServerException -{ - /** - * @var string - */ - protected $message = - 'The server either does not recognize the request method, or it lacks the ability to fulfill the request'; - - /** - * @param string $message - * @param mixed $errors - */ - public function __construct($message = '', $errors = '') - { - parent::__construct(501, $message, $errors); - } -} diff --git a/src/ZfrRest/Http/Exception/Server/ServiceUnavailableException.php b/src/ZfrRest/Http/Exception/Server/ServiceUnavailableException.php deleted file mode 100644 index 7570cb2..0000000 --- a/src/ZfrRest/Http/Exception/Server/ServiceUnavailableException.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ -class ServiceUnavailableException extends ServerException -{ - /** - * @var string - */ - protected $message = - 'The server is currently unavailable (because it is overloaded or down for maintenance). Please try again later'; - - - /** - * @param string $message - * @param mixed $errors - */ - public function __construct($message = '', $errors = '') - { - parent::__construct(503, $message, $errors); - } -} diff --git a/src/ZfrRest/Http/Exception/ServerException.php b/src/ZfrRest/Http/Exception/ServerException.php deleted file mode 100644 index 13ea7a4..0000000 --- a/src/ZfrRest/Http/Exception/ServerException.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ -class ServerException extends AbstractHttpException -{ - /** - * @var string - */ - protected $message = 'A server error occurred'; - - - /** - * @param null|int $statusCode - * @param string $message - * @param mixed $errors - * @throws \InvalidArgumentException If status code is not 5xx - */ - public function __construct($statusCode, $message = '', $errors = '') - { - // Server errors code are 5xx - if ($statusCode < 500 || $statusCode > 599) { - throw new InvalidArgumentException( - sprintf('Status code for server errors must be between 500 and 599, %s given', $statusCode) - ); - } - - parent::__construct($statusCode, $message, $errors); - } -} diff --git a/src/ZfrRest/Module.php b/src/ZfrRest/Module.php index adb3f14..6fac060 100644 --- a/src/ZfrRest/Module.php +++ b/src/ZfrRest/Module.php @@ -22,8 +22,6 @@ use Zend\EventManager\EventInterface; use Zend\ModuleManager\Feature\BootstrapListenerInterface; use Zend\ModuleManager\Feature\ConfigProviderInterface; -use Zend\ModuleManager\Feature\ConsoleBannerProviderInterface; -use Zend\ModuleManager\Feature\ConsoleUsageProviderInterface; use Zend\ModuleManager\Feature\DependencyIndicatorInterface; /** @@ -34,8 +32,6 @@ class Module implements BootstrapListenerInterface, ConfigProviderInterface, - ConsoleBannerProviderInterface, - ConsoleUsageProviderInterface, DependencyIndicatorInterface { /** @@ -49,7 +45,7 @@ public function onBootstrap(EventInterface $event) $eventManager = $application->getEventManager(); /** @var $moduleOptions \ZfrRest\Options\ModuleOptions */ - $moduleOptions = $serviceManager->get('ZfrRest\Options\ModuleOptions'); + /*$moduleOptions = $serviceManager->get('ZfrRest\Options\ModuleOptions'); $listenersOptions = $moduleOptions->getListeners(); if ($listenersOptions->getRegisterHttpException()) { @@ -66,7 +62,7 @@ public function onBootstrap(EventInterface $event) if ($listenersOptions->getRegisterSelectModel()) { $eventManager->attach($serviceManager->get('ZfrRest\Mvc\View\Http\SelectModelListener')); - } + }*/ } /** @@ -77,26 +73,6 @@ public function getConfig() return include __DIR__ . '/../../config/module.config.php'; } - /** - * {@inheritDoc} - */ - public function getConsoleBanner(AdapterInterface $console) - { - return 'ZfrRest ' . Version::VERSION; - } - - /** - * {@inheritDoc} - */ - public function getConsoleUsage(AdapterInterface $console) - { - return array( - 'Usage:', - - 'rest clear metadata cache' => 'Clear all resource metadata cache' - ); - } - /** * {@inheritDoc} */ diff --git a/src/ZfrRest/Mvc/Controller/AbstractRestfulController.php b/src/ZfrRest/Mvc/Controller/AbstractRestfulController.php deleted file mode 100644 index c1ec67d..0000000 --- a/src/ZfrRest/Mvc/Controller/AbstractRestfulController.php +++ /dev/null @@ -1,297 +0,0 @@ - - */ -abstract class AbstractRestfulController extends AbstractController -{ - /** - * {@inheritDoc} - */ - public function dispatch(RequestInterface $request, ResponseInterface $response = null) - { - if (!$request instanceof HttpRequest) { - throw new Exception\InvalidArgumentException('Expected an HTTP request'); - } - - return parent::dispatch($request, $response); - } - - /** - * Execute the request. Try to match the HTTP verb to an action - * - * @param MvcEvent $event - * @return mixed - * @throws Client\NotFoundException If the resource cannot be found - * @throws Client\MethodNotAllowedException If the method to handle the request is not implemented - */ - public function onDispatch(MvcEvent $event) - { - $method = strtolower($this->getRequest()->getMethod()); - $handler = 'handle' . ucfirst($method) . 'Method'; - - if (!method_exists($this, $method) || !method_exists($this, $handler)) { - throw new Client\MethodNotAllowedException(); - } - - /** @var \ZfrRest\Resource\ResourceInterface $resource */ - $resource = $event->getRouteMatch()->getParam('resource', null); - - // We should always have a resource, otherwise throw an 404 exception - if (null === $resource) { - throw new Client\NotFoundException(); - } - - $return = $this->$handler($resource); - - $event->setResult($return); - - return $return; - } - - /** - * GET method is used to retrieve (or read) a representation of a resource. Get method is idempotent, which means - * that making multiple identical requests ends up having the same result as a single request. Get requests should - * not modify any resources - * - * @param ResourceInterface $resource - * @return mixed - */ - protected function handleGetMethod(ResourceInterface $resource) - { - return $this->get($resource->getData(), $resource->getMetadata()); - } - - /** - * DELETE method is used to delete a representation of a resource - * - * @param ResourceInterface $resource - * @return mixed - */ - protected function handleDeleteMethod(ResourceInterface $resource) - { - return $this->delete($resource->getData(), $resource->getMetadata()); - } - - /** - * POST method is used to create a new resource. On successful creation, POST method should return a HTTP status - * 201, with a Location header containing the URL of the newly created resource. We are doing several things for the - * user automatically: - * - we validate post data with the input filter defined in metadata - * - we hydrate valid data - * - we pass the object to the post method of the controller - * - * As you can see, the post method have three arguments: the object that is inserted, the resource metadata and - * the resource itself (which is the Collection where the object is added) - * - * Note that if you have set "auto_validate" and/or "auto_hydrate" to false in ZfrRest config, those steps will - * do nothing - * - * @param ResourceInterface $resource - * @throws Client\BadRequestException if validation fails - * @return mixed - */ - protected function handlePostMethod(ResourceInterface $resource) - { - $metadata = $resource->getMetadata(); - $singleResource = $metadata->createResource(); - - $data = $this->validateData($metadata->getInputFilterName(), $this->decodeBody()); - $data = $this->hydrateData($metadata->getHydratorName(), $data, $singleResource); - - $data = $this->post($data, $metadata, $resource); - - // Set the Location header with the URL to the newly created resource - if (is_object($data)) { - // @FIXME: use Router for that - $identifierValues = $metadata->getClassMetadata()->getIdentifierValues($data); - $identifierValue = reset($identifierValues); - $url = '/' . trim($this->request->getUri()->getPath(), '/') . '/' . $identifierValue; - - $this->response->getHeaders()->addHeaderLine('Location', $url); - } - - $this->response->setStatusCode(201); - - return $data; - } - - /** - * PUT method is used to update an existing resource. We are doing several things for the user automatically: - * - we validate post data with the input filter defined in metadata - * - we hydrate valid data to update existing resource - * - we pass the object to the put method of the controller - * - * Note that if you have set "auto_validate" and/or "auto_hydrate" to false in ZfrRest config, those steps will - * do nothing - * - * @param ResourceInterface $resource - * @throws Client\BadRequestException if validation fails - * @return mixed - */ - protected function handlePutMethod(ResourceInterface $resource) - { - $metadata = $resource->getMetadata(); - - $data = $this->validateData($metadata->getInputFilterName(), $this->decodeBody()); - $data = $this->hydrateData($metadata->getHydratorName(), $data, $resource); - - return $this->put($data, $metadata); - } - - /** - * Automatically create an InputFilter object, and validate data against it. - * - * @param string $inputFilterName - * @param array $data - * @throws Server\InternalServerErrorException If the input filter class is not valid - * @throws Client\BadRequestException If input filter create validation errors - * @return array - */ - protected function validateData($inputFilterName, array $data) - { - /** @var $moduleOptions \ZfrRest\Options\ModuleOptions */ - $moduleOptions = $this->serviceLocator->get('ZfrRest\Options\ModuleOptions'); - $controllerBehaviours = $moduleOptions->getControllerBehaviours(); - - if (!$controllerBehaviours->getAutoValidate()) { - return $data; - } - - if (empty($inputFilterName)) { - throw InternalServerErrorException::missingInputFilter(); - } - - $inputFilterManager = $this->serviceLocator->get('InputFilterManager'); - - try { - $inputFilter = $inputFilterManager->get($inputFilterName); - } catch (ServiceNotFoundException $exception) { - throw InternalServerErrorException::invalidInputFilter($inputFilterName, $exception); - } - - /** @param \Zend\InputFilter\InputFilterInterface $inputFilter */ - $inputFilter->setData($data); - if (!$inputFilter->isValid()) { - throw BadRequestException::invalidInput($inputFilter); - } - - // Return validated and filtered values - return $inputFilter->getValues(); - } - - /** - * Automatically create a Hydrator object, and hydrate object. If ZfrRest was configured to not hydrate - * automatically, then this method only returns untouched data as array - * - * @param string $hydratorName - * @param array $data - * @param ResourceInterface $resource - * @throws Server\InternalServerErrorException - * @return array|object - */ - public function hydrateData($hydratorName, array $data, ResourceInterface $resource) - { - /** @var $moduleOptions \ZfrRest\Options\ModuleOptions */ - $moduleOptions = $this->serviceLocator->get('ZfrRest\Options\ModuleOptions'); - $controllerBehaviours = $moduleOptions->getControllerBehaviours(); - - if (!$controllerBehaviours->getAutoHydrate()) { - return $data; - } - - if (empty($hydratorName)) { - throw InternalServerErrorException::missingHydrator(); - } - - $hydratorManager = $this->serviceLocator->get('HydratorManager'); - - try { - $hydrator = $hydratorManager->get($hydratorName); - } catch (ServiceNotFoundException $exception) { - throw InternalServerErrorException::invalidHydrator($hydratorName, $exception); - } - - /** @param \Zend\Stdlib\Hydrator\HydratorInterface $hydrator */ - return $hydrator->hydrate($data, $resource->getData()); - } - - /** - * Parse the body content according to the Content-Type value - * - * @return array - */ - protected function decodeBody() - { - return $this->decode($this->request->getContent()) ?: array(); - } - - /** - * Decode a content according to the Content-Type value - * - * @param mixed $content - * @return array - */ - protected function decode($content) - { - /** @var $decoderPluginManager \ZfrRest\Serializer\DecoderPluginManager */ - $decoderPluginManager = $this->serviceLocator->get('ZfrRest\Serializer\DecoderPluginManager'); - - /* @var $request \Zend\Http\Request */ - $request = $this->getRequest(); - /* @var $header \Zend\Http\Header\ContentType */ - $header = $request->getHeader('Content-Type'); - - if (!$header) { - return null; - } - - $contentTypes = Accept::fromString('Accept: ' . $header->getFieldValue())->getPrioritized(); - /* @var $contentType \Zend\Http\Header\Accept\FieldValuePart\AcceptFieldValuePart */ - $contentType = reset($contentTypes); - $mimeType = $contentType->getTypeString(); - - return $decoderPluginManager->get($mimeType)->decode($content, ''); - } -} diff --git a/src/ZfrRest/Mvc/Exception/BadMethodCallException.php b/src/ZfrRest/Mvc/Exception/BadMethodCallException.php deleted file mode 100644 index ae667b8..0000000 --- a/src/ZfrRest/Mvc/Exception/BadMethodCallException.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -class BadMethodCallException extends BaseBadMethodCallException implements ExceptionInterface -{ -} diff --git a/src/ZfrRest/Mvc/Exception/DomainException.php b/src/ZfrRest/Mvc/Exception/DomainException.php deleted file mode 100644 index f20e83a..0000000 --- a/src/ZfrRest/Mvc/Exception/DomainException.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -class DomainException extends BaseDomainException implements ExceptionInterface -{ -} diff --git a/src/ZfrRest/Mvc/Exception/RuntimeException.php b/src/ZfrRest/Mvc/Exception/RuntimeException.php deleted file mode 100644 index 088a604..0000000 --- a/src/ZfrRest/Mvc/Exception/RuntimeException.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ -class RuntimeException extends BaseRuntimeException implements ExceptionInterface -{ - /** - * @param ClassMetadata $classMetadata - * - * @return self - */ - public static function missingCollectionMetadata(ClassMetadata $classMetadata) - { - return new self( - sprintf( - 'Collection metadata not found. Do you have a @Collection annotation for the resource "%s"?', - $classMetadata->getName() - ) - ); - } - - /** - * @param mixed $resource - * - * @return self - */ - public static function unsupportedResourceType($resource) - { - return new self( - sprintf( - 'Resource "%s" is not supported: either specify an ObjectRepository instance, or an entity class name', - is_object($resource) ? get_class($resource) : gettype($resource) - ) - ); - } -} diff --git a/src/ZfrRest/Mvc/HttpExceptionListener.php b/src/ZfrRest/Mvc/HttpExceptionListener.php deleted file mode 100644 index cef0a85..0000000 --- a/src/ZfrRest/Mvc/HttpExceptionListener.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ -class HttpExceptionListener extends AbstractListenerAggregate -{ - /** - * {@inheritDoc} - */ - public function attach(EventManagerInterface $events) - { - $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, array($this, 'onDispatchError'), 100); - } - - /** - * Get the exception and optionally set status code and reason message - * - * @param MvcEvent $event - * @return void - */ - public function onDispatchError(MvcEvent $event) - { - /** @var $response HttpResponse */ - $response = $event->getResponse(); - $exception = $event->getParam('exception'); - - // We just deal with our Http error codes here ! - if (!$exception instanceof AbstractHttpException || !$response instanceof HttpResponse) { - return; - } - - $exception->prepareResponse($response); - - $data = array( - 'status_code' => $exception->getStatusCode(), - 'message' => $exception->getMessage(), - 'errors' => $exception->getErrors() - ); - - $event->setResult(array_filter($data)); - } -} diff --git a/src/ZfrRest/Mvc/HttpMethodOverrideListener.php b/src/ZfrRest/Mvc/HttpMethodOverrideListener.php deleted file mode 100644 index b18b3a3..0000000 --- a/src/ZfrRest/Mvc/HttpMethodOverrideListener.php +++ /dev/null @@ -1,67 +0,0 @@ - - */ -class HttpMethodOverrideListener extends AbstractListenerAggregate -{ - /** - * {@inheritDoc} - */ - public function attach(EventManagerInterface $events) - { - $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, array($this, 'overrideHttpMethod'), 1000); - } - - /** - * Check if the X-HTTP-Method-Override exist in the request, and if so, change the method - * - * @param MvcEvent $event - * @return void - */ - public function overrideHttpMethod(MvcEvent $event) - { - $request = $event->getRequest(); - - if (!$request instanceof HttpRequest) { - return; - } - - $header = $request->getHeader('X-HTTP-Method-Override', null); - - if ($header !== null) { - $method = $header->getFieldValue(); - $request->setMethod($method); - } - } -} diff --git a/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php b/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php deleted file mode 100644 index 707875e..0000000 --- a/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php +++ /dev/null @@ -1,320 +0,0 @@ - - * @author Michaël Gallego - */ -class ResourceGraphRoute implements RouteInterface -{ - /** - * @var MetadataFactoryInterface - */ - protected $metadataFactory; - - /** - * @var ResourceInterface|mixed - */ - protected $resource; - - /** - * Entry point route - * - * @var string - */ - protected $route; - - /** - * Optional GET parameters that are extracted from the request - * - * @var array - */ - protected $query; - - - /** - * @param MetadataFactoryInterface $metadataFactory - * @param mixed $resource - * @param string $route - */ - public function __construct(MetadataFactoryInterface $metadataFactory, $resource, $route) - { - $this->metadataFactory = $metadataFactory; - $this->route = (string) $route; - $this->resource = $resource; - } - - /** - * {@inheritDoc} - */ - public function assemble(array $params = array(), array $options = array()) - { - // TODO: Implement assemble() method. - } - - /** - * {@inheritDoc} - */ - public function getAssembledParams() - { - // TODO: Implement getAssembledParams() method. - } - - /** - * {@inheritDoc} - */ - public static function factory($options = array()) - { - throw new Exception\BadMethodCallException('Not supported'); - } - - /** - * {@inheritDoc} - */ - public function match(Request $request) - { - if (!method_exists($request, 'getUri')) { - return null; - } - - /* @var $request \Zend\Http\Request */ - $uri = $request->getUri(); - $path = $uri->getPath(); - - // we must ommit the basePath - if (method_exists($request, 'getBaseUrl') && $baseUrl = $request->getBaseUrl()) { - $path = substr($path, strlen(rtrim($baseUrl, '/'))); - } - - $matchedPath = rtrim($path, '/'); - - // Save the query part (GET parameters) to optionally filter the result at the end - $this->query = $uri->getQueryAsArray(); - - // @todo consider using a segment/part route to handle this logic instead - // If the route is not even contained within the URI, this means we can return early... - if (strpos($matchedPath, $this->route) === false && strpos($path, $this->route) === false) { - return null; - } - - // ...and we can now initialize the resource - $this->initializeResource(); - - if ($matchedPath === $this->route || $path === $this->route) { - return $this->buildRouteMatch($this->resource, $path); - } - - $identifierPath = substr($path, strlen(rtrim($this->route, '/'))); - - if (0 !== strpos($identifierPath, '/') || ! $this->resource->isCollection()) { - return null; - } - - return $this->matchIdentifier($this->resource, $identifierPath); - } - - /** - * @param ResourceInterface $resource - * @param string $path - * @throws Exception\RuntimeException - * @return RouteMatch|null - */ - protected function matchIdentifier(ResourceInterface $resource, $path) - { - $path = trim($path, '/'); - $classMetadata = $resource->getMetadata()->getClassMetadata(); - $identifiers = $classMetadata->getIdentifierFieldNames(); - - if (count($identifiers) > 1) { - throw new Exception\RuntimeException('Composite identifiers are not currently supported by ZfrRest'); - } - - $data = $resource->getData(); - $chunks = explode('/', $path); - - // Favor Repository over Selectable as it allows to call custom repository methods - if ($data instanceof ObjectRepository) { - $data = $data->find(array_shift($chunks)); - } elseif ($resource instanceof Selectable) { - $expression = Criteria::expr()->eq(current($identifiers), array_shift($chunks)); - $data = $data->matching(new Criteria($expression))->first(); - } - - if (null === $data) { - return $this->buildErrorRouteMatch($resource, $path); - } - - // We matched an identifier, so the metadata stay the same (but we moved from a Collection to - // a single item) - $resource = new Resource($data, $resource->getMetadata()); - - // If empty, then we have processed the whole path - if (empty($chunks)) { - return $this->buildRouteMatch($resource, $path); - } - - return $this->matchAssociation($resource, substr($path, strpos($path, '/'))); - } - - /** - * @param ResourceInterface $resource - * @param string $path - * @return RouteMatch|null - */ - protected function matchAssociation(ResourceInterface $resource, $path) - { - $path = trim($path, '/'); - $resourceMetadata = $resource->getMetadata(); - $classMetadata = $resourceMetadata->getClassMetadata(); - - $chunks = explode('/', $path); - $associationName = array_shift($chunks); - - if (!$resourceMetadata->hasAssociation($associationName)) { - return null; - } - - $reflectionClass = $classMetadata->getReflectionClass(); - $reflectionProperty = $reflectionClass->getProperty($associationName); - - $reflectionProperty->setAccessible(true); - - $data = $reflectionProperty->getValue($resource->getData()); - - $resourceMetadata = $resourceMetadata->getAssociationMetadata($associationName); - $resource = new Resource($data, $resourceMetadata); - - // If empty, we have processed the whole path - if (empty($chunks)) { - return $this->buildRouteMatch($resource, $path); - } - - return $this->matchIdentifier($resource, substr($path, strpos($path, '/'))); - } - - /** - * Build a route match. This function extract the controller from the resource metadata, and does - * optional filtering by query - * - * @param ResourceInterface $resource - * @param string $path - * @throws Exception\RuntimeException - * @return RouteMatch - */ - protected function buildRouteMatch(ResourceInterface $resource, $path) - { - $resourceMetadata = $resource->getMetadata(); - $collectionMetadata = $resourceMetadata->getCollectionMetadata(); - $classMetadata = $resourceMetadata->getClassMetadata(); - $data = $resource->getData(); - - if ($data instanceof Selectable) { - $criteria = Criteria::create(); - - foreach ($this->query as $key => $value) { - if ($classMetadata->hasField($key)) { - $criteria->andWhere(Criteria::expr()->eq($key, $value)); - } - } - - // @TODO: for now, collection is always wrapped around a ResourcePaginator, should instead be configurable - $data = new ResourcePaginator($resourceMetadata, new SelectableAdapter($data, $criteria)); - - $resource = new Resource($data, $resourceMetadata); - } - - // If returned $data is a collection, then we use the controller specified in Collection mapping - if ($resource->isCollection()) { - if (null === $collectionMetadata) { - throw Exception\RuntimeException::missingCollectionMetadata($classMetadata); - } - - $controllerName = $collectionMetadata->getControllerName(); - } else { - $controllerName = $resourceMetadata->getControllerName(); - } - - return new RouteMatch( - array( - 'resource' => $resource, - 'controller' => $controllerName - ), - strlen($path) - ); - } - - /** - * Build an error route match. This can happen if, for instance, no object was found after matching an - * identifier. However, we still want to dispatch to the controller so that we can do further error handling - * - * @param ResourceInterface $resource - * @param string $path - * @return RouteMatch - */ - protected function buildErrorRouteMatch(ResourceInterface $resource, $path) - { - return new RouteMatch(array('controller' => $resource->getMetadata()->getControllerName()), strlen($path)); - } - - /** - * Initialize the resource to create an object implementing the ResourceInterface interface. A resource can - * be anything: an entity, a collection, a Selectable... However, any ResourceInterface object contains both - * the resource AND metadata associated to it. This metadata is usually extracted from the entity name - * - * @throws Exception\RuntimeException - * @return void - */ - private function initializeResource() - { - // Don't initialize twice - if ($this->resource instanceof ResourceInterface) { - return; - } - - /** @var $metadata \Metadata\ClassHierarchyMetadata */ - $metadata = null; - $resource = $this->resource; - - if ($resource instanceof ObjectRepository) { - $metadata = $this->metadataFactory->getMetadataForClass($resource->getClassName()); - } elseif (is_string($resource)) { - $metadata = $this->metadataFactory->getMetadataForClass($resource); - } else { - throw RuntimeException::unsupportedResourceType($resource); - } - - $this->resource = new Resource($resource, $metadata->getOutsideClassMetadata()); - } -} diff --git a/src/ZfrRest/Mvc/View/Http/CreateResourcePayloadListener.php b/src/ZfrRest/Mvc/View/Http/CreateResourcePayloadListener.php deleted file mode 100644 index 8f81771..0000000 --- a/src/ZfrRest/Mvc/View/Http/CreateResourcePayloadListener.php +++ /dev/null @@ -1,105 +0,0 @@ - - */ -class CreateResourcePayloadListener extends AbstractListenerAggregate -{ - /** - * @var HydratorPluginManager - */ - protected $hydratorPluginManager; - - /** - * @param HydratorPluginManager $hydratorPluginManager - */ - public function __construct(HydratorPluginManager $hydratorPluginManager) - { - $this->hydratorPluginManager = $hydratorPluginManager; - } - - /** - * {@inheritDoc} - */ - public function attach(EventManagerInterface $events) - { - $sharedManager = $events->getSharedManager(); - - $sharedManager->attach( - 'Zend\Stdlib\DispatchableInterface', - MvcEvent::EVENT_DISPATCH, - array($this, 'createPayload'), - -40 - ); - } - - /** - * The logic is as follow: extract the resource metadata, use the bound hydrator to extract data, and set the - * data as new result - * - * @param MvcEvent $event - * @return void - */ - public function createPayload(MvcEvent $event) - { - $result = $event->getResult(); - - if ($result instanceof ModelInterface || $result instanceof ResponseInterface || empty($result)) { - return; - } - - $routeMatch = $event->getRouteMatch(); - - if (! $routeMatch) { - return; - } - - $resource = $routeMatch->getParam('resource'); - - if (! $resource instanceof ResourceInterface) { - return; - } - - $resourceMetadata = $resource->getMetadata(); - - if ($result instanceof Traversable || is_array($result)) { - $hydratorName = $resourceMetadata->getCollectionMetadata()->getHydratorName(); - } else { - $hydratorName = $resourceMetadata->getHydratorName(); - } - - $hydrator = $this->hydratorPluginManager->get($hydratorName); - - $event->setResult($hydrator->extract($result)); - } -} diff --git a/src/ZfrRest/Mvc/View/Http/SelectModelListener.php b/src/ZfrRest/Mvc/View/Http/SelectModelListener.php deleted file mode 100644 index 3289f43..0000000 --- a/src/ZfrRest/Mvc/View/Http/SelectModelListener.php +++ /dev/null @@ -1,155 +0,0 @@ - - */ -class SelectModelListener extends AbstractListenerAggregate -{ - /** - * @var ModelPluginManager - */ - protected $modelPluginManager; - - - /** - * Constructor - * - * @param ModelPluginManager $modelPluginManager - */ - public function __construct(ModelPluginManager $modelPluginManager) - { - $this->modelPluginManager = $modelPluginManager; - } - - /** - * {@inheritDoc} - */ - public function attach(EventManagerInterface $events) - { - $sharedManager = $events->getSharedManager(); - - $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, array($this, 'injectErrorModel'), 80); - $sharedManager->attach( - 'Zend\Stdlib\DispatchableInterface', - MvcEvent::EVENT_DISPATCH, - array($this, 'selectModel'), - -60 - ); - } - - /** - * Select the correct ModelInterface instance by matching the values of the Accept header to a ModelInterface - * - * @param MvcEvent $event - * @return void - */ - public function selectModel(MvcEvent $event) - { - $result = $event->getResult(); - - // If a view model was already set, or if the application errored with no produced result, no - // view model replacement should happen - if ($result instanceof ModelInterface - || $result instanceof ResponseInterface - || (!isset($result) && $event->getError()) - ) { - return; - } - - $request = $event->getRequest(); - if (!$request instanceof HttpRequest) { - return; - } - - $acceptType = $this->getAcceptType($request); - $model = $this->modelPluginManager->create($acceptType); - - if ($result !== null) { - $model->setVariables($result); - } - - $event->setResult($model); - } - - /** - * When an exception is thrown, this listener proxies to selectModel. If, according to the Accept header, - * we get a Zend\View\Model\ViewModel instance, this means we are in "website" context, so we just return - * to let the other listeners render the template error. - * - * Otherwise (if we have a JsonModel or FeedModel or anything else...) we just set the view model and stop - * propagation so that the response only contains the error message and status code - * - * @param MvcEvent $event - * @return void - */ - public function injectErrorModel(MvcEvent $event) - { - $this->selectModel($event); - - $result = $event->getResult(); - if (!$result instanceof ModelInterface) { - return; - } - - // If it's an exact instance of Zend\View\Model\ViewModel we return as we want to let the other - // listeners to inject layout - if (get_class($result) === 'Zend\View\Model\ViewModel') { - return; - } - - // Otherwise, we stop propagation and set the view model - $event->setViewModel($result); - $event->stopPropagation(); - } - - /** - * Get the Accept type with higher priority in the request - * - * @param HttpRequest $request - * @return string|null - */ - protected function getAcceptType(HttpRequest $request) - { - /** @var $acceptHeader \Zend\Http\Header\Accept */ - $acceptHeader = $request->getHeader('Accept', null); - if ($acceptHeader === null) { - return null; - } - - $acceptValues = $acceptHeader->getPrioritized(); - $acceptValue = reset($acceptValues); - - return $acceptValue->getTypeString(); - } -} diff --git a/src/ZfrRest/Options/ControllerBehavioursOptions.php b/src/ZfrRest/Options/ControllerBehavioursOptions.php deleted file mode 100644 index 94afd4f..0000000 --- a/src/ZfrRest/Options/ControllerBehavioursOptions.php +++ /dev/null @@ -1,83 +0,0 @@ - - */ -class ControllerBehavioursOptions extends AbstractOptions -{ - /** - * If this is set to true, then controller will automatically instantiate the input filter specified in - * resource metadata (if there is one) - from service locator first, or directly instantiate it if not found -, - * and validate data. If data is incorrect, it will return a 400 HTTP error (Bad Request) with the failed - * validation messages in it). - * - * @var bool - */ - protected $autoValidate; - - /** - * If this is set to true, then controller will automatically instantiate the hydrator specified in resource - * metadata (if there is one) - from service locator first, or directly instantiate it if not found - and - * hydrate resource object with previously validated data - * - * @var bool - */ - protected $autoHydrate; - - /** - * @param bool $autoValidate - * @return void - */ - public function setAutoValidate($autoValidate) - { - $this->autoValidate = (bool) $autoValidate; - } - - /** - * @return bool - */ - public function getAutoValidate() - { - return $this->autoValidate; - } - - /** - * @param bool $autoHydrate - * @return void - */ - public function setAutoHydrate($autoHydrate) - { - $this->autoHydrate = (bool) $autoHydrate; - } - - /** - * @return bool - */ - public function getAutoHydrate() - { - return $this->autoHydrate; - } -} diff --git a/src/ZfrRest/Options/Exception/RuntimeException.php b/src/ZfrRest/Options/Exception/RuntimeException.php deleted file mode 100644 index d473ed0..0000000 --- a/src/ZfrRest/Options/Exception/RuntimeException.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -class RuntimeException extends BaseRuntimeException implements ExceptionInterface -{ -} diff --git a/src/ZfrRest/Options/ListenersOptions.php b/src/ZfrRest/Options/ListenersOptions.php deleted file mode 100644 index 1b79393..0000000 --- a/src/ZfrRest/Options/ListenersOptions.php +++ /dev/null @@ -1,133 +0,0 @@ - - */ -class ListenersOptions extends AbstractOptions -{ - /** - * If this listener is registered, then any exceptions that could be thrown in controller is serialized - * to the proper format extracted from Content-Type header - * - * @var bool - */ - protected $registerHttpException; - - /** - * If this listener is registered, it will check if the request contains a header "X-HTTP-Method-Override". - * This header allows to change the HTTP verb. This is useful in some contexts (for instance, some companies' - * proxies only allow GET and POST methods) - * - * @var bool - */ - protected $registerHttpMethodOverride; - - /** - * If this listener is registered, it allows you to return an object (an entity, for instance) from a - * controller's action, and automatically create payload from its content. Please note that if you deactivate - * this listener, you MUST NOT return a resource (entity...) from your controller, but an array or Model - * - * @var bool - */ - protected $registerCreateResourcePayload; - - /** - * If this listener is registered (it is by default), please note that if you deactivate this listener, - * then you need to return a concrete ModelInterface object (ViewModel, JsonModel...) from your actions, - * or manually use the AcceptableViewModelSelector to return the right model according to Content-Type - * - * @var bool - */ - protected $registerSelectModel; - - /** - * @param bool $registerHttpException - * @return void - */ - public function setRegisterHttpException($registerHttpException) - { - $this->registerHttpException = (bool) $registerHttpException; - } - - /** - * @return bool - */ - public function getRegisterHttpException() - { - return $this->registerHttpException; - } - - /** - * @param bool $registerHttpMethodOverride - * @return void - */ - public function setRegisterHttpMethodOverride($registerHttpMethodOverride) - { - $this->registerHttpMethodOverride = (bool) $registerHttpMethodOverride; - } - - /** - * @return bool - */ - public function getRegisterHttpMethodOverride() - { - return $this->registerHttpMethodOverride; - } - - /** - * @param bool $registerCreateResourcePayload - * @return void - */ - public function setRegisterCreateResourcePayload($registerCreateResourcePayload) - { - $this->registerCreateResourcePayload = $registerCreateResourcePayload; - } - - /** - * @return bool - */ - public function getRegisterCreateResourcePayload() - { - return $this->registerCreateResourcePayload; - } - - /** - * @param bool $registerSelectModel - * @return void - */ - public function setRegisterSelectModel($registerSelectModel) - { - $this->registerSelectModel = (bool) $registerSelectModel; - } - - /** - * @return bool - */ - public function getRegisterSelectModel() - { - return $this->registerSelectModel; - } -} diff --git a/src/ZfrRest/Options/ModuleOptions.php b/src/ZfrRest/Options/ModuleOptions.php deleted file mode 100644 index e490480..0000000 --- a/src/ZfrRest/Options/ModuleOptions.php +++ /dev/null @@ -1,180 +0,0 @@ - - */ -class ModuleOptions extends AbstractOptions -{ - /** - * Key of the object manager fetched from the service locator - * - * @var string - */ - protected $objectManager; - - /** - * Listeners options (allow to activate/deactive listeners) - * - * @var ListenersOptions - */ - protected $listenersOptions; - - /** - * Controller behaviours options - * - * @var ControllerBehavioursOptions - */ - protected $controllerBehavioursOptions; - - /** - * Options for resource metadata - * - * @var ResourceMetadataOptions - */ - protected $resourceMetadataOptions; - - /** - * Plugin manager configuration for the content decoders - * - * @var array - */ - protected $decoders = array(); - - /** - * Plugin manager configuration for the view models - * - * @var array - */ - protected $models = array(); - - /** - * @param string $objectManager - * - * @return void - */ - public function setObjectManager($objectManager) - { - $this->objectManager = (string) $objectManager; - } - - /** - * @return string - */ - public function getObjectManager() - { - return $this->objectManager; - } - - /** - * @param array $options - * - * @return void - */ - public function setListeners(array $options) - { - $this->listenersOptions = new ListenersOptions($options); - } - - /** - * @return ListenersOptions - */ - public function getListeners() - { - return $this->listenersOptions; - } - - /** - * @param array $options - * - * @return void - */ - public function setControllerBehaviours(array $options) - { - $this->controllerBehavioursOptions = new ControllerBehavioursOptions($options); - } - - /** - * @return ControllerBehavioursOptions - */ - public function getControllerBehaviours() - { - return $this->controllerBehavioursOptions; - } - - /** - * @param array $options - * - * @return void - */ - public function setResourceMetadata(array $options) - { - $this->resourceMetadataOptions = new ResourceMetadataOptions($options); - } - - /** - * @return ResourceMetadataOptions - */ - public function getResourceMetadata() - { - return $this->resourceMetadataOptions; - } - - /** - * @param array $decoders - * - * @return void - */ - public function setDecoders(array $decoders) - { - $this->decoders = $decoders; - } - - /** - * @return array - */ - public function getDecoders() - { - return $this->decoders; - } - - /** - * @param array $models - * - * @return void - */ - public function setModels(array $models) - { - $this->models = $models; - } - - /** - * @return array - */ - public function getModels() - { - return $this->models; - } -} diff --git a/src/ZfrRest/Options/ResourceMetadataOptions.php b/src/ZfrRest/Options/ResourceMetadataOptions.php deleted file mode 100644 index 5aff50b..0000000 --- a/src/ZfrRest/Options/ResourceMetadataOptions.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ -class ResourceMetadataOptions extends AbstractOptions -{ - /** - * FQCN of a class cache that implements Doctrine\Common\Cache\Cache - * - * @var string - */ - protected $cache; - - /** - * Drivers that are used for creating the metadata factory - * - * @var array - */ - protected $drivers; - - - /** - * Set the class name of a cache that implements Doctrine\Common\Cache\Cache - * - * @param string $cache - * @throws Exception\RuntimeException - * @return void - */ - public function setCache($cache) - { - $this->cache = (string) $cache; - } - - /** - * Get the class name of the cache used for metadata - * - * @return string - */ - public function getCache() - { - return $this->cache; - } - - /** - * Set the list of drivers - * - * @param array $drivers - * @throws Exception\RuntimeException - * @return void - */ - public function setDrivers(array $drivers) - { - $this->drivers = $drivers; - } - - /** - * Get the list of drivers - * - * @return array - */ - public function getDrivers() - { - return $this->drivers; - } -} diff --git a/src/ZfrRest/Paginator/ResourcePaginator.php b/src/ZfrRest/Paginator/ResourcePaginator.php deleted file mode 100644 index 3f0b7e6..0000000 --- a/src/ZfrRest/Paginator/ResourcePaginator.php +++ /dev/null @@ -1,53 +0,0 @@ - - */ -class ResourcePaginator extends Paginator implements ResourcePaginatorInterface -{ - /** - * @var ResourceMetadataInterface - */ - protected $resourceMetadata; - - /** - * {@inheritDoc} - */ - public function __construct(ResourceMetadataInterface $resourceMetadata, $adapter) - { - $this->resourceMetadata = $resourceMetadata; - parent::__construct($adapter); - } - - /** - * {@inheritDoc} - */ - public function getResourceMetadata() - { - return $this->resourceMetadata; - } -} diff --git a/src/ZfrRest/Paginator/ResourcePaginatorInterface.php b/src/ZfrRest/Paginator/ResourcePaginatorInterface.php deleted file mode 100644 index de300e0..0000000 --- a/src/ZfrRest/Paginator/ResourcePaginatorInterface.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ -interface ResourcePaginatorInterface -{ - /** - * Get the resource metadata of the underlying object - * - * @return ResourceMetadataInterface - */ - public function getResourceMetadata(); -} diff --git a/src/ZfrRest/Resource/Exception/InvalidResourceException.php b/src/ZfrRest/Resource/Exception/InvalidResourceException.php deleted file mode 100644 index f82867b..0000000 --- a/src/ZfrRest/Resource/Exception/InvalidResourceException.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ -class InvalidResourceException extends InvalidArgumentException implements ExceptionInterface -{ - /** - * @param mixed $resource - * @param \ZfrRest\Resource\Metadata\ResourceMetadataInterface $metadata - * @return \ZfrRest\Resource\Exception\InvalidResourceException - */ - public static function invalidResourceProvided($resource, ResourceMetadataInterface $metadata) - { - return new self( - sprintf( - 'Provided resource of type "%s" is not an instance nor collection of requested type "%s"', - is_object($resource) ? get_class($resource) : gettype($resource), - $metadata->getClassMetadata()->getName() - ) - ); - } -} diff --git a/src/ZfrRest/Resource/Exception/RuntimeException.php b/src/ZfrRest/Resource/Exception/RuntimeException.php deleted file mode 100644 index c03d6fe..0000000 --- a/src/ZfrRest/Resource/Exception/RuntimeException.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -class RuntimeException extends BaseRuntimeException implements ExceptionInterface -{ -} diff --git a/src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php b/src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php deleted file mode 100644 index 9f7fb38..0000000 --- a/src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ -interface AnnotationInterface -{ - /** - * Get the key of the annotation - * - * @return string - */ - public function getKey(); - - /** - * Get the value of the annotation - * - * @return mixed - */ - public function getValue(); -} diff --git a/src/ZfrRest/Resource/Metadata/Annotation/Association.php b/src/ZfrRest/Resource/Metadata/Annotation/Association.php deleted file mode 100644 index 365dbcb..0000000 --- a/src/ZfrRest/Resource/Metadata/Annotation/Association.php +++ /dev/null @@ -1,47 +0,0 @@ - $this->controller, - 'inputFilter' => $this->inputFilter, - 'hydrator' => $this->hydrator - ); - } -} diff --git a/src/ZfrRest/Resource/Metadata/Annotation/Resource.php b/src/ZfrRest/Resource/Metadata/Annotation/Resource.php deleted file mode 100644 index b7ffa13..0000000 --- a/src/ZfrRest/Resource/Metadata/Annotation/Resource.php +++ /dev/null @@ -1,61 +0,0 @@ - $this->controller, - 'inputFilter' => $this->inputFilter, - 'hydrator' => $this->hydrator - ); - } -} diff --git a/src/ZfrRest/Resource/Metadata/CollectionResourceMetadata.php b/src/ZfrRest/Resource/Metadata/CollectionResourceMetadata.php deleted file mode 100644 index 88592fb..0000000 --- a/src/ZfrRest/Resource/Metadata/CollectionResourceMetadata.php +++ /dev/null @@ -1,69 +0,0 @@ - - */ -class CollectionResourceMetadata extends ClassMetadata implements CollectionResourceMetadataInterface -{ - /** - * @var string - */ - public $controller; - - /** - * @var string - */ - public $inputFilter; - - /** - * @var string - */ - public $hydrator = 'ZfrRest\Stdlib\Hydrator\PaginatorHydrator'; - - /** - * {@inheritDoc} - */ - public function getControllerName() - { - return $this->controller; - } - - /** - * {@inheritDoc} - */ - public function getInputFilterName() - { - return $this->inputFilter; - } - - /** - * {@inheritDoc} - */ - public function getHydratorName() - { - return $this->hydrator; - } -} diff --git a/src/ZfrRest/Resource/Metadata/CollectionResourceMetadataInterface.php b/src/ZfrRest/Resource/Metadata/CollectionResourceMetadataInterface.php deleted file mode 100644 index 93b939b..0000000 --- a/src/ZfrRest/Resource/Metadata/CollectionResourceMetadataInterface.php +++ /dev/null @@ -1,49 +0,0 @@ - - */ -class AnnotationDriver implements DriverInterface, ResourceMetadataDriverInterface -{ - /** - * @var AnnotationReader - */ - protected $annotationReader; - - /** - * @var DoctrineMetadataFactory - */ - protected $doctrineMetadataFactory; - - /** - * @var ResourceMetadataFactory - */ - protected $resourceMetadataFactory; - - /** - * Constructor - * - * @param AnnotationReader $reader - * @param DoctrineMetadataFactory $doctrineMetadataFactory - */ - public function __construct(AnnotationReader $reader, DoctrineMetadataFactory $doctrineMetadataFactory) - { - $this->annotationReader = $reader; - $this->doctrineMetadataFactory = $doctrineMetadataFactory; - } - - /** - * {@inheritDoc} - */ - public function setResourceMetadataFactory(ResourceMetadataFactory $metadataFactory) - { - $this->resourceMetadataFactory = $metadataFactory; - } - - /** - * {@inheritDoc} - */ - public function loadMetadataForClass(ReflectionClass $class) - { - $classMetadata = $this->doctrineMetadataFactory->getMetadataFor($class->getName()); - - $resourceMetadata = new ResourceMetadata($class->getName()); - $resourceMetadata->classMetadata = $classMetadata; - - // Process class level annotations - $classAnnotations = $this->annotationReader->getClassAnnotations($class); - $this->processMetadata($resourceMetadata, $classAnnotations); - - // Then process properties level annotations (for associations) - $classProperties = $class->getProperties(); - foreach ($classProperties as $classProperty) { - $propertyAnnotations = $this->annotationReader->getPropertyAnnotations($classProperty); - - // We need to have at least the Association annotation, so we loop through all the annotations, - // check if it exists, and remove it so that we can process other annotations - foreach ($propertyAnnotations as $key => $propertyAnnotation) { - if ($propertyAnnotation instanceof Annotation\Association) { - unset($propertyAnnotations[$key]); - - $associationName = $classProperty->getName(); - $targetClass = $classMetadata->getAssociationTargetClass($associationName); - - // We first load the metadata for the entity, and we then loop through the annotations defined - // at the association level so that the user can override some properties - $resourceAssociationMetadata = $this - ->resourceMetadataFactory - ->getMetadataForClass($targetClass) - ->getOutsideClassMetadata(); - - $this->processMetadata($resourceAssociationMetadata, $propertyAnnotations); - $resourceMetadata->associations[$associationName] = $resourceAssociationMetadata; - - break; - } - } - } - - return $resourceMetadata; - } - - /** - * @param ResourceMetadata $metadata - * @param AnnotationInterface[] $annotations - */ - private function processMetadata(ResourceMetadata $metadata, array $annotations) - { - foreach ($annotations as $annotation) { - if (!($annotation instanceof AnnotationInterface)) { - continue; - } - - // Resource annotation - if ($annotation instanceof Resource) { - $this->processResourceMetadata($metadata, $annotation); - } - - // Collection annotation - if ($annotation instanceof Collection) { - $this->processCollectionMetadata($metadata, $annotation); - } - } - } - - /** - * @param ResourceMetadata $metadata - * @param Resource $annotation - */ - private function processResourceMetadata(ResourceMetadata $metadata, Resource $annotation) - { - $values = $annotation->getValue(); - - foreach ($values as $key => $value) { - // Ignore null values in order to make cascading work as expected - if (null === $value) { - continue; - } - - $propertyMetadata = new PropertyMetadata($metadata, $key); - $propertyMetadata->setValue($metadata, $value); - - $metadata->addPropertyMetadata($propertyMetadata); - } - } - - /** - * @param ResourceMetadata $metadata - * @param Collection $annotation - */ - private function processCollectionMetadata(ResourceMetadata $metadata, Collection $annotation) - { - $values = $annotation->getValue(); - $collectionMetadata = new CollectionResourceMetadata($metadata->getClassName()); - - foreach ($values as $key => $value) { - $propertyMetadata = new PropertyMetadata($collectionMetadata, $key); - - // If the value is null, then we reuse the value defined at "resource-level" - if (null === $value && isset($metadata->propertyMetadata[$key])) { - $propertyMetadata->setValue( - $collectionMetadata, - $metadata->propertyMetadata[$key]->getValue($metadata) - ); - } else { - $propertyMetadata->setValue($collectionMetadata, $value); - } - - $collectionMetadata->addPropertyMetadata($propertyMetadata); - } - - $metadata->collectionMetadata = $collectionMetadata; - } -} diff --git a/src/ZfrRest/Resource/Metadata/Driver/ResourceMetadataDriverInterface.php b/src/ZfrRest/Resource/Metadata/Driver/ResourceMetadataDriverInterface.php deleted file mode 100644 index 25cd689..0000000 --- a/src/ZfrRest/Resource/Metadata/Driver/ResourceMetadataDriverInterface.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ -interface ResourceMetadataDriverInterface -{ - /** - * @param MetadataFactoryInterface $metadataFactory - * - * @return void - */ - public function setResourceMetadataFactory(MetadataFactoryInterface $metadataFactory); -} diff --git a/src/ZfrRest/Resource/Metadata/ResourceMetadata.php b/src/ZfrRest/Resource/Metadata/ResourceMetadata.php deleted file mode 100644 index 2a16bca..0000000 --- a/src/ZfrRest/Resource/Metadata/ResourceMetadata.php +++ /dev/null @@ -1,142 +0,0 @@ - - */ -class ResourceMetadata extends ClassMetadata implements ResourceMetadataInterface -{ - /** - * @var DoctrineClassMetadata - */ - public $classMetadata; - - /** - * @var string - */ - public $controller; - - /** - * @var string - */ - public $inputFilter; - - /** - * @var string - */ - public $hydrator = 'DoctrineModule\Stdlib\Hydrator\DoctrineObject'; - - /** - * @var ResourceMetadataInterface[]|array - */ - public $associations; - - /** - * @var CollectionResourceMetadataInterface - */ - public $collectionMetadata; - - - /** - * {@inheritDoc} - */ - public function createResource() - { - $args = func_get_args(); - - if (empty($args)) { - return new Resource($this->reflection->newInstance(), $this); - } - - return new Resource($this->reflection->newInstanceArgs($args), $this); - } - - /** - * {@inheritDoc} - */ - public function getClassName() - { - return $this->name; - } - - /** - * {@inheritDoc} - */ - public function getClassMetadata() - { - return $this->classMetadata; - } - - /** - * {@inheritDoc} - */ - public function getControllerName() - { - return $this->controller; - } - - /** - * {@inheritDoc} - */ - public function getInputFilterName() - { - return $this->inputFilter; - } - - /** - * {@inheritDoc} - */ - public function getHydratorName() - { - return $this->hydrator; - } - - /** - * {@inheritDoc} - */ - public function getAssociationMetadata($association) - { - return $this->associations[$association]; - } - - /** - * {@inheritDoc} - */ - public function hasAssociation($association) - { - return isset($this->associations[$association]); - } - - /** - * {@inheritDoc} - */ - public function getCollectionMetadata() - { - return $this->collectionMetadata; - } -} diff --git a/src/ZfrRest/Resource/Metadata/ResourceMetadataInterface.php b/src/ZfrRest/Resource/Metadata/ResourceMetadataInterface.php deleted file mode 100644 index ff47caa..0000000 --- a/src/ZfrRest/Resource/Metadata/ResourceMetadataInterface.php +++ /dev/null @@ -1,92 +0,0 @@ - - */ -interface ResourceMetadataInterface -{ - /** - * Create a new resource whose type is equals to class name - * - * @return \ZfrRest\Resource\ResourceInterface - */ - public function createResource(); - - /** - * Get the class name (this is a shortcut of retrieving it using class metadata) - * - * @return string - */ - public function getClassName(); - - /** - * Get the class metadata (provides information about Doctrine mapping) - * - * @return \Doctrine\Common\Persistence\Mapping\ClassMetadata - */ - public function getClassMetadata(); - - /** - * Get the controller's FQCN - * - * @return string|null - */ - public function getControllerName(); - - /** - * Get the input filter's FQCN to be used for this resource - * - * @return string|null - */ - public function getInputFilterName(); - - /** - * Get the hydrator's FQCN to be used for this resource - * - * @return string|null - */ - public function getHydratorName(); - - /** - * Get the metadata to a given association - * - * @param string $association - * @return ResourceMetadataInterface - */ - public function getAssociationMetadata($association); - - /** - * Check if the resource metadata can traverse the given association - * - * @param string $association - * @return bool - */ - public function hasAssociation($association); - - /** - * Get metadata to use in case the resource is a collection of item - * - * @return CollectionResourceMetadataInterface - */ - public function getCollectionMetadata(); -} diff --git a/src/ZfrRest/Resource/Resource.php b/src/ZfrRest/Resource/Resource.php deleted file mode 100644 index 17c68c8..0000000 --- a/src/ZfrRest/Resource/Resource.php +++ /dev/null @@ -1,82 +0,0 @@ - - */ -class Resource implements ResourceInterface -{ - /** - * @var mixed - */ - protected $data; - - /** - * @var ResourceMetadataInterface - */ - protected $metadata; - - - /** - * @param mixed $data - * @param ResourceMetadataInterface $metadata - * @throws Exception\InvalidResourceException - */ - public function __construct($data, ResourceMetadataInterface $metadata) - { - $this->data = $data; - $this->metadata = $metadata; - - $reflectionClass = $metadata->getClassMetadata()->getReflectionClass(); - - if (!$this->isCollection() && ! $reflectionClass->isInstance($data)) { - throw InvalidResourceException::invalidResourceProvided($data, $metadata); - } - } - - /** - * {@inheritDoc} - */ - public function getMetadata() - { - return $this->metadata; - } - - /** - * {@inheritDoc} - */ - public function getData() - { - return $this->data; - } - - /** - * {@inheritDoc} - */ - public function isCollection() - { - return ($this->data instanceof Selectable || $this->data instanceof Traversable || is_array($this->data)); - } -} diff --git a/src/ZfrRest/Resource/ResourceInterface.php b/src/ZfrRest/Resource/ResourceInterface.php deleted file mode 100644 index 4ae60df..0000000 --- a/src/ZfrRest/Resource/ResourceInterface.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ -interface ResourceInterface -{ - /** - * Get the resource metadata - * - * @return \ZfrRest\Resource\Metadata\ResourceMetadataInterface - */ - public function getMetadata(); - - /** - * Retrieves the resource data, which can be either an instance of the provided metadata name or - * a collection of these instances - * - * @return mixed - */ - public function getData(); - - /** - * Check if the resource is a collection of instances of the resource type - * - * @return bool - */ - public function isCollection(); -} diff --git a/src/ZfrRest/Serializer/DecoderPluginManager.php b/src/ZfrRest/Serializer/DecoderPluginManager.php deleted file mode 100644 index 7641980..0000000 --- a/src/ZfrRest/Serializer/DecoderPluginManager.php +++ /dev/null @@ -1,68 +0,0 @@ - - */ -class DecoderPluginManager extends AbstractPluginManager -{ - /** - * @var array - */ - protected $invokableClasses = array( - 'application/xml' => 'Symfony\Component\Serializer\Encoder\XmlEncoder' - ); - - /** - * Factories are used for JsonDecoder because, by default, Symfony\Serializer component return - * data as stdClass, while we prefer the data to be returned as plain associative arrays - * - * @var array - */ - protected $factories = array( - 'application/json' => 'ZfrRest\Factory\JsonDecoderFactory', - 'application/javascript' => 'ZfrRest\Factory\JsonDecoderFactory', - ); - - /** - * {@inheritDoc} - */ - public function validatePlugin($plugin) - { - if (! $plugin instanceof DecoderInterface) { - throw RuntimeException::invalidDecoderPlugin($plugin); - } - } - - /** - * {@inheritDoc} - */ - protected function canonicalizeName($name) - { - return $name; - } -} diff --git a/src/ZfrRest/Serializer/Exception/RuntimeException.php b/src/ZfrRest/Serializer/Exception/RuntimeException.php deleted file mode 100644 index 5d3ffdb..0000000 --- a/src/ZfrRest/Serializer/Exception/RuntimeException.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ -class RuntimeException extends BaseRuntimeException implements ExceptionInterface -{ - /** - * @param mixed $plugin - * - * @return self - */ - public static function invalidDecoderPlugin($plugin) - { - return new self( - sprintf( - 'Plugin of type %s is invalid; must implement Symfony\Component\Serializer\Encoder\DecoderInterface', - (is_object($plugin) ? get_class($plugin) : gettype($plugin)) - ) - ); - } -} diff --git a/src/ZfrRest/Stdlib/Hydrator/PaginatorHydrator.php b/src/ZfrRest/Stdlib/Hydrator/PaginatorHydrator.php deleted file mode 100644 index e9a90bc..0000000 --- a/src/ZfrRest/Stdlib/Hydrator/PaginatorHydrator.php +++ /dev/null @@ -1,76 +0,0 @@ - - */ -class PaginatorHydrator extends AbstractHydrator -{ - /** - * @var HydratorPluginManager - */ - protected $hydratorManager; - - /** - * @param HydratorPluginManager $hydratorManager - */ - public function __construct(HydratorPluginManager $hydratorManager) - { - $this->hydratorManager = $hydratorManager; - } - - /** - * {@inheritDoc} - */ - public function extract($object) - { - if (!$object instanceof ResourcePaginator) { - return array(); - } - - $payload = array( - 'current_page' => $object->getCurrentPageNumber(), - 'count_per_page' => $object->getItemCountPerPage() - ); - - $resourceHydrator = $object->getResourceMetadata()->getHydratorName(); - $resourceHydrator = $this->hydratorManager->get($resourceHydrator); - - foreach ($object as $item) { - $payload['items'][] = $resourceHydrator->extract($item); - } - - return $payload; - } - - /** - * {@inheritDoc} - */ - public function hydrate(array $data, $object) - { - // TODO: Implement hydrate() method. - } -} diff --git a/src/ZfrRest/Version.php b/src/ZfrRest/Version.php deleted file mode 100644 index 9441260..0000000 --- a/src/ZfrRest/Version.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -class RuntimeException extends BaseRuntimeException implements ExceptionInterface -{ - /** - * @param mixed $plugin - * - * @return self - */ - public static function invalidViewModelPlugin($plugin) - { - return new self( - sprintf( - 'Plugin of type %s is invalid; must implement Zend\View\ViewModel', - (is_object($plugin) ? get_class($plugin) : gettype($plugin)) - ) - ); - } -} diff --git a/src/ZfrRest/View/Model/ModelPluginManager.php b/src/ZfrRest/View/Model/ModelPluginManager.php deleted file mode 100644 index 8be73ef..0000000 --- a/src/ZfrRest/View/Model/ModelPluginManager.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ -class ModelPluginManager extends AbstractPluginManager -{ - /** - * @var array - */ - protected $invokableClasses = array( - '*/*' => 'Zend\View\Model\ViewModel', - 'text/html' => 'Zend\View\Model\ViewModel', - 'application/xhtml+xml' => 'Zend\View\Model\ViewModel', - 'application/json' => 'Zend\View\Model\JsonModel', - 'application/javascript' => 'Zend\View\Model\JsonModel', - ); - - - /** - * {@inheritDoc} - */ - public function validatePlugin($plugin) - { - if (! $plugin instanceof ModelInterface) { - throw RuntimeException::invalidViewModelPlugin($plugin); - } - } - - /** - * {@inheritDoc} - */ - protected function canonicalizeName($name) - { - return $name; - } -} diff --git a/tests/ZfrRestTest/Asset/Annotation/AbstractPage.php b/tests/ZfrRestTest/Asset/Annotation/AbstractPage.php deleted file mode 100644 index 7682499..0000000 --- a/tests/ZfrRestTest/Asset/Annotation/AbstractPage.php +++ /dev/null @@ -1,44 +0,0 @@ -id; - } -} diff --git a/tests/ZfrRestTest/Asset/Annotation/Page.php b/tests/ZfrRestTest/Asset/Annotation/Page.php deleted file mode 100644 index 0c6754d..0000000 --- a/tests/ZfrRestTest/Asset/Annotation/Page.php +++ /dev/null @@ -1,54 +0,0 @@ -name; - } - - /** - * @param string $name - */ - public function setName($name) - { - $this->name = (string) $name; - } -} diff --git a/tests/ZfrRestTest/Asset/Annotation/Tweet.php b/tests/ZfrRestTest/Asset/Annotation/Tweet.php deleted file mode 100644 index 68a2cbe..0000000 --- a/tests/ZfrRestTest/Asset/Annotation/Tweet.php +++ /dev/null @@ -1,108 +0,0 @@ -id = $id; - } - - /** - * @return int - */ - public function getId() - { - return $this->id; - } - - /** - * @param User|null $user - * @return void - */ - public function setUser(User $user = null) - { - $this->user = $user; - } - - /** - * @return User|null - */ - public function getUser() - { - return $this->user; - } - - /** - * @param string $content - * @return void - */ - public function setContent($content) - { - $this->content = $content; - } - - /** - * @return string - */ - public function getContent() - { - return $this->content; - } -} diff --git a/tests/ZfrRestTest/Asset/Annotation/User.php b/tests/ZfrRestTest/Asset/Annotation/User.php deleted file mode 100644 index 9ca7416..0000000 --- a/tests/ZfrRestTest/Asset/Annotation/User.php +++ /dev/null @@ -1,111 +0,0 @@ -tweets = new ArrayCollection(); - } - - /** - * @param int $id - * @return void - */ - public function setId($id) - { - $this->id = $id; - } - - /** - * @return int - */ - public function getId() - { - return $this->id; - } - - /** - * @param string $name - * @return void - */ - public function setName($name) - { - $this->name = $name; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * @return Array|ArrayCollection - */ - public function getTweets() - { - return $this->tweets; - } -} diff --git a/tests/ZfrRestTest/Asset/Controller/UserController.php b/tests/ZfrRestTest/Asset/Controller/UserController.php deleted file mode 100644 index b2369fd..0000000 --- a/tests/ZfrRestTest/Asset/Controller/UserController.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ -class UserController extends AbstractRestfulController -{ - public function post($data, ResourceMetadataInterface $metadata, ResourceInterface $resource) - { - return new ViewModel(array('data' => $data, 'metadata' => $metadata, 'resource' => $resource)); - } -} diff --git a/tests/ZfrRestTest/Asset/Hydrator/UserHydrator.php b/tests/ZfrRestTest/Asset/Hydrator/UserHydrator.php deleted file mode 100644 index cb6a729..0000000 --- a/tests/ZfrRestTest/Asset/Hydrator/UserHydrator.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ -class UserHydrator implements HydratorInterface -{ - /** - * {@inheritDoc} - */ - public function extract($object) - { - if (! $object instanceof User) { - return array(); - } - - return array('name' => $object->getName()); - } - - /** - * {@inheritDoc} - */ - public function hydrate(array $data, $object) - { - if (! $object instanceof User) { - return $object; - } - - $object->setName($data['name']); - - return $object; - } -} diff --git a/tests/ZfrRestTest/Asset/InputFilter/UserInputFilter.php b/tests/ZfrRestTest/Asset/InputFilter/UserInputFilter.php deleted file mode 100644 index 4b536a2..0000000 --- a/tests/ZfrRestTest/Asset/InputFilter/UserInputFilter.php +++ /dev/null @@ -1,38 +0,0 @@ - - */ -class UserInputFilter extends BaseInputFilter -{ - /** - * Constructor - */ - public function __construct() - { - $this->add(new Input('name')); - } -} diff --git a/tests/ZfrRestTest/Asset/Request/Request.php b/tests/ZfrRestTest/Asset/Request/Request.php deleted file mode 100644 index f8dccf2..0000000 --- a/tests/ZfrRestTest/Asset/Request/Request.php +++ /dev/null @@ -1,36 +0,0 @@ -baseUrl = $baseUrl; - return $this; - } - - /** - * @return string - */ - public function getBaseUrl() - { - return $this->baseUrl; - } -} diff --git a/tests/ZfrRestTest/Http/Exception/Client/UnauthorizedExceptionTest.php b/tests/ZfrRestTest/Http/Exception/Client/UnauthorizedExceptionTest.php deleted file mode 100644 index ade7ea9..0000000 --- a/tests/ZfrRestTest/Http/Exception/Client/UnauthorizedExceptionTest.php +++ /dev/null @@ -1,32 +0,0 @@ -assertEquals('Basic', $exception->getChallenge()); - } -} diff --git a/tests/ZfrRestTest/Http/Exception/ClientExceptionTest.php b/tests/ZfrRestTest/Http/Exception/ClientExceptionTest.php deleted file mode 100644 index 4f1df1e..0000000 --- a/tests/ZfrRestTest/Http/Exception/ClientExceptionTest.php +++ /dev/null @@ -1,52 +0,0 @@ -setExpectedException( - 'InvalidArgumentException', - 'Status code for client errors must be between 400 and 499, 500 given' - ); - - new Exception\ClientException(500); - } - - public function testThrowExceptionIfStatusCodeIsBelowRange() - { - $this->setExpectedException( - 'InvalidArgumentException', - 'Status code for client errors must be between 400 and 499, 399 given' - ); - - new Exception\ClientException(399); - } - - public function testAlwaysContainDefaultMessage() - { - $exception = new Exception\ClientException(401); - - $this->assertContains('A client error occurred', $exception->getMessage()); - } -} diff --git a/tests/ZfrRestTest/Http/Exception/ServerExceptionTest.php b/tests/ZfrRestTest/Http/Exception/ServerExceptionTest.php deleted file mode 100644 index 10a764a..0000000 --- a/tests/ZfrRestTest/Http/Exception/ServerExceptionTest.php +++ /dev/null @@ -1,51 +0,0 @@ -setExpectedException( - 'InvalidArgumentException', - 'Status code for server errors must be between 500 and 599, 600 given' - ); - - new Exception\ServerException(600); - } - - public function testThrowExceptionIfStatusCodeIsBelowRange() - { - $this->setExpectedException( - 'InvalidArgumentException', - 'Status code for server errors must be between 500 and 599, 499 given' - ); - - new Exception\ServerException(499); - } - - public function testAlwaysContainDefaultMessage() - { - $exception = new Exception\ServerException(501); - $this->assertContains('A server error occurred', $exception->getMessage()); - } -} diff --git a/tests/ZfrRestTest/Mvc/Asset/DummyController.php b/tests/ZfrRestTest/Mvc/Asset/DummyController.php deleted file mode 100644 index c88028a..0000000 --- a/tests/ZfrRestTest/Mvc/Asset/DummyController.php +++ /dev/null @@ -1,48 +0,0 @@ - $resource)); - } - - /** - * @param mixed $data - * @param ResourceMetadataInterface $metadata - * @param ResourceInterface $resource - * - * @return ViewModel - */ - public function post($data, ResourceMetadataInterface $metadata, ResourceInterface $resource) - { - return new ViewModel(array('resource' => $resource)); - } -} diff --git a/tests/ZfrRestTest/Mvc/Controller/AbstractRestfulControllerFunctionalTest.php b/tests/ZfrRestTest/Mvc/Controller/AbstractRestfulControllerFunctionalTest.php deleted file mode 100644 index dc98174..0000000 --- a/tests/ZfrRestTest/Mvc/Controller/AbstractRestfulControllerFunctionalTest.php +++ /dev/null @@ -1,112 +0,0 @@ - - * - * @covers \ZfrRest\Mvc\Controller\AbstractRestfulController - * @group Functional - */ -class AbstractRestfulControllerFunctionalTest extends TestCase -{ - /** - * @var \Zend\ServiceManager\ServiceManager - */ - protected $serviceManager; - - /** - * @var \Zend\ServiceManager\AbstractPluginManager - */ - protected $controllerLoader; - /** - * @var \Zend\Mvc\Router\Http\TreeRouteStack - */ - protected $router; - - /** - * {@inheritDoc} - */ - public function setUp() - { - $this->serviceManager = ServiceManagerFactory::getServiceManager(); - $config = $this->serviceManager->get('Config'); - /* @var $entityManager \Doctrine\ORM\EntityManager */ - $entityManager = $this->serviceManager->get('Doctrine\ORM\EntityManager'); - - $config['router']['routes']['user'] = array( - 'type' => 'ResourceGraphRoute', - 'options' => array( - 'route' => '/user/', - 'resource' => 'ZfrRestTest\Asset\Repository\UserRepository', - ), - ); - - $this->serviceManager->setAllowOverride(true); - $this->serviceManager->setService('Config', $config); - $this->serviceManager->setService( - 'ZfrRestTest\\Asset\\Repository\\UserRepository', - $entityManager->getRepository('ZfrRestTest\Asset\Annotation\User') - ); - - $this->router = $this->serviceManager->get('HttpRouter'); - $this->controllerLoader = $this->serviceManager->get('ControllerLoader'); - } - - public function testHandlesRequestBodyWithCharsetContentType() - { - $request = new HttpRequest(); - - $request->getHeaders()->addHeaderLine('Content-Type: application/json; charset=UTF-8'); - $request->setMethod(HttpRequest::METHOD_POST); - $request->setContent('{"name":"Zoidberg"}'); - $request->setUri('/user/'); - - $match = $this->router->match($request); - /* @var $controller \ZfrRestTest\Asset\Controller\UserController */ - $controller = $this->controllerLoader->get('ZfrRestTest\Asset\Controller\UserController'); - - $this->assertInstanceOf('Zend\Mvc\Router\RouteMatch', $match); - $this->assertInstanceOf('ZfrRestTest\Asset\Controller\UserController', $controller); - - $controller->getEvent()->setRouteMatch($match); - - - /* @var $result \Zend\View\Model\ViewModel */ - $result = $controller->dispatch($request); - - $this->assertInstanceOf('Zend\View\Model\ViewModel', $result); - - /* @var $data \ZfrRestTest\Asset\Annotation\User */ - $data = $result->getVariable('data'); - $metadata = $result->getVariable('metadata'); - $resource = $result->getVariable('resource'); - - $this->assertInstanceOf('ZfrRestTest\Asset\Annotation\User', $data); - $this->assertSame('Zoidberg', $data->getName()); - $this->assertInstanceOf('ZfrRest\Resource\Metadata\ResourceMetadataInterface', $metadata); - $this->assertInstanceOf('ZfrRest\Resource\ResourceInterface', $resource); - } -} diff --git a/tests/ZfrRestTest/Mvc/Controller/AbstractRestfulControllerTest.php b/tests/ZfrRestTest/Mvc/Controller/AbstractRestfulControllerTest.php deleted file mode 100644 index 2f706f1..0000000 --- a/tests/ZfrRestTest/Mvc/Controller/AbstractRestfulControllerTest.php +++ /dev/null @@ -1,106 +0,0 @@ -event = new MvcEvent(); - - $this->controller = new DummyController(); - $this->controller->setEvent($this->event); - $this->controller->setEventManager(new EventManager()); - } - - public function testOnlyHandleHttpRequests() - { - $this->setExpectedException('Zend\Mvc\Exception\InvalidArgumentException'); - $this->controller->dispatch(new ConsoleRequest()); - } - - /*public function testThrowsExceptionForUnhandledMethods() - { - $this->setExpectedException('ZfrRest\Http\Exception\Client\MethodNotAllowedException'); - - $request = new HttpRequest(); - $request->setMethod('post'); - - $this->controller->dispatch($request); - } - - public function testThrowsExceptionForNotFoundResources() - { - $this->setExpectedException('ZfrRest\Http\Exception\Client\NotFoundException'); - - $request = new HttpRequest(); - $request->setMethod('get'); - - $routeMatch = $this->getMock('Zend\Mvc\Router\RouteMatch', array(), array(), '', false); - $routeMatch->expects($this->once()) - ->method('getParam') - ->with('resource') - ->will($this->returnValue(null)); - - $this->event->setRouteMatch($routeMatch); - - $this->controller->dispatch($request); - } - - public function testCorrectlyInjectDispatchResponseInMvcEvent() - { - $request = new HttpRequest(); - $request->setMethod('get'); - - $resource = new \stdClass(); - - $routeMatch = $this->getMock('Zend\Mvc\Router\RouteMatch', array(), array(), '', false); - $routeMatch->expects($this->once()) - ->method('getParam') - ->with('resource') - ->will($this->returnValue($resource)); - - $this->event->setRouteMatch($routeMatch); - - $this->controller->dispatch($request); - - $viewModel = $this->event->getResult(); - - $this->assertInstanceOf('Zend\View\Model\ViewModel', $viewModel); - $this->assertSame($resource, $viewModel->getVariable('resource')); - }*/ -} diff --git a/tests/ZfrRestTest/Mvc/HttpExceptionListenerTest.php b/tests/ZfrRestTest/Mvc/HttpExceptionListenerTest.php deleted file mode 100644 index a607cd8..0000000 --- a/tests/ZfrRestTest/Mvc/HttpExceptionListenerTest.php +++ /dev/null @@ -1,96 +0,0 @@ -httpExceptionListener = new HttpExceptionListener(); - - // Init the MvcEvent object - $this->response = new HttpResponse(); - - $this->event = new MvcEvent(); - $this->event->setResponse($this->response); - } - - public function testCorrectlySetStatusCodeIfHttpExceptionIsRaised() - { - $error = new Exception\ClientException(404); - $this->event->setParam('exception', $error); - - $this->httpExceptionListener->onDispatchError($this->event); - - $this->assertEquals(404, $this->response->getStatusCode()); - $this->assertEquals('A client error occurred', $this->response->getReasonPhrase()); - } - - public function testCorrectlySetReasonPhraseIfHttpExceptionIsRaised() - { - $error = new Exception\ServerException(500); - $this->event->setParam('exception', $error); - - $this->httpExceptionListener->onDispatchError($this->event); - - $this->assertEquals(500, $this->response->getStatusCode()); - $this->assertEquals('A server error occurred', $this->response->getReasonPhrase()); - } - - public function testAssertWWWAuthenticateHeaderIsAutomaticallyAddedWhenUnauthorizedExceptionIsRaised() - { - $error = new Exception\Client\UnauthorizedException(); - $this->event->setParam('exception', $error); - - $this->httpExceptionListener->onDispatchError($this->event); - - $this->assertEquals(401, $this->response->getStatusCode()); - $this->assertEquals( - 'You are not authorized to access to the requested resource', - $this->response->getReasonPhrase() - ); - $this->assertTrue($this->response->getHeaders()->has('WWWAuthenticate')); - } -} diff --git a/tests/ZfrRestTest/Mvc/HttpMethodOverrideListenerTest.php b/tests/ZfrRestTest/Mvc/HttpMethodOverrideListenerTest.php deleted file mode 100644 index 29c8c0e..0000000 --- a/tests/ZfrRestTest/Mvc/HttpMethodOverrideListenerTest.php +++ /dev/null @@ -1,64 +0,0 @@ -httpMethodOverrideListener = new HttpMethodOverrideListener(); - } - - public function testChangeMethodIfHttpMethodOverrideHeaderIsPresent() - { - $event = new MvcEvent(); - $request = new HttpRequest(); - $request->getHeaders()->addHeaderLine('X-HTTP-Method-Override', 'DELETE'); - - $event->setRequest($request); - - $this->httpMethodOverrideListener->overrideHttpMethod($event); - - $this->assertEquals('DELETE', $event->getRequest()->getMethod()); - } - - public function testThrowExceptionIfInvalidMethodIsSetInHeader() - { - $this->setExpectedException('Zend\Http\Exception\InvalidArgumentException', 'Invalid HTTP method passed'); - - $event = new MvcEvent(); - $request = new HttpRequest(); - $request->getHeaders()->addHeaderLine('X-HTTP-Method-Override', 'FOO'); - - $event->setRequest($request); - - $this->httpMethodOverrideListener->overrideHttpMethod($event); - } -} diff --git a/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteFunctionalTest.php b/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteFunctionalTest.php deleted file mode 100644 index 532ff53..0000000 --- a/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteFunctionalTest.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * @covers \ZfrRest\Mvc\Router\Http\ResourceGraphRoute - * @group Functional - */ -class ResourceGraphRouteFunctionalTest extends TestCase -{ - /** - * Verifies that the resource graph route retrieves the correct metadata - * for an inheritance of classes - */ - public function testRetrievesChildClassMetadata() - { - $serviceManager = ServiceManagerFactory::getServiceManager(); - $config = $serviceManager->get('Config'); - /* @var $entityManager \Doctrine\ORM\EntityManager */ - $entityManager = $serviceManager->get('Doctrine\\ORM\\EntityManager'); - $request = new Request(); - - $config['router']['routes']['foo_route'] = array( - 'type' => 'ResourceGraphRoute', - 'options' => array( - 'route' => '/foo/bar/', - 'resource' => 'Foo\\Repository', - ), - ); - - $request->setUri('/foo/bar/'); - $serviceManager->setAllowOverride(true); - $serviceManager->setService('Config', $config); - $serviceManager->setService( - 'Foo\\Repository', - $entityManager->getRepository('ZfrRestTest\Asset\Annotation\Page') - ); - - /* @var $router \Zend\Mvc\Router\Http\TreeRouteStack */ - $router = $serviceManager->get('HttpRouter'); - - - $match = $router->match($request); - - $this->assertInstanceOf('Zend\\Mvc\\Router\\RouteMatch', $match); - - /* @var $resource \ZfrRest\Resource\ResourceInterface */ - $resource = $match->getParam('resource'); - - $this->assertInstanceOf('ZfrRest\\Resource\\ResourceInterface', $resource); - - $this->assertSame('ZfrRestTest\Asset\Annotation\Page', $resource->getMetadata()->getClassName()); - } -} diff --git a/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php b/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php deleted file mode 100644 index cf3529f..0000000 --- a/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php +++ /dev/null @@ -1,170 +0,0 @@ - - * - * @covers \ZfrRest\Mvc\Router\Http\ResourceGraphRoute - */ -class ResourceGraphRouteTest extends TestCase -{ - /** - * @covers \ZfrRest\Mvc\Router\Http\ResourceGraphRoute::match - */ - public function testMatchesConfiguredTrailingSlash() - { - $metadataFactory = new MetadataFactory($this->getMock('Metadata\\Driver\\DriverInterface')); - $resource = $this->getMock('ZfrRest\\Resource\\ResourceInterface'); - $request = new Request(); - $routeMatch = $this->getMock('Zend\\Mvc\\Router\\RouteMatch', array(), array(), '', false); - $route = $this->getMock( - 'ZfrRest\Mvc\Router\Http\ResourceGraphRoute', - array('buildRouteMatch'), - array($metadataFactory, $resource, '/foo/bar/') - ); - - $route->expects($this->any())->method('buildRouteMatch')->will($this->returnValue($routeMatch)); - - $request->setUri('foo/bar'); - $this->assertNull($route->match($request)); - - $request->setUri('/foo/bar'); - $this->assertNull($route->match($request)); - - $request->setUri('/foo/bar/'); - $this->assertSame($routeMatch, $route->match($request)); - - $this->markTestIncomplete('Should not mock the resource graph route itself'); - } - - /** - * @covers \ZfrRest\Mvc\Router\Http\ResourceGraphRoute::match - */ - public function testMatchesOnMissingConfiguredTrailingSlash() - { - $metadataFactory = new MetadataFactory($this->getMock('Metadata\\Driver\\DriverInterface')); - $resource = $this->getMock('ZfrRest\\Resource\\ResourceInterface'); - $request = new Request(); - $routeMatch = $this->getMock('Zend\\Mvc\\Router\\RouteMatch', array(), array(), '', false); - $route = $this->getMock( - 'ZfrRest\Mvc\Router\Http\ResourceGraphRoute', - array('buildRouteMatch'), - array($metadataFactory, $resource, '/foo/bar') - ); - - $route->expects($this->any())->method('buildRouteMatch')->will($this->returnValue($routeMatch)); - - $request->setUri('foo/bar'); - $this->assertNull($route->match($request)); - - $request->setUri('/foo/bar'); - $this->assertSame($routeMatch, $route->match($request)); - - $request->setUri('/foo/bar/'); - $this->assertSame($routeMatch, $route->match($request)); - - $this->markTestIncomplete('Should not mock the resource graph route itself'); - } - - /** - * @covers \ZfrRest\Mvc\Router\Http\ResourceGraphRoute::match - */ - public function testDoesNotMatchCollectionItemsWithoutSlashSeparator() - { - $metadataFactory = new MetadataFactory($this->getMock('Metadata\\Driver\\DriverInterface')); - $resource = $this->getMock('ZfrRest\\Resource\\ResourceInterface'); - $request = new Request(); - $routeMatch = $this->getMock('Zend\\Mvc\\Router\\RouteMatch', array(), array(), '', false); - $route = $this->getMock( - 'ZfrRest\Mvc\Router\Http\ResourceGraphRoute', - array('buildRouteMatch', 'matchIdentifier'), - array($metadataFactory, $resource, '/foo/bar') - ); - - $resource->expects($this->any())->method('isCollection')->will($this->returnValue(true)); - $route->expects($this->any())->method('buildRouteMatch')->will($this->returnValue($routeMatch)); - $route - ->expects($this->any()) - ->method('matchIdentifier') - ->with($resource, '/123') - ->will($this->returnValue($routeMatch)); - - $request->setUri('/foo/bar'); - $this->assertSame($routeMatch, $route->match($request)); - - $request->setUri('/foo/bar/'); - $this->assertSame($routeMatch, $route->match($request)); - - $request->setUri('/foo/bar/123'); - $this->assertSame($routeMatch, $route->match($request)); - - $request->setUri('/foo/barbaz'); - $this->assertNull($route->match($request)); - - $this->markTestIncomplete('Should not mock the resource graph route itself'); - } - - /** - * Test if route can match when there is a baseUrl for the application - * @covers \ZfrRest\Mvc\Router\Http\ResourceGraphRoute::match - */ - public function testMatchWithBaseUrl() - { - $metadataFactory = new MetadataFactory($this->getMock('Metadata\\Driver\\DriverInterface')); - $resource = $this->getMock('ZfrRest\\Resource\\ResourceInterface'); - $routeMatch = $this->getMock('Zend\\Mvc\\Router\\RouteMatch', array(), array(), '', false); - $route = $this->getMock( - 'ZfrRest\Mvc\Router\Http\ResourceGraphRoute', - array('buildRouteMatch', 'matchIdentifier'), - array($metadataFactory, $resource, '/foo/bar') - ); - - $resource->expects($this->any())->method('isCollection')->will($this->returnValue(true)); - $route->expects($this->any())->method('buildRouteMatch')->will($this->returnValue($routeMatch)); - $route - ->expects($this->any()) - ->method('matchIdentifier') - ->with($resource, '/123') - ->will($this->returnValue($routeMatch)); - - $request = new \ZfrRestTest\Asset\Request\Request(); - $request->setBaseUrl('/base/'); - - $request->setUri('/foo/bar'); - $this->assertNull($route->match($request)); - $request->setUri('/foo/bar/123'); - $this->assertNull($route->match($request)); - - $request->setUri('/base/foo/bar'); - $this->assertSame($routeMatch, $route->match($request)); - $request->setUri('/base/foo/bar/123'); - $this->assertSame($routeMatch, $route->match($request)); - - $this->markTestIncomplete('Should not mock the resource graph route itself'); - } -} diff --git a/tests/ZfrRestTest/Mvc/View/Http/CreateResourcePayloadListenerTest.php b/tests/ZfrRestTest/Mvc/View/Http/CreateResourcePayloadListenerTest.php deleted file mode 100644 index b129eac..0000000 --- a/tests/ZfrRestTest/Mvc/View/Http/CreateResourcePayloadListenerTest.php +++ /dev/null @@ -1,113 +0,0 @@ -createResourcePayloadListener = new CreateResourcePayloadListener(new HydratorPluginManager()); - - // Init the MvcEvent object - $request = new HttpRequest(); - - $this->event = new MvcEvent(); - $this->event->setRequest($request); - } - - public function testCanCreatePayload() - { - $data = new \stdClass(); - $data->foo = 'bar'; - - $classMetadata = $this->getMock('Doctrine\\Common\\Persistence\\Mapping\\ClassMetadata'); - $resourceMetadata = new ResourceMetadata('stdClass'); - $reflectionClass = new ReflectionClass($data); - - $resourceMetadata->hydrator = 'Zend\Stdlib\Hydrator\ObjectProperty'; - $resourceMetadata->classMetadata = $classMetadata; - - - $classMetadata->expects($this->any())->method('getReflectionClass')->will($this->returnValue($reflectionClass)); - - $resource = new Resource($data, $resourceMetadata); - $routeMatch = new RouteMatch(array('resource' => $resource)); - - $this->event->setRouteMatch($routeMatch); - $this->event->setResult($data); - - $this->createResourcePayloadListener->createPayload($this->event); - - $this->assertEquals(array('foo' => 'bar'), $this->event->getResult()); - } - - public function testWillSkipOnMissingRouteMatch() - { - $data = new \stdClass(); - - $this->event->setResult($data); - - $this->createResourcePayloadListener->createPayload($this->event); - - $this->assertSame($data, $this->event->getResult()); - } - - public function testWillSkipOnMissingResource() - { - $data = new \stdClass(); - $routeMatch = new RouteMatch(array()); - - $this->event->setRouteMatch($routeMatch); - $this->event->setResult($data); - - $this->createResourcePayloadListener->createPayload($this->event); - - $this->assertSame($data, $this->event->getResult()); - } -} diff --git a/tests/ZfrRestTest/Mvc/View/Http/SelectModelListenerTest.php b/tests/ZfrRestTest/Mvc/View/Http/SelectModelListenerTest.php deleted file mode 100644 index 249efdd..0000000 --- a/tests/ZfrRestTest/Mvc/View/Http/SelectModelListenerTest.php +++ /dev/null @@ -1,148 +0,0 @@ -selectModelListener = new SelectModelListener(new ModelPluginManager()); - - // Init the MvcEvent object - $request = new HttpRequest(); - - $this->event = new MvcEvent(); - $this->event->setRequest($request); - } - - public function typeProvider() - { - return array( - array('text/html', 'Zend\View\Model\ViewModel'), - array('application/xhtml+xml', 'Zend\View\Model\ViewModel'), - array('application/json', 'Zend\View\Model\JsonModel'), - array('application/javascript', 'Zend\View\Model\JsonModel'), - ); - } - - /** - * @dataProvider typeProvider - */ - public function testCanChooseAppropriateModelFromAcceptHeader($mimeType, $modelClass) - { - $accept = new AcceptHeader(); - $accept->addMediaType($mimeType); - - $request = $this->event->getRequest(); - $request->getHeaders()->clearHeaders()->addHeader($accept); - - $this->selectModelListener->selectModel($this->event); - - $this->assertInstanceOf($modelClass, $this->event->getResult()); - } - - public function testCanForceModelBySendingItFromController() - { - // Explicitely set the type to text/html... - $accept = new AcceptHeader(); - $accept->addMediaType('text/html'); - - $request = $this->event->getRequest(); - $request->getHeaders()->clearHeaders()->addHeader($accept); - - // ... but explicitely simulate a JsonModel return value from Controller - $this->event->setResult(new JsonModel()); - - $this->selectModelListener->selectModel($this->event); - - $this->assertInstanceOf('Zend\View\Model\JsonModel', $this->event->getResult()); - } - - /** - * @dataProvider typeProvider - */ - public function testCanChooseAppropriateErrorModelFromAcceptHeader($mimeType, $modelClass) - { - $accept = new AcceptHeader(); - $accept->addMediaType($mimeType); - - $request = $this->event->getRequest(); - $request->getHeaders()->clearHeaders()->addHeader($accept); - - $this->selectModelListener->injectErrorModel($this->event); - - $this->assertInstanceOf($modelClass, $this->event->getResult()); - } - - /** - * @dataProvider typeProvider - */ - public function testAlwaysStopEventPropagationOnErrorIfFormatIsNotHtml($mimeType, $modelClass) - { - $accept = new AcceptHeader(); - $accept->addMediaType($mimeType); - - $request = $this->event->getRequest(); - $request->getHeaders()->clearHeaders()->addHeader($accept); - - $this->selectModelListener->injectErrorModel($this->event); - $model = $this->event->getViewModel(); - - if (get_class($model) === 'Zend\View\Model\ViewModel') { - $this->assertFalse($this->event->propagationIsStopped()); - } else { - $this->assertInstanceOf($modelClass, $this->event->getViewModel()); - $this->assertTrue($this->event->propagationIsStopped()); - } - } - - public function testDoesNotInterceptErrorApplicationResponse() - { - // typical application scenario - no result and the application has exited - $this->event->setError('application-hates-you'); - $this->event->setResult(null); - - $this->selectModelListener->selectModel($this->event); - - $this->assertNull($this->event->getResult()); - } -} diff --git a/tests/ZfrRestTest/Resource/Metadata/CollectionResourceMetadataTest.php b/tests/ZfrRestTest/Resource/Metadata/CollectionResourceMetadataTest.php deleted file mode 100644 index a692a49..0000000 --- a/tests/ZfrRestTest/Resource/Metadata/CollectionResourceMetadataTest.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ -class CollectionResourceMetadataTest extends TestCase -{ - /** - * @covers \ZfrRest\Resource\Metadata\CollectionResourceMetadata - */ - public function testResourceMetadata() - { - $resourceMetadata = new CollectionResourceMetadata('stdClass'); - - $resourceMetadata->controller = 'test'; - $this->assertSame('test', $resourceMetadata->getControllerName()); - $resourceMetadata->controller = null; - $this->assertSame(null, $resourceMetadata->getControllerName()); - - $resourceMetadata->inputFilter = 'test'; - $this->assertSame('test', $resourceMetadata->getInputFilterName()); - $resourceMetadata->inputFilter = null; - $this->assertSame(null, $resourceMetadata->getInputFilterName()); - - $resourceMetadata->hydrator = 'test'; - $this->assertSame('test', $resourceMetadata->getHydratorName()); - $resourceMetadata->hydrator = null; - $this->assertSame(null, $resourceMetadata->getHydratorName()); - } - - /** - * @covers \ZfrRest\Resource\Metadata\CollectionResourceMetadata - */ - public function testAssertHasDefaultHydrator() - { - $resourceMetadata = new CollectionResourceMetadata('stdClass'); - $this->assertSame('ZfrRest\Stdlib\Hydrator\PaginatorHydrator', $resourceMetadata->getHydratorName()); - } -} diff --git a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverTest.php b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverTest.php deleted file mode 100644 index 83a87d3..0000000 --- a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverTest.php +++ /dev/null @@ -1,66 +0,0 @@ -get('ZfrRest\Resource\Metadata\MetadataFactory'); - - /** @var \ZfrRest\Resource\Metadata\ResourceMetadataInterface $resourceMetadata */ - $resourceMetadata = $resourceMetadataFactory - ->getMetadataForClass('ZfrRestTest\Asset\Annotation\User') - ->getOutsideClassMetadata(); - - $this->assertEquals('ZfrRestTest\Asset\Annotation\User', $resourceMetadata->getClassName()); - $this->assertEquals('ZfrRestTest\Asset\Controller\UserController', $resourceMetadata->getControllerName()); - $this->assertSame('ZfrRestTest\Asset\\InputFilter\UserInputFilter', $resourceMetadata->getInputFilterName()); - $this->assertSame('ZfrRestTest\Asset\\Hydrator\UserHydrator', $resourceMetadata->getHydratorName()); - - $collectionMetadata = $resourceMetadata->getCollectionMetadata(); - $this->assertInstanceOf('ZfrRest\Resource\Metadata\CollectionResourceMetadataInterface', $collectionMetadata); - $this->assertEquals('Application\Controller\UserListController', $collectionMetadata->getControllerName()); - $this->assertSame('ZfrRest\Stdlib\Hydrator\PaginatorHydrator', $collectionMetadata->getHydratorName()); - - // Should reuse the input filter from Resource annotation as none is defined at collection level - $this->assertSame('ZfrRestTest\Asset\\InputFilter\UserInputFilter', $collectionMetadata->getInputFilterName()); - - $this->assertTrue($resourceMetadata->hasAssociation('tweets')); - $this->assertFalse($resourceMetadata->hasAssociation('posts')); - - $tweetMetadata = $resourceMetadata->getAssociationMetadata('tweets'); - - $this->assertInstanceOf('ZfrRest\Resource\Metadata\ResourceMetadataInterface', $tweetMetadata); - $this->assertEquals('ZfrRestTest\Asset\Annotation\Tweet', $tweetMetadata->getClassName()); - $this->assertEquals('Application\Controller\TweetController', $tweetMetadata->getControllerName()); - $this->assertEquals('Application\InputFilter\TweetInputFilter', $tweetMetadata->getInputFilterName()); - $this->assertEquals('DoctrineModule\Stdlib\Hydrator\DoctrineObject', $tweetMetadata->getHydratorName()); - - // Note that this one has been overriden by the User class at the association level - $this->assertEquals( - 'Application\Controller\UserTweetListController', - $tweetMetadata->getCollectionMetadata()->getControllerName() - ); - } -} diff --git a/tests/ZfrRestTest/Resource/Metadata/ResourceMetadataTest.php b/tests/ZfrRestTest/Resource/Metadata/ResourceMetadataTest.php deleted file mode 100644 index 22147ba..0000000 --- a/tests/ZfrRestTest/Resource/Metadata/ResourceMetadataTest.php +++ /dev/null @@ -1,112 +0,0 @@ - - */ -class ResourceMetadataTest extends TestCase -{ - /** - * @covers \ZfrRest\Resource\Metadata\ResourceMetadata - */ - public function testResourceMetadata() - { - $resourceMetadata = new ResourceMetadata('stdClass'); - $this->assertSame('stdClass', $resourceMetadata->getClassName()); - - $metadata = $this->getMock('Doctrine\\Common\\Persistence\\Mapping\\ClassMetadata'); - $resourceMetadata->classMetadata = $metadata; - $this->assertSame($metadata, $resourceMetadata->getClassMetadata()); - - $resourceMetadata->controller = 'test'; - $this->assertSame('test', $resourceMetadata->getControllerName()); - $resourceMetadata->controller = null; - $this->assertSame(null, $resourceMetadata->getControllerName()); - - $resourceMetadata->inputFilter = 'test'; - $this->assertSame('test', $resourceMetadata->getInputFilterName()); - $resourceMetadata->inputFilter = null; - $this->assertSame(null, $resourceMetadata->getInputFilterName()); - - $resourceMetadata->hydrator = 'test'; - $this->assertSame('test', $resourceMetadata->getHydratorName()); - $resourceMetadata->hydrator = null; - $this->assertSame(null, $resourceMetadata->getHydratorName()); - - $this->assertFalse($resourceMetadata->hasAssociation('assoc')); - $associationMetadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); - $resourceMetadata->associations['assoc'] = $associationMetadata; - $this->assertTrue($resourceMetadata->hasAssociation('assoc')); - $this->assertSame($associationMetadata, $resourceMetadata->getAssociationMetadata('assoc')); - } - - /** - * @covers \ZfrRest\Resource\Metadata\ResourceMetadata - */ - public function testAssertHasDefaultHydrator() - { - $resourceMetadata = new ResourceMetadata('stdClass'); - $this->assertSame('DoctrineModule\Stdlib\Hydrator\DoctrineObject', $resourceMetadata->getHydratorName()); - } - - /** - * @covers \ZfrRest\Resource\Metadata\ResourceMetadata - */ - public function testCanCreateEmptyResource() - { - $resourceMetadata = new ResourceMetadata('stdClass'); - $metadata = $this->getMock('Doctrine\\Common\\Persistence\\Mapping\\ClassMetadata'); - $resourceMetadata->classMetadata = $metadata; - $reflectionClass = new ReflectionClass('stdClass'); - - $metadata->expects($this->any())->method('getReflectionClass')->will($this->returnValue($reflectionClass)); - - $resource = $resourceMetadata->createResource(); - - $this->assertInstanceOf('ZfrRest\Resource\ResourceInterface', $resource); - $this->assertSame($resource->getMetadata(), $resourceMetadata); - $this->assertInstanceOf('stdClass', $resource->getData()); - } - - /** - * @covers \ZfrRest\Resource\Metadata\ResourceMetadata - */ - public function testCanCreateEmptyResourceWithParameter() - { - $resourceMetadata = new ResourceMetadata('ReflectionFunction'); - $metadata = $this->getMock('Doctrine\\Common\\Persistence\\Mapping\\ClassMetadata'); - $resourceMetadata->classMetadata = $metadata; - - $reflectionClass = new \ReflectionClass('ReflectionFunction'); - $metadata->expects($this->any())->method('getReflectionClass')->will($this->returnValue($reflectionClass)); - - $resource = $resourceMetadata->createResource('substr'); - - $this->assertInstanceOf('ZfrRest\Resource\ResourceInterface', $resource); - $this->assertSame($resource->getMetadata(), $resourceMetadata); - $this->assertInstanceOf('ReflectionFunction', $resource->getData()); - } -} diff --git a/tests/ZfrRestTest/Resource/ResourceTest.php b/tests/ZfrRestTest/Resource/ResourceTest.php deleted file mode 100644 index b78ac68..0000000 --- a/tests/ZfrRestTest/Resource/ResourceTest.php +++ /dev/null @@ -1,98 +0,0 @@ - - */ -class ResourceTest extends TestCase -{ - /** - * @covers \ZfrRest\Resource\Resource::__construct - * @covers \ZfrRest\Resource\Resource::getData - * @covers \ZfrRest\Resource\Resource::getMetadata - * @covers \ZfrRest\Resource\Resource::isCollection - * - * @dataProvider collectionResourceProvider - * - * @param string $className - * @param mixed $instance - * @param bool $isCollection - */ - public function testResource($className, $instance, $isCollection) - { - $metadata = $this->createMetadata(new ReflectionClass($className)); - $resource = new Resource($instance, $metadata); - - $this->assertSame($instance, $resource->getData()); - $this->assertSame($metadata, $resource->getMetadata()); - $this->assertSame($isCollection, $resource->isCollection()); - } - - /** - * @covers \ZfrRest\Resource\Resource::__construct - * @covers \ZfrRest\Resource\Exception\InvalidResourceException::invalidResourceProvided - */ - public function testDisallowsInvalidResource() - { - $metadata = $this->createMetadata(new ReflectionClass($this)); - - $this->setExpectedException('ZfrRest\\Resource\\Exception\\InvalidResourceException'); - - new Resource(new \stdClass(), $metadata); - } - - /** - * Data provider for various collection types - * - * @return array - */ - public function collectionResourceProvider() - { - return array( - array('stdClass', $this->getMock('Iterator'), true), - array('stdClass', $this->getMock('Doctrine\\Common\\Collections\\Selectable'), true), - array('stdClass', $this->getMock('Doctrine\\Common\\Collections\\Collection'), true), - array('stdClass', array(), true), - array('stdClass', new \stdClass(), false), - ); - } - - /** - * @param ReflectionClass $reflectionClass - * - * @return \PHPUnit_Framework_MockObject_MockObject|\ZfrRest\Resource\Metadata\ResourceMetadataInterface - */ - private function createMetadata(ReflectionClass $reflectionClass) - { - $resourceMetadata = $this->getMock('ZfrRest\\Resource\\Metadata\\ResourceMetadataInterface'); - $metadata = $this->getMock('Doctrine\\Common\\Persistence\\Mapping\\ClassMetadata'); - - $resourceMetadata->expects($this->any())->method('getClassMetadata')->will($this->returnValue($metadata)); - $metadata->expects($this->any())->method('getReflectionClass')->will($this->returnValue($reflectionClass)); - - return $resourceMetadata; - } -} diff --git a/tests/ZfrRestTest/Serializer/DecoderPluginManagerTest.php b/tests/ZfrRestTest/Serializer/DecoderPluginManagerTest.php deleted file mode 100644 index 95cafce..0000000 --- a/tests/ZfrRestTest/Serializer/DecoderPluginManagerTest.php +++ /dev/null @@ -1,70 +0,0 @@ -decoderPluginManager = new DecoderPluginManager(); - } - - public function testCanRetrieveEncodersFromDefaultFormat() - { - $plugin = $this->decoderPluginManager->get('application/json'); - $this->assertInstanceOf('Symfony\Component\Serializer\Encoder\JsonDecode', $plugin); - $this->assertInstanceOf('Symfony\Component\Serializer\Encoder\DecoderInterface', $plugin); - - $plugin = $this->decoderPluginManager->get('application/xml'); - $this->assertInstanceOf('Symfony\Component\Serializer\Encoder\XmlEncoder', $plugin); - $this->assertInstanceOf('Symfony\Component\Serializer\Encoder\DecoderInterface', $plugin); - } - - public function testCanRetrievePluginManagerWithServiceManager() - { - $serviceManager = new ServiceManager( - new ServiceManagerConfig( - array( - 'factories' => array( - 'DecoderPluginManager' => 'ZfrRest\Factory\DecoderPluginManagerFactory', - ), - ) - ) - ); - - $serviceManager->setService('ZfrRest\Options\ModuleOptions', new ModuleOptions()); - - $decoderPluginManager = $serviceManager->get('DecoderPluginManager'); - - $this->assertInstanceOf('ZfrRest\Serializer\DecoderPluginManager', $decoderPluginManager); - } -} diff --git a/tests/ZfrRestTest/View/Model/ModelPluginManagerTest.php b/tests/ZfrRestTest/View/Model/ModelPluginManagerTest.php deleted file mode 100644 index a49d122..0000000 --- a/tests/ZfrRestTest/View/Model/ModelPluginManagerTest.php +++ /dev/null @@ -1,67 +0,0 @@ -modelPluginManager = new ModelPluginManager(); - } - - public function testCanRetrieveModelFromDefaultFormat() - { - $plugin = $this->modelPluginManager->get('text/html'); - $this->assertInstanceOf('Zend\View\Model\ViewModel', $plugin); - - $plugin = $this->modelPluginManager->get('application/json'); - $this->assertInstanceOf('Zend\View\Model\JsonModel', $plugin); - } - - public function testCanRetrievePluginManagerWithServiceManager() - { - $serviceManager = new ServiceManager( - new ServiceManagerConfig( - array( - 'factories' => array( - 'ModelPluginManager' => 'ZfrRest\Factory\ModelPluginManagerFactory', - ) - ) - ) - ); - $serviceManager->setService('ZfrRest\Options\ModuleOptions', new ModuleOptions()); - - $modelPluginManager = $serviceManager->get('ModelPluginManager'); - - $this->assertInstanceOf('ZfrRest\View\Model\ModelPluginManager', $modelPluginManager); - } -} From 9f8f4b995d9fee93b7fd3edab2fe13eabd347e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Fri, 11 Oct 2013 16:07:31 +0200 Subject: [PATCH 02/19] Clean matchers and test for AssociationSubMatch --- composer.json | 8 +- src/ZfrRest/Exception/ExceptionInterface.php | 29 +++ .../Mvc/Exception/BadMethodCallException.php | 32 +++ .../Mvc/Exception/RuntimeException.php | 62 ++++++ .../Exception/UnexpectedValueException.php | 48 ++++ .../Matcher/AssociationSubPathMatcher.php | 76 +++++++ .../Http/Matcher/BaseSubPathMatcher.php | 87 ++++++++ .../Http/Matcher/CollectionFilteringEvent.php | 79 +++++++ .../Http/Matcher/CollectionSubPathMatcher.php | 165 ++++++++++++++ .../Mvc/Router/Http/Matcher/SubPathMatch.php | 88 ++++++++ .../Http/Matcher/SubPathMatcherInterface.php | 49 +++++ .../Mvc/Router/Http/ResourceGraphRoute.php | 208 ++++++++++++++++++ .../Exception/InvalidResourceException.php | 47 ++++ .../Resource/Exception/RuntimeException.php | 32 +++ .../Metadata/CollectionResourceMetadata.php | 69 ++++++ .../CollectionResourceMetadataInterface.php | 49 +++++ .../Resource/Metadata/ResourceMetadata.php | 133 +++++++++++ .../Metadata/ResourceMetadataInterface.php | 85 +++++++ src/ZfrRest/Resource/Resource.php | 81 +++++++ src/ZfrRest/Resource/ResourceInterface.php | 50 +++++ tests/ZfrRestTest/Asset/TweetAsset.php | 57 +++++ tests/ZfrRestTest/Asset/UserAsset.php | 89 ++++++++ .../Matcher/AssociationSubPathMatcherTest.php | 158 +++++++++++++ .../Resource/Asset/ResourceAsset.php | 23 ++ .../Asset/ResourceWithParametersAsset.php | 40 ++++ .../Metadata/ResourceMetadataTest.php | 72 ++++++ tests/ZfrRestTest/Resource/ResourceTest.php | 89 ++++++++ 27 files changed, 2001 insertions(+), 4 deletions(-) create mode 100755 src/ZfrRest/Exception/ExceptionInterface.php create mode 100755 src/ZfrRest/Mvc/Exception/BadMethodCallException.php create mode 100755 src/ZfrRest/Mvc/Exception/RuntimeException.php create mode 100644 src/ZfrRest/Mvc/Exception/UnexpectedValueException.php create mode 100644 src/ZfrRest/Mvc/Router/Http/Matcher/AssociationSubPathMatcher.php create mode 100644 src/ZfrRest/Mvc/Router/Http/Matcher/BaseSubPathMatcher.php create mode 100644 src/ZfrRest/Mvc/Router/Http/Matcher/CollectionFilteringEvent.php create mode 100644 src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php create mode 100644 src/ZfrRest/Mvc/Router/Http/Matcher/SubPathMatch.php create mode 100644 src/ZfrRest/Mvc/Router/Http/Matcher/SubPathMatcherInterface.php create mode 100644 src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php create mode 100755 src/ZfrRest/Resource/Exception/InvalidResourceException.php create mode 100755 src/ZfrRest/Resource/Exception/RuntimeException.php create mode 100755 src/ZfrRest/Resource/Metadata/CollectionResourceMetadata.php create mode 100755 src/ZfrRest/Resource/Metadata/CollectionResourceMetadataInterface.php create mode 100755 src/ZfrRest/Resource/Metadata/ResourceMetadata.php create mode 100755 src/ZfrRest/Resource/Metadata/ResourceMetadataInterface.php create mode 100755 src/ZfrRest/Resource/Resource.php create mode 100755 src/ZfrRest/Resource/ResourceInterface.php create mode 100644 tests/ZfrRestTest/Asset/TweetAsset.php create mode 100644 tests/ZfrRestTest/Asset/UserAsset.php create mode 100644 tests/ZfrRestTest/Mvc/Router/Http/Matcher/AssociationSubPathMatcherTest.php create mode 100644 tests/ZfrRestTest/Resource/Asset/ResourceAsset.php create mode 100644 tests/ZfrRestTest/Resource/Asset/ResourceWithParametersAsset.php create mode 100644 tests/ZfrRestTest/Resource/Metadata/ResourceMetadataTest.php create mode 100755 tests/ZfrRestTest/Resource/ResourceTest.php diff --git a/composer.json b/composer.json index cc535ab..f038501 100644 --- a/composer.json +++ b/composer.json @@ -26,10 +26,10 @@ "minimum-stability": "dev", "require": { "php": ">=5.3.3", - "symfony/serializer": ">=2.1", - "jms/metadata": ">=1.3", - "zendframework/zendframework": ">=2.2", - "doctrine/common": ">=2.3", + "symfony/serializer": "~2.2", + "jms/metadata": "~1.4", + "zendframework/zendframework": "~2.2", + "doctrine/common": "~2.4", "doctrine/doctrine-module": "~0.8" }, "require-dev": { diff --git a/src/ZfrRest/Exception/ExceptionInterface.php b/src/ZfrRest/Exception/ExceptionInterface.php new file mode 100755 index 0000000..d3a1885 --- /dev/null +++ b/src/ZfrRest/Exception/ExceptionInterface.php @@ -0,0 +1,29 @@ + + */ +interface ExceptionInterface +{ +} diff --git a/src/ZfrRest/Mvc/Exception/BadMethodCallException.php b/src/ZfrRest/Mvc/Exception/BadMethodCallException.php new file mode 100755 index 0000000..ae667b8 --- /dev/null +++ b/src/ZfrRest/Mvc/Exception/BadMethodCallException.php @@ -0,0 +1,32 @@ + + */ +class BadMethodCallException extends BaseBadMethodCallException implements ExceptionInterface +{ +} diff --git a/src/ZfrRest/Mvc/Exception/RuntimeException.php b/src/ZfrRest/Mvc/Exception/RuntimeException.php new file mode 100755 index 0000000..088a604 --- /dev/null +++ b/src/ZfrRest/Mvc/Exception/RuntimeException.php @@ -0,0 +1,62 @@ + + */ +class RuntimeException extends BaseRuntimeException implements ExceptionInterface +{ + /** + * @param ClassMetadata $classMetadata + * + * @return self + */ + public static function missingCollectionMetadata(ClassMetadata $classMetadata) + { + return new self( + sprintf( + 'Collection metadata not found. Do you have a @Collection annotation for the resource "%s"?', + $classMetadata->getName() + ) + ); + } + + /** + * @param mixed $resource + * + * @return self + */ + public static function unsupportedResourceType($resource) + { + return new self( + sprintf( + 'Resource "%s" is not supported: either specify an ObjectRepository instance, or an entity class name', + is_object($resource) ? get_class($resource) : gettype($resource) + ) + ); + } +} diff --git a/src/ZfrRest/Mvc/Exception/UnexpectedValueException.php b/src/ZfrRest/Mvc/Exception/UnexpectedValueException.php new file mode 100644 index 0000000..34cf3ab --- /dev/null +++ b/src/ZfrRest/Mvc/Exception/UnexpectedValueException.php @@ -0,0 +1,48 @@ + + */ +class UnexpectedValueException extends BaseUnexpectedValueException implements ExceptionInterface +{ + /** + * @param ResourceMetadataInterface $metadata + * @param mixed $item + * @return UnexpectedValueException + */ + public static function unexpectedResourceType(ResourceMetadataInterface $metadata, $item) + { + return new self( + sprintf( + 'Unexpected value of type "%s" found, expecting', + is_object($item) ? get_class($item) : gettype($item), + $metadata->getClassMetadata()->getName() + ) + ); + } +} diff --git a/src/ZfrRest/Mvc/Router/Http/Matcher/AssociationSubPathMatcher.php b/src/ZfrRest/Mvc/Router/Http/Matcher/AssociationSubPathMatcher.php new file mode 100644 index 0000000..05e20a0 --- /dev/null +++ b/src/ZfrRest/Mvc/Router/Http/Matcher/AssociationSubPathMatcher.php @@ -0,0 +1,76 @@ + + * @author Michaël Gallego + */ +class AssociationSubPathMatcher implements SubPathMatcherInterface +{ + /** + * {@inheritDoc} + */ + public function matchSubPath( + ResourceInterface $resource, + $subPath, + HttpRequest $request, + SubPathMatch $previousMatch = null + ) { + if ($resource->isCollection()) { + return null; + } + + $data = $resource->getData(); + $resourceMetadata = $resource->getMetadata(); + + $pathChunks = explode('/', trim($subPath, '/'), 2); + $associationName = array_shift($pathChunks); + + if (!$resourceMetadata->hasAssociation($associationName)) { + return null; + } + + $classMetadata = $resourceMetadata->getClassMetadata(); + $reflectionClass = $classMetadata->getReflectionClass(); + $reflectionProperty = $reflectionClass->getProperty($associationName); + $associationMetadata = $resourceMetadata->getAssociationMetadata($associationName); + + $reflectionProperty->setAccessible(true); + + $associationData = $reflectionProperty->getValue($data); + + return new SubPathMatch( + new Resource($associationData, $associationMetadata), + $associationName, + $previousMatch + ); + } +} diff --git a/src/ZfrRest/Mvc/Router/Http/Matcher/BaseSubPathMatcher.php b/src/ZfrRest/Mvc/Router/Http/Matcher/BaseSubPathMatcher.php new file mode 100644 index 0000000..f389f11 --- /dev/null +++ b/src/ZfrRest/Mvc/Router/Http/Matcher/BaseSubPathMatcher.php @@ -0,0 +1,87 @@ + + * @author Michaël Gallego + */ +class BaseSubPathMatcher implements SubPathMatcherInterface +{ + /** + * @var CollectionSubPathMatcher + */ + private $collectionMatcher; + + /** + * @var AssociationSubPathMatcher + */ + private $associationMatcher; + + /** + * Constructor + */ + public function __construct( + CollectionSubPathMatcher $collectionMatcher, + AssociationSubPathMatcher $associationMatcher + ) { + $this->collectionMatcher = $collectionMatcher; + $this->associationMatcher = $associationMatcher; + } + + /** + * {@inheritDoc} + */ + public function matchSubPath( + ResourceInterface $resource, + $subPath, + HttpRequest $request, + SubPathMatch $previousMatch = null + ) { + $path = trim($subPath, '/'); + + if (empty($path)) { + return new SubPathMatch($resource, $subPath); + } + + if ($resource->isCollection()) { + $match = $this->collectionMatcher->matchSubPath($resource, $path, $request); + } else { + $match = $this->associationMatcher->matchSubPath($resource, $path, $request); + } + + if (!$match) { + return null; + } + + return $this->matchSubPath( + $match->getMatchedResource(), + substr($path, strlen($match->getMatchedPath())), + $request, + $match + ); + } +} diff --git a/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionFilteringEvent.php b/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionFilteringEvent.php new file mode 100644 index 0000000..acd643d --- /dev/null +++ b/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionFilteringEvent.php @@ -0,0 +1,79 @@ + + */ +class CollectionFilteringEvent extends Event +{ + /** + * Key for event + */ + const EVENT_COLLECTION_FILTERING = 'collectionFiltering'; + + /** + * @var ResourceInterface + */ + protected $resource; + + /** + * @var HttpRequest + */ + protected $request; + + /** + * Constructor + * + * @param ResourceInterface $resource + * @param HttpRequest $request + */ + public function __construct(ResourceInterface $resource, HttpRequest $request) + { + $this->resource = $resource; + $this->request = $request; + } + + /** + * Get the resource + * + * @return ResourceInterface + */ + public function getResource() + { + return $this->resource; + } + + /** + * Get the HTTP request + * + * @return HttpRequest + */ + public function getRequest() + { + return $this->request; + } +} diff --git a/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php b/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php new file mode 100644 index 0000000..3fd29f8 --- /dev/null +++ b/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php @@ -0,0 +1,165 @@ + + * @author Michaël Gallego + */ +class CollectionSubPathMatcher implements SubPathMatcherInterface, EventManagerAwareInterface +{ + /** + * @var EventManagerInterface + */ + protected $eventManager; + + /** + * {@inheritDoc} + */ + public function matchSubPath( + ResourceInterface $resource, + $subPath, + HttpRequest $request, + SubPathMatch $previousMatch = null + ) { + if (!$resource->isCollection()) { + return null; + } + + $path = trim($subPath, '/'); + + if (empty($path)) { + return new SubPathMatch($this->filterAssociation($resource, $request), $subPath); + } + + $pathChunks = explode('/', trim($subPath, '/'), 2); + $identifier = array_shift($pathChunks); + $classMetadata = $resource->getMetadata()->getClassMetadata(); + $data = $this->findItem($resource->getData(), $classMetadata->getIdentifierFieldNames(), $identifier); + + if (null === $data) { + return null; + } + + return new SubPathMatch( + new Resource($data, $resource->getMetadata()), + array_shift($pathChunks), + $previousMatch + ); + } + + /** + * {@inheritDoc} + */ + public function setEventManager(EventManagerInterface $eventManager) + { + $eventManager->setIdentifiers(array( + __CLASS__, + get_called_class() + )); + + $this->eventManager = $eventManager; + } + + /** + * {@inheritDoc} + */ + public function getEventManager() + { + if (null === $this->eventManager) { + $this->setEventManager(new EventManager()); + } + + return $this->eventManager; + } + + /** + * Retrieves a single item in the collection by its identifier + * + * @param mixed $data + * @param array $identifierNames + * @param mixed $identifier + * @return mixed|null + * + * @throws RuntimeException on composite identifiers (not yet supported) + */ + protected function findItem($data, array $identifierNames, $identifier) + { + if (count($identifierNames) > 1) { + throw new RuntimeException(get_class($this) . ' is not able to handle composite identifiers'); + } + + if (!$data instanceof Selectable && $data instanceof Traversable) { + $data = new ArrayCollection(iterator_to_array($data)); + } + + $criteria = new Criteria(); + $criteria->andWhere($criteria->expr()->eq(reset($identifierNames), $identifier)); + + $found = $data->matching($criteria); + + return $found->isEmpty() ? null : $found->first(); + } + + /** + * Filters the given resource by using the request object, then return the filtered subset + * + * @param ResourceInterface $resource + * @param HttpRequest $request + * @return ResourceInterface + */ + protected function filterAssociation(ResourceInterface $resource, HttpRequest $request) + { + $data = $resource->getData(); + + if (!$data instanceof Selectable) { + return $resource; + } + + // Trigger an event to allow custom filtering + $this->eventManager->trigger( + CollectionFilteringEvent::EVENT_COLLECTION_FILTERING, + new CollectionFilteringEvent($resource, $request) + ); + + return $resource; + } +} diff --git a/src/ZfrRest/Mvc/Router/Http/Matcher/SubPathMatch.php b/src/ZfrRest/Mvc/Router/Http/Matcher/SubPathMatch.php new file mode 100644 index 0000000..98f15e0 --- /dev/null +++ b/src/ZfrRest/Mvc/Router/Http/Matcher/SubPathMatch.php @@ -0,0 +1,88 @@ + + * @author Michaël Gallego + */ +class SubPathMatch +{ + /** + * @var string + */ + protected $matchedPath; + + /** + * @var ResourceInterface + */ + protected $matchedResource; + + /** + * @var SubPathMatch|null + */ + protected $previousMatch; + + /** + * @param ResourceInterface $matchedResource + * @param string $matchedPath + * @param SubPathMatch|null $previousMatch + */ + public function __construct(ResourceInterface $matchedResource, $matchedPath, SubPathMatch $previousMatch = null) + { + $this->matchedResource = $matchedResource; + $this->matchedPath = $matchedPath; + $this->previousMatch = $previousMatch; + } + + /** + * Get the matched path + * + * @return string + */ + public function getMatchedPath() + { + return $this->matchedPath; + } + + /** + * Get the matched resource + * + * @return ResourceInterface + */ + public function getMatchedResource() + { + return $this->matchedResource; + } + + /** + * Get the previous match (null if none) + * + * @return SubPathMatch|null + */ + public function getPreviousMatch() + { + return $this->previousMatch; + } +} diff --git a/src/ZfrRest/Mvc/Router/Http/Matcher/SubPathMatcherInterface.php b/src/ZfrRest/Mvc/Router/Http/Matcher/SubPathMatcherInterface.php new file mode 100644 index 0000000..b9d3011 --- /dev/null +++ b/src/ZfrRest/Mvc/Router/Http/Matcher/SubPathMatcherInterface.php @@ -0,0 +1,49 @@ + + * @author Michaël Gallego + */ +interface SubPathMatcherInterface +{ + /** + * @param ResourceInterface $resource + * @param string $subPath + * @param HttpRequest $request + * @param SubPathMatch|null $previousMatch + * @return SubPathMatch|null + */ + public function matchSubPath( + ResourceInterface $resource, + $subPath, + HttpRequest $request, + SubPathMatch $previousMatch = null + ); +} diff --git a/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php b/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php new file mode 100644 index 0000000..4fd0e8b --- /dev/null +++ b/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php @@ -0,0 +1,208 @@ + + * @author Michaël Gallego + */ +class ResourceGraphRoute implements RouteInterface +{ + /** + * @var MetadataFactoryInterface + */ + protected $metadataFactory; + + /** + * @var ResourceInterface|mixed + */ + protected $resource; + + /** + * @var BaseSubPathMatcher + */ + protected $subPathMatcher; + + /** + * @var string + */ + protected $route; + + /** + * Constructor + * + * @param MetadataFactoryInterface $metadataFactory + * @param BaseSubPathMatcher $matcher + * @param ResourceInterface|mixed $resource + * @param string $route + */ + public function __construct( + MetadataFactoryInterface $metadataFactory, + BaseSubPathMatcher $matcher, + $resource, + $route + ) { + $this->metadataFactory = $metadataFactory; + $this->subPathMatcher = $matcher; + $this->resource = $resource; + $this->route = $route; + } + + /** + * {@inheritDoc} + */ + public function assemble(array $params = array(), array $options = array()) + { + // TODO: Implement assemble() method. + } + + /** + * {@inheritDoc} + */ + public function getAssembledParams() + { + // TODO: Implement getAssembledParams() method. + } + + /** + * {@inheritDoc} + */ + public static function factory($options = array()) + { + throw new Exception\BadMethodCallException('Not supported'); + } + + /** + * {@inheritDoc} + */ + public function match(Request $request) + { + if (!$request instanceof HttpRequest) { + return null; + } + + $uri = $request->getUri(); + $path = $uri->getPath(); + + // If the route is not even contained within the URI, we can return early... + if (strpos($path, $this->route) === false) { + return null; + } + + $subPath = substr($path, strlen($this->route)); + + if (! $match = $this->subPathMatcher->matchSubPath($this->getResource(), $subPath, $request)) { + return null; + } + + return $this->buildRouteMatch($match->matchedResource, $path); + } + + /** + * Build a route match. This function extract the controller from the resource metadata, and does + * optional filtering by query + * + * @param ResourceInterface $resource + * @param string $path + * @throws Exception\RuntimeException + * @return RouteMatch + */ + protected function buildRouteMatch(ResourceInterface $resource, $path) + { + $metadata = $resource->getMetadata(); + $collectionMetadata = $metadata->getCollectionMetadata(); + $classMetadata = $metadata->getClassMetadata(); + $data = $resource->getData(); + + // If returned $data is a collection, then we use the controller specified in Collection mapping + if ($resource->isCollection()) { + if (null === $collectionMetadata) { + throw Exception\RuntimeException::missingCollectionMetadata($classMetadata); + } + + if ($data instanceof Collection) { + $resource = new Resource(new ResourcePaginator($metadata, new CollectionAdapter($data)), $metadata); + } elseif ($data instanceof Selectable) { + $resource = new Resource(new ResourcePaginator($metadata, new SelectableAdapter($data)), $metadata); + } + + $controllerName = $collectionMetadata->getControllerName(); + } else { + $controllerName = $metadata->getControllerName(); + } + + return new RouteMatch( + array( + 'resource' => $resource, + 'controller' => $controllerName + ), + strlen($path) + ); + } + + /** + * Initialize the resource to create an object implementing the ResourceInterface interface. A resource can + * be anything: an entity, a collection, a Selectable... However, any ResourceInterface object contains both + * the resource AND metadata associated to it. This metadata is usually extracted from the entity name + * + * @throws Exception\RuntimeException + * + * @return ResourceInterface + */ + private function getResource() + { + // Don't initialize twice + if ($this->resource instanceof ResourceInterface) { + return $this->resource; + } + + /** @var $metadata \Metadata\ClassHierarchyMetadata */ + $metadata = null; + $resource = $this->resource; + + if ($resource instanceof ObjectRepository) { + $metadata = $this->metadataFactory->getMetadataForClass($resource->getClassName()); + } elseif (is_string($resource)) { + $metadata = $this->metadataFactory->getMetadataForClass($resource); + } else { + throw RuntimeException::unsupportedResourceType($resource); + } + + return $this->resource = new Resource($resource, $metadata->getOutsideClassMetadata()); + } +} diff --git a/src/ZfrRest/Resource/Exception/InvalidResourceException.php b/src/ZfrRest/Resource/Exception/InvalidResourceException.php new file mode 100755 index 0000000..0985240 --- /dev/null +++ b/src/ZfrRest/Resource/Exception/InvalidResourceException.php @@ -0,0 +1,47 @@ + + */ +class InvalidResourceException extends InvalidArgumentException implements ExceptionInterface +{ + /** + * @param mixed $resource + * @param ResourceMetadataInterface $metadata + * @return InvalidResourceException + */ + public static function invalidResourceProvided($resource, ResourceMetadataInterface $metadata) + { + return new self( + sprintf( + 'Provided resource of type "%s" is not an instance nor collection of requested type "%s"', + is_object($resource) ? get_class($resource) : gettype($resource), + $metadata->getClassMetadata()->getName() + ) + ); + } +} diff --git a/src/ZfrRest/Resource/Exception/RuntimeException.php b/src/ZfrRest/Resource/Exception/RuntimeException.php new file mode 100755 index 0000000..c03d6fe --- /dev/null +++ b/src/ZfrRest/Resource/Exception/RuntimeException.php @@ -0,0 +1,32 @@ + + */ +class RuntimeException extends BaseRuntimeException implements ExceptionInterface +{ +} diff --git a/src/ZfrRest/Resource/Metadata/CollectionResourceMetadata.php b/src/ZfrRest/Resource/Metadata/CollectionResourceMetadata.php new file mode 100755 index 0000000..ebc6ff4 --- /dev/null +++ b/src/ZfrRest/Resource/Metadata/CollectionResourceMetadata.php @@ -0,0 +1,69 @@ + + */ +class CollectionResourceMetadata extends ClassMetadata implements CollectionResourceMetadataInterface +{ + /** + * @var string + */ + public $controller; + + /** + * @var string + */ + public $inputFilter; + + /** + * @var string + */ + public $hydrator; + + /** + * {@inheritDoc} + */ + public function getControllerName() + { + return $this->controller; + } + + /** + * {@inheritDoc} + */ + public function getInputFilterName() + { + return $this->inputFilter; + } + + /** + * {@inheritDoc} + */ + public function getHydratorName() + { + return $this->hydrator; + } +} diff --git a/src/ZfrRest/Resource/Metadata/CollectionResourceMetadataInterface.php b/src/ZfrRest/Resource/Metadata/CollectionResourceMetadataInterface.php new file mode 100755 index 0000000..93b939b --- /dev/null +++ b/src/ZfrRest/Resource/Metadata/CollectionResourceMetadataInterface.php @@ -0,0 +1,49 @@ + + */ +class ResourceMetadata extends ClassMetadata implements ResourceMetadataInterface +{ + /** + * @var DoctrineClassMetadata + */ + public $classMetadata; + + /** + * @var string + */ + public $controller; + + /** + * @var string + */ + public $inputFilter; + + /** + * @var string + */ + public $hydrator; + + /** + * @var ResourceMetadataInterface[]|array + */ + public $associations; + + /** + * @var CollectionResourceMetadataInterface + */ + public $collectionMetadata; + + /** + * {@inheritDoc} + */ + public function createResource() + { + $args = func_get_args(); + + if (empty($args)) { + return new Resource($this->reflection->newInstance(), $this); + } + + return new Resource($this->reflection->newInstanceArgs($args), $this); + } + + /** + * {@inheritDoc} + */ + public function getClassMetadata() + { + return $this->classMetadata; + } + + /** + * {@inheritDoc} + */ + public function getControllerName() + { + return $this->controller; + } + + /** + * {@inheritDoc} + */ + public function getInputFilterName() + { + return $this->inputFilter; + } + + /** + * {@inheritDoc} + */ + public function getHydratorName() + { + return $this->hydrator; + } + + /** + * {@inheritDoc} + */ + public function getAssociationMetadata($association) + { + return $this->associations[$association]; + } + + /** + * {@inheritDoc} + */ + public function hasAssociation($association) + { + return isset($this->associations[$association]); + } + + /** + * {@inheritDoc} + */ + public function getCollectionMetadata() + { + return $this->collectionMetadata; + } +} diff --git a/src/ZfrRest/Resource/Metadata/ResourceMetadataInterface.php b/src/ZfrRest/Resource/Metadata/ResourceMetadataInterface.php new file mode 100755 index 0000000..2b79792 --- /dev/null +++ b/src/ZfrRest/Resource/Metadata/ResourceMetadataInterface.php @@ -0,0 +1,85 @@ + + */ +interface ResourceMetadataInterface +{ + /** + * Create a new resource whose type is equals to class name + * + * @return \ZfrRest\Resource\ResourceInterface + */ + public function createResource(); + + /** + * Get the class metadata (provides information about Doctrine mapping) + * + * @return \Doctrine\Common\Persistence\Mapping\ClassMetadata + */ + public function getClassMetadata(); + + /** + * Get the controller's FQCN + * + * @return string|null + */ + public function getControllerName(); + + /** + * Get the input filter's FQCN to be used for this resource + * + * @return string|null + */ + public function getInputFilterName(); + + /** + * Get the hydrator's FQCN to be used for this resource + * + * @return string|null + */ + public function getHydratorName(); + + /** + * Get the metadata to a given association + * + * @param string $association + * @return ResourceMetadataInterface + */ + public function getAssociationMetadata($association); + + /** + * Check if the resource metadata can traverse the given association + * + * @param string $association + * @return bool + */ + public function hasAssociation($association); + + /** + * Get metadata for this resource when treated as a collection + * + * @return CollectionResourceMetadataInterface + */ + public function getCollectionMetadata(); +} diff --git a/src/ZfrRest/Resource/Resource.php b/src/ZfrRest/Resource/Resource.php new file mode 100755 index 0000000..08efde6 --- /dev/null +++ b/src/ZfrRest/Resource/Resource.php @@ -0,0 +1,81 @@ + + */ +class Resource implements ResourceInterface +{ + /** + * @var mixed + */ + protected $data; + + /** + * @var ResourceMetadataInterface + */ + protected $metadata; + + /** + * @param mixed $data + * @param ResourceMetadataInterface $metadata + * @throws Exception\InvalidResourceException + */ + public function __construct($data, ResourceMetadataInterface $metadata) + { + $this->data = $data; + $this->metadata = $metadata; + + $reflectionClass = $metadata->getClassMetadata()->getReflectionClass(); + + if (!$this->isCollection() && !$reflectionClass->isInstance($data)) { + throw InvalidResourceException::invalidResourceProvided($data, $metadata); + } + } + + /** + * {@inheritDoc} + */ + public function getMetadata() + { + return $this->metadata; + } + + /** + * {@inheritDoc} + */ + public function getData() + { + return $this->data; + } + + /** + * {@inheritDoc} + */ + public function isCollection() + { + return ($this->data instanceof Selectable || $this->data instanceof Traversable || is_array($this->data)); + } +} diff --git a/src/ZfrRest/Resource/ResourceInterface.php b/src/ZfrRest/Resource/ResourceInterface.php new file mode 100755 index 0000000..be81358 --- /dev/null +++ b/src/ZfrRest/Resource/ResourceInterface.php @@ -0,0 +1,50 @@ + + */ +interface ResourceInterface +{ + /** + * Get the resource metadata + * + * @return Metadata\ResourceMetadataInterface + */ + public function getMetadata(); + + /** + * Retrieves the resource data, which can be either an instance of the provided metadata name or + * a collection of these instances + * + * @return array|object + */ + public function getData(); + + /** + * Check if the resource is a collection of instances of the resource type + * + * @return bool + */ + public function isCollection(); +} diff --git a/tests/ZfrRestTest/Asset/TweetAsset.php b/tests/ZfrRestTest/Asset/TweetAsset.php new file mode 100644 index 0000000..a3b48c8 --- /dev/null +++ b/tests/ZfrRestTest/Asset/TweetAsset.php @@ -0,0 +1,57 @@ +id; + } + + /** + * @param string $content + * @return void + */ + public function setContent($content) + { + $this->content = $content; + } + + /** + * @return string + */ + public function getContent() + { + return $this->content; + } +} diff --git a/tests/ZfrRestTest/Asset/UserAsset.php b/tests/ZfrRestTest/Asset/UserAsset.php new file mode 100644 index 0000000..56898a2 --- /dev/null +++ b/tests/ZfrRestTest/Asset/UserAsset.php @@ -0,0 +1,89 @@ +tweets = new ArrayCollection(); + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @param string $username + * @return void + */ + public function setUsername($username) + { + $this->username = $username; + } + + /** + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * @param array $tweets + * @return void + */ + public function setTweets(array $tweets) + { + $this->tweets->add($tweets); + } + + /** + * @return ArrayCollection + */ + public function getTweets() + { + return $this->tweets; + } +} diff --git a/tests/ZfrRestTest/Mvc/Router/Http/Matcher/AssociationSubPathMatcherTest.php b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/AssociationSubPathMatcherTest.php new file mode 100644 index 0000000..e10655d --- /dev/null +++ b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/AssociationSubPathMatcherTest.php @@ -0,0 +1,158 @@ + + * @covers \ZfrRest\Mvc\Router\Http\Matcher\AssociationSubPathMatcher + */ +class AssociationSubPathMatcherTest extends TestCase +{ + public function testReturnNullIfResourceIsCollection() + { + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $resource->expects($this->once()) + ->method('isCollection') + ->will($this->returnValue(true)); + + $associationPathMatcher = new AssociationSubPathMatcher(); + $this->assertNull($associationPathMatcher->matchSubPath($resource, 'path', new HttpRequest())); + } + + public function testReturnNullIfNoAssociationIsFound() + { + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $resource->expects($this->once()) + ->method('isCollection') + ->will($this->returnValue(false)); + + $resource->expects($this->once()) + ->method('getData') + ->will($this->returnValue(new UserAsset())); + + $resourceMetadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); + $resourceMetadata->expects($this->once()) + ->method('hasAssociation') + ->with('foo') + ->will($this->returnValue(false)); + + $resource->expects($this->once()) + ->method('getMetadata') + ->will($this->returnValue($resourceMetadata)); + + $associationPathMatcher = new AssociationSubPathMatcher(); + $this->assertNull($associationPathMatcher->matchSubPath($resource, 'foo/bar/baz', new HttpRequest())); + } + + public function testCanMatchAssociation() + { + $user = new UserAsset(); + $user->setTweets(array(new TweetAsset(), new TweetAsset())); + + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $resource->expects($this->any()) + ->method('isCollection') + ->will($this->returnValue(false)); + + $resource->expects($this->any()) + ->method('getData') + ->will($this->returnValue($user)); + + $resourceMetadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); + $resourceMetadata->expects($this->any()) + ->method('hasAssociation') + ->with('tweets') + ->will($this->returnValue(true)); + + $resource->expects($this->any()) + ->method('getMetadata') + ->will($this->returnValue($resourceMetadata)); + + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $classMetadata->expects($this->any()) + ->method('getReflectionClass') + ->will($this->returnValue(new \ReflectionClass($user))); + + $resourceMetadata->expects($this->any()) + ->method('getClassMetadata') + ->will($this->returnValue($classMetadata)); + + $associationClassMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $associationClassMetadata->expects($this->any()) + ->method('getReflectionClass') + ->will($this->returnValue(new \ReflectionClass('ZfrRestTest\Asset\TweetAsset'))); + + $associationMetadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); + $associationMetadata->expects($this->any()) + ->method('getClassMetadata') + ->will($this->returnValue($associationClassMetadata)); + + $resourceMetadata->expects($this->any()) + ->method('getAssociationMetadata') + ->with('tweets') + ->will($this->returnValue($associationMetadata)); + + $associationPathMatcher = new AssociationSubPathMatcher(); + + // Simple path with trailing / + $result = $associationPathMatcher->matchSubPath($resource, '/tweets', new HttpRequest()); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', $result); + $this->assertNull($result->getPreviousMatch()); + $this->assertEquals('tweets', $result->getMatchedPath()); + $this->assertSame($user->getTweets(), $result->getMatchedResource()->getData()); + $this->assertSame($associationMetadata, $result->getMatchedResource()->getMetadata()); + + // Simple path with ending / + $result = $associationPathMatcher->matchSubPath($resource, 'tweets/', new HttpRequest()); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', $result); + $this->assertNull($result->getPreviousMatch()); + $this->assertEquals('tweets', $result->getMatchedPath()); + $this->assertSame($user->getTweets(), $result->getMatchedResource()->getData()); + $this->assertSame($associationMetadata, $result->getMatchedResource()->getMetadata()); + + // Simple path with trailing and ending / + $result = $associationPathMatcher->matchSubPath($resource, '/tweets/', new HttpRequest()); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', $result); + $this->assertNull($result->getPreviousMatch()); + $this->assertEquals('tweets', $result->getMatchedPath()); + $this->assertSame($user->getTweets(), $result->getMatchedResource()->getData()); + $this->assertSame($associationMetadata, $result->getMatchedResource()->getMetadata()); + + // Simple path with a previous submatch + $previousSubMatch = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', array(), array(), '', false); + $result = $associationPathMatcher->matchSubPath($resource, '/tweets', new HttpRequest(), $previousSubMatch); + $this->assertSame($previousSubMatch, $result->getPreviousMatch()); + + // With more complex path + $result = $associationPathMatcher->matchSubPath($resource, '/tweets/5/retweets', new HttpRequest()); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', $result); + $this->assertNull($result->getPreviousMatch()); + $this->assertEquals('tweets', $result->getMatchedPath()); + $this->assertSame($user->getTweets(), $result->getMatchedResource()->getData()); + $this->assertSame($associationMetadata, $result->getMatchedResource()->getMetadata()); + } +} diff --git a/tests/ZfrRestTest/Resource/Asset/ResourceAsset.php b/tests/ZfrRestTest/Resource/Asset/ResourceAsset.php new file mode 100644 index 0000000..9a39334 --- /dev/null +++ b/tests/ZfrRestTest/Resource/Asset/ResourceAsset.php @@ -0,0 +1,23 @@ +param = $param; + } + + public function getParam() + { + return $this->param; + } +} diff --git a/tests/ZfrRestTest/Resource/Metadata/ResourceMetadataTest.php b/tests/ZfrRestTest/Resource/Metadata/ResourceMetadataTest.php new file mode 100644 index 0000000..7dc82e2 --- /dev/null +++ b/tests/ZfrRestTest/Resource/Metadata/ResourceMetadataTest.php @@ -0,0 +1,72 @@ + + * @covers \ZfrRest\Resource\Metadata\ResourceMetadata + */ +class ResourceMetadataTest extends TestCase +{ + public function testCanCheckAssociations() + { + $resourceMetadata = new ResourceMetadata('stdClass'); + $resourceMetadata->associations['tweets'] = new \stdClass(); + + $this->assertTrue($resourceMetadata->hasAssociation('tweets')); + $this->assertFalse($resourceMetadata->hasAssociation('retweets')); + } + + public function testCanCreateSimpleResource() + { + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $classMetadata->expects($this->once()) + ->method('getReflectionClass') + ->will($this->returnValue(new ReflectionClass('ZfrRestTest\Resource\Asset\ResourceAsset'))); + + $resourceMetadata = new ResourceMetadata('ZfrRestTest\Resource\Asset\ResourceAsset'); + $resourceMetadata->classMetadata = $classMetadata; + + $resource = $resourceMetadata->createResource(); + $this->assertInstanceOf('ZfrRestTest\Resource\Asset\ResourceAsset', $resource->getData()); + $this->assertSame($resourceMetadata, $resource->getMetadata()); + } + + public function testCanCreateComplexResource() + { + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $classMetadata->expects($this->once()) + ->method('getReflectionClass') + ->will($this->returnValue(new ReflectionClass('ZfrRestTest\Resource\Asset\ResourceWithParametersAsset'))); + + $resourceMetadata = new ResourceMetadata('ZfrRestTest\Resource\Asset\ResourceWithParametersAsset'); + $resourceMetadata->classMetadata = $classMetadata; + + $resource = $resourceMetadata->createResource('foo'); + $this->assertInstanceOf('ZfrRestTest\Resource\Asset\ResourceWithParametersAsset', $resource->getData()); + $this->assertEquals('foo', $resource->getData()->getParam()); + $this->assertSame($resourceMetadata, $resource->getMetadata()); + } +} diff --git a/tests/ZfrRestTest/Resource/ResourceTest.php b/tests/ZfrRestTest/Resource/ResourceTest.php new file mode 100755 index 0000000..4cdff78 --- /dev/null +++ b/tests/ZfrRestTest/Resource/ResourceTest.php @@ -0,0 +1,89 @@ + + * @covers \ZfrRest\Resource\Resource + */ +class ResourceTest extends TestCase +{ + /** + * @dataProvider collectionResourceProvider + * + * @param string $className + * @param mixed $instance + * @param bool $isCollection + */ + public function testResource($className, $instance, $isCollection) + { + $metadata = $this->createMetadata(new ReflectionClass($className)); + $resource = new Resource($instance, $metadata); + + $this->assertSame($instance, $resource->getData()); + $this->assertSame($metadata, $resource->getMetadata()); + $this->assertSame($isCollection, $resource->isCollection()); + } + + public function testDisallowsInvalidResource() + { + $metadata = $this->createMetadata(new ReflectionClass($this)); + + $this->setExpectedException('ZfrRest\\Resource\\Exception\\InvalidResourceException'); + + new Resource(new \stdClass(), $metadata); + } + + /** + * Data provider for various collection types + * + * @return array + */ + public function collectionResourceProvider() + { + return array( + array('stdClass', $this->getMock('Iterator'), true), + array('stdClass', $this->getMock('Doctrine\\Common\\Collections\\Selectable'), true), + array('stdClass', $this->getMock('Doctrine\\Common\\Collections\\Collection'), true), + array('stdClass', array(), true), + array('stdClass', new \stdClass(), false), + ); + } + + /** + * @param ReflectionClass $reflectionClass + * @return \PHPUnit_Framework_MockObject_MockObject|\ZfrRest\Resource\Metadata\ResourceMetadataInterface + */ + private function createMetadata(ReflectionClass $reflectionClass) + { + $resourceMetadata = $this->getMock('ZfrRest\\Resource\\Metadata\\ResourceMetadataInterface'); + $metadata = $this->getMock('Doctrine\\Common\\Persistence\\Mapping\\ClassMetadata'); + + $resourceMetadata->expects($this->any())->method('getClassMetadata')->will($this->returnValue($metadata)); + $metadata->expects($this->any())->method('getReflectionClass')->will($this->returnValue($reflectionClass)); + + return $resourceMetadata; + } +} From d9b0a56080a1d961823ec4880a17691b2d02436d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Fri, 11 Oct 2013 16:09:11 +0200 Subject: [PATCH 03/19] Don't allow failure for 5.5 anymore --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 01e2d49..65d3ac9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,10 +6,6 @@ php: - 5.4 - 5.5 -matrix: - allow_failures: - - php: 5.5 - before_script: - composer self-update - composer update --prefer-source --dev From d9a5f21466b352bb70cd6c1c76935fd926e710bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Fri, 11 Oct 2013 19:51:18 +0200 Subject: [PATCH 04/19] Remove useless optimization --- .../Http/Matcher/AssociationSubPathMatcher.php | 2 +- .../Router/Http/Matcher/CollectionSubPathMatcher.php | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/ZfrRest/Mvc/Router/Http/Matcher/AssociationSubPathMatcher.php b/src/ZfrRest/Mvc/Router/Http/Matcher/AssociationSubPathMatcher.php index 05e20a0..d201d5e 100644 --- a/src/ZfrRest/Mvc/Router/Http/Matcher/AssociationSubPathMatcher.php +++ b/src/ZfrRest/Mvc/Router/Http/Matcher/AssociationSubPathMatcher.php @@ -51,7 +51,7 @@ public function matchSubPath( $data = $resource->getData(); $resourceMetadata = $resource->getMetadata(); - $pathChunks = explode('/', trim($subPath, '/'), 2); + $pathChunks = explode('/', trim($subPath, '/')); $associationName = array_shift($pathChunks); if (!$resourceMetadata->hasAssociation($associationName)) { diff --git a/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php b/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php index 3fd29f8..87ae032 100644 --- a/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php +++ b/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php @@ -70,7 +70,7 @@ public function matchSubPath( return new SubPathMatch($this->filterAssociation($resource, $request), $subPath); } - $pathChunks = explode('/', trim($subPath, '/'), 2); + $pathChunks = explode('/', trim($subPath, '/')); $identifier = array_shift($pathChunks); $classMetadata = $resource->getMetadata()->getClassMetadata(); $data = $this->findItem($resource->getData(), $classMetadata->getIdentifierFieldNames(), $identifier); @@ -81,7 +81,7 @@ public function matchSubPath( return new SubPathMatch( new Resource($data, $resource->getMetadata()), - array_shift($pathChunks), + $identifier, $previousMatch ); } @@ -143,17 +143,11 @@ protected function findItem($data, array $identifierNames, $identifier) * Filters the given resource by using the request object, then return the filtered subset * * @param ResourceInterface $resource - * @param HttpRequest $request + * @param HttpRequest $request * @return ResourceInterface */ protected function filterAssociation(ResourceInterface $resource, HttpRequest $request) { - $data = $resource->getData(); - - if (!$data instanceof Selectable) { - return $resource; - } - // Trigger an event to allow custom filtering $this->eventManager->trigger( CollectionFilteringEvent::EVENT_COLLECTION_FILTERING, From 75ff00e78bb80c14e8d2e775a3da4a57e0109c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Fri, 11 Oct 2013 22:03:57 +0200 Subject: [PATCH 05/19] Add tests for collection matcher --- config/module.config.php | 15 +- .../Factory/Exception/RuntimeException.php | 113 +++++++++++++ .../Factory/ResourceGraphRouteFactory.php | 82 +++++++++ .../Mvc/Router/Http/ResourceGraphRoute.php | 1 - tests/ZfrRestTest/Asset/TweetAsset.php | 8 + tests/ZfrRestTest/Asset/UserAsset.php | 3 +- .../Matcher/AssociationSubPathMatcherTest.php | 2 +- .../Matcher/CollectionSubPathMatcherTest.php | 159 ++++++++++++++++++ 8 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 src/ZfrRest/Factory/Exception/RuntimeException.php create mode 100644 src/ZfrRest/Factory/ResourceGraphRouteFactory.php create mode 100644 tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php diff --git a/config/module.config.php b/config/module.config.php index d1f3680..0b23aeb 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -17,5 +17,18 @@ */ return array( - 'service_manager' => array() + 'service_manager' => array(), + + /** + * ROUTE PLUGIN MANAGER + */ + 'route_manager' => array( + 'factories' => array( + 'ZfrRest\Mvc\Router\Http\ResourceGraphRoute' => 'ZfrRest\Factory\ResourceGraphRouteFactory' + ), + + 'aliases' => array( + 'ResourceGraphRoute' => 'ZfrRest\Mvc\Router\Http\ResourceGraphRoute' + ), + ), ); diff --git a/src/ZfrRest/Factory/Exception/RuntimeException.php b/src/ZfrRest/Factory/Exception/RuntimeException.php new file mode 100644 index 0000000..339612c --- /dev/null +++ b/src/ZfrRest/Factory/Exception/RuntimeException.php @@ -0,0 +1,113 @@ + + */ +class RuntimeException extends BaseRuntimeException implements ExceptionInterface +{ + /** + * @param ServiceLocatorInterface $serviceLocator + * + * @return self + */ + public static function pluginManagerExpected(ServiceLocatorInterface $serviceLocator) + { + return new self( + sprintf('A plugin manager was expected, but "%s" was given', get_class($serviceLocator)) + ); + } + + /** + * @param string $resourceName + * @param Exception|null $previous + * + * @return self + */ + public static function missingResource($resourceName, Exception $previous = null) + { + return new self(sprintf('Resource "%s" cannot be found in the service locator', $resourceName), 0, $previous); + } + + /** + * @param string $serviceName + * @param Exception|null $previous + * + * @return self + */ + public static function missingObjectManager($serviceName, Exception $previous = null) + { + return new self(sprintf('The object manager key is not valid, "%s" given', $serviceName), 0, $previous); + } + + /** + * @param string $serviceName + * @param mixed $objectManager + * + * @return self + */ + public static function invalidObjectManager($serviceName, $objectManager) + { + return new self( + sprintf( + 'Invalid ObjectManager retrieved for service "%s", instance of "%s" found', + $serviceName, + is_object($objectManager) ? get_class($objectManager) : gettype($objectManager) + ) + ); + } + + /** + * @param string $serviceName + * @param mixed $cache + * + * @return self + */ + public static function invalidCache($serviceName, $cache) + { + return new self( + sprintf( + 'Invalid CacheInterface retrieved for service "%s", instance of "%s" found', + $serviceName, + is_object($cache) ? get_class($cache) : gettype($cache) + ) + ); + } + + /** + * @param string $driverClass + * + * @return self + */ + public static function invalidDriverClass($driverClass) + { + return new self( + sprintf('Unrecognized driver class "%s" given', $driverClass) + ); + } +} diff --git a/src/ZfrRest/Factory/ResourceGraphRouteFactory.php b/src/ZfrRest/Factory/ResourceGraphRouteFactory.php new file mode 100644 index 0000000..aacbc39 --- /dev/null +++ b/src/ZfrRest/Factory/ResourceGraphRouteFactory.php @@ -0,0 +1,82 @@ +creationOptions = $creationOptions; + } + + /** + * {@inheritDoc} + * + * @return ResourceGraphRoute + * + * @throws RuntimeException + */ + public function createService(ServiceLocatorInterface $serviceLocator) + { + if (! $serviceLocator instanceof AbstractPluginManager) { + throw RuntimeException::pluginManagerExpected($serviceLocator); + } + + $parentLocator = $serviceLocator->getServiceLocator(); + + try { + $resource = $parentLocator->get($this->creationOptions['resource']); + } catch (ServiceNotFoundException $exception) { + throw RuntimeException::missingResource($this->creationOptions['resource'], $exception); + } + + /* @var $metadataFactory \Metadata\MetadataFactoryInterface */ + $metadataFactory = $parentLocator->get('ZfrRest\Resource\Metadata\MetadataFactory'); + + return new ResourceGraphRoute($metadataFactory, $resource, $this->creationOptions['route']); + } +} diff --git a/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php b/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php index 4fd0e8b..dddab97 100644 --- a/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php +++ b/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php @@ -32,7 +32,6 @@ use ZfrRest\Mvc\Exception; use ZfrRest\Mvc\Exception\RuntimeException; use ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher; -use ZfrRest\Paginator\ResourcePaginator; use ZfrRest\Resource\Resource; use ZfrRest\Resource\ResourceInterface; diff --git a/tests/ZfrRestTest/Asset/TweetAsset.php b/tests/ZfrRestTest/Asset/TweetAsset.php index a3b48c8..4c9fc65 100644 --- a/tests/ZfrRestTest/Asset/TweetAsset.php +++ b/tests/ZfrRestTest/Asset/TweetAsset.php @@ -30,6 +30,14 @@ class TweetAsset */ protected $content; + /** + * @param mixed $id + */ + public function __construct($id = null) + { + $this->id = $id; + } + /** * @return int */ diff --git a/tests/ZfrRestTest/Asset/UserAsset.php b/tests/ZfrRestTest/Asset/UserAsset.php index 56898a2..eae7b59 100644 --- a/tests/ZfrRestTest/Asset/UserAsset.php +++ b/tests/ZfrRestTest/Asset/UserAsset.php @@ -40,8 +40,9 @@ class UserAsset /** * Constructor */ - public function __construct() + public function __construct($id = null) { + $this->id = $id; $this->tweets = new ArrayCollection(); } diff --git a/tests/ZfrRestTest/Mvc/Router/Http/Matcher/AssociationSubPathMatcherTest.php b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/AssociationSubPathMatcherTest.php index e10655d..dfc459c 100644 --- a/tests/ZfrRestTest/Mvc/Router/Http/Matcher/AssociationSubPathMatcherTest.php +++ b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/AssociationSubPathMatcherTest.php @@ -27,7 +27,7 @@ /** * Tests for {@see \ZfrRest\Mvc\Router\Http\Matcher\AssociationSubPathMatcher} * - * @author Marco Pivetta + * @author Michaël Gallego * @covers \ZfrRest\Mvc\Router\Http\Matcher\AssociationSubPathMatcher */ class AssociationSubPathMatcherTest extends TestCase diff --git a/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php new file mode 100644 index 0000000..0afce5f --- /dev/null +++ b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php @@ -0,0 +1,159 @@ + + * @covers \ZfrRest\Mvc\Router\Http\Matcher\CollectionSubPathMatcher + */ +class CollectionSubPathMatcherTest extends TestCase +{ + public function testReturnNullIfResourceIsNotCollection() + { + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $resource->expects($this->once()) + ->method('isCollection') + ->will($this->returnValue(false)); + + $collectionPathMatcher = new CollectionSubPathMatcher(); + $this->assertNull($collectionPathMatcher->matchSubPath($resource, 'path', new HttpRequest())); + } + + public function testFilterCollectionIfPathIsEmpty() + { + $resourceData = new ArrayCollection(); + $resourceMetadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); + $httpRequest = new HttpRequest(); + + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $classMetadata->expects($this->once()) + ->method('getReflectionClass') + ->will($this->returnValue(new \ReflectionClass($resourceData))); + + $resourceMetadata->expects($this->once()) + ->method('getClassMetadata') + ->will($this->returnValue($classMetadata)); + + $resource = new Resource($resourceData, $resourceMetadata); + + $called = false; + $self = $this; + $eventManager = new EventManager(); + $eventManager->attach(CollectionFilteringEvent::EVENT_COLLECTION_FILTERING, function($event) use ($self, &$called) { + $called = true; + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\CollectionFilteringEvent', $event); + }); + + $collectionPathMatcher = new CollectionSubPathMatcher(); + $collectionPathMatcher->setEventManager($eventManager); + $result = $collectionPathMatcher->matchSubPath($resource, '', $httpRequest); + + $this->assertTrue($called); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', $result); + $this->assertNull($result->getPreviousMatch()); + $this->assertEmpty($result->getMatchedPath()); + $this->assertSame($resource, $result->getMatchedResource()); + } + + public function testCanMatchCollection() + { + $data = $this->getMock('Doctrine\Common\Collections\Selectable'); + $data->expects($this->any()) + ->method('matching') + ->will($this->returnValue(new ArrayCollection(array(new UserAsset(1))))); + + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $resource->expects($this->any()) + ->method('isCollection') + ->will($this->returnValue(true)); + + $resource->expects($this->any()) + ->method('getData') + ->will($this->returnValue($data)); + + $resourceMetadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); + + $resource->expects($this->any()) + ->method('getMetadata') + ->will($this->returnValue($resourceMetadata)); + + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $classMetadata->expects($this->any()) + ->method('getReflectionClass') + ->will($this->returnValue(new \ReflectionClass('ZfrRestTest\Asset\UserAsset'))); + + $classMetadata->expects($this->any()) + ->method('getIdentifierFieldNames') + ->will($this->returnValue(array('id'))); + + $resourceMetadata->expects($this->any()) + ->method('getClassMetadata') + ->will($this->returnValue($classMetadata)); + + $collectionPathMatcher = new CollectionSubPathMatcher(); + + // Simple path with trailing / + $result = $collectionPathMatcher->matchSubPath($resource, '/1', new HttpRequest()); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', $result); + $this->assertNull($result->getPreviousMatch()); + $this->assertEquals('1', $result->getMatchedPath()); + $this->assertInstanceOf('ZfrRestTest\Asset\UserAsset', $result->getMatchedResource()->getData()); + $this->assertEquals(1, $result->getMatchedResource()->getData()->getId()); + + // Simple path with ending / + $result = $collectionPathMatcher->matchSubPath($resource, '1/', new HttpRequest()); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', $result); + $this->assertNull($result->getPreviousMatch()); + $this->assertEquals('1', $result->getMatchedPath()); + $this->assertInstanceOf('ZfrRestTest\Asset\UserAsset', $result->getMatchedResource()->getData()); + $this->assertEquals(1, $result->getMatchedResource()->getData()->getId()); + + // Simple path with trailing and ending / + $result = $collectionPathMatcher->matchSubPath($resource, '/1/', new HttpRequest()); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', $result); + $this->assertNull($result->getPreviousMatch()); + $this->assertEquals('1', $result->getMatchedPath()); + $this->assertInstanceOf('ZfrRestTest\Asset\UserAsset', $result->getMatchedResource()->getData()); + $this->assertEquals(1, $result->getMatchedResource()->getData()->getId()); + + // Simple path with a previous submatch + $previousSubMatch = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', array(), array(), '', false); + $result = $collectionPathMatcher->matchSubPath($resource, '/1', new HttpRequest(), $previousSubMatch); + $this->assertSame($previousSubMatch, $result->getPreviousMatch()); + + // Simple path with a more complex path + $result = $collectionPathMatcher->matchSubPath($resource, '/1/tweets/5/retweets', new HttpRequest()); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', $result); + $this->assertNull($result->getPreviousMatch()); + $this->assertEquals('1', $result->getMatchedPath()); + $this->assertInstanceOf('ZfrRestTest\Asset\UserAsset', $result->getMatchedResource()->getData()); + $this->assertEquals(1, $result->getMatchedResource()->getData()->getId()); + } +} From b517cd1e210ae7531a21dff54d69472977a3886e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Fri, 11 Oct 2013 22:13:31 +0200 Subject: [PATCH 06/19] Add tests for SubPathMatch --- .../Router/Http/Matcher/SubPathMatchTest.php | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/ZfrRestTest/Mvc/Router/Http/Matcher/SubPathMatchTest.php diff --git a/tests/ZfrRestTest/Mvc/Router/Http/Matcher/SubPathMatchTest.php b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/SubPathMatchTest.php new file mode 100644 index 0000000..3883230 --- /dev/null +++ b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/SubPathMatchTest.php @@ -0,0 +1,43 @@ + + * @covers \ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch + */ +class SubPathMatchTest extends TestCase +{ + public function testSubPathMatch() + { + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $previousMatch = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', array(), array(), '', false); + + $subPathMatch = new SubPathMatch($resource, 'foo', $previousMatch); + + $this->assertSame($resource, $subPathMatch->getMatchedResource()); + $this->assertEquals('foo', $subPathMatch->getMatchedPath()); + $this->assertSame($previousMatch, $subPathMatch->getPreviousMatch()); + } +} From 4f76d5c6f026ec7b84c43b5fbc68a39ef7d8a728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Fri, 11 Oct 2013 22:17:17 +0200 Subject: [PATCH 07/19] Add more coverage for subpath matcher --- .../Matcher/CollectionSubPathMatcherTest.php | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php index 0afce5f..b1f4735 100644 --- a/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php +++ b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php @@ -156,4 +156,45 @@ public function testCanMatchCollection() $this->assertInstanceOf('ZfrRestTest\Asset\UserAsset', $result->getMatchedResource()->getData()); $this->assertEquals(1, $result->getMatchedResource()->getData()->getId()); } + + public function testReturnsNullIfNoItemIsFound() + { + $data = $this->getMock('Doctrine\Common\Collections\Selectable'); + $data->expects($this->any()) + ->method('matching') + ->will($this->returnValue(new ArrayCollection())); + + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $resource->expects($this->any()) + ->method('isCollection') + ->will($this->returnValue(true)); + + $resource->expects($this->any()) + ->method('getData') + ->will($this->returnValue($data)); + + $resourceMetadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); + + $resource->expects($this->any()) + ->method('getMetadata') + ->will($this->returnValue($resourceMetadata)); + + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $classMetadata->expects($this->any()) + ->method('getReflectionClass') + ->will($this->returnValue(new \ReflectionClass('ZfrRestTest\Asset\UserAsset'))); + + $classMetadata->expects($this->any()) + ->method('getIdentifierFieldNames') + ->will($this->returnValue(array('id'))); + + $resourceMetadata->expects($this->any()) + ->method('getClassMetadata') + ->will($this->returnValue($classMetadata)); + + $collectionPathMatcher = new CollectionSubPathMatcher(); + + $result = $collectionPathMatcher->matchSubPath($resource, '/1', new HttpRequest()); + $this->assertNull($result); + } } From 96b635d60855ee1c0f0756a31c84985474cbcfc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Fri, 11 Oct 2013 22:34:00 +0200 Subject: [PATCH 08/19] Add tests for BaseSubPathMatcher --- .../Http/Matcher/CollectionSubPathMatcher.php | 2 +- .../Http/Matcher/BaseSubPathMatcherTest.php | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/ZfrRestTest/Mvc/Router/Http/Matcher/BaseSubPathMatcherTest.php diff --git a/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php b/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php index 87ae032..51b714d 100644 --- a/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php +++ b/src/ZfrRest/Mvc/Router/Http/Matcher/CollectionSubPathMatcher.php @@ -70,7 +70,7 @@ public function matchSubPath( return new SubPathMatch($this->filterAssociation($resource, $request), $subPath); } - $pathChunks = explode('/', trim($subPath, '/')); + $pathChunks = explode('/', $path); $identifier = array_shift($pathChunks); $classMetadata = $resource->getMetadata()->getClassMetadata(); $data = $this->findItem($resource->getData(), $classMetadata->getIdentifierFieldNames(), $identifier); diff --git a/tests/ZfrRestTest/Mvc/Router/Http/Matcher/BaseSubPathMatcherTest.php b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/BaseSubPathMatcherTest.php new file mode 100644 index 0000000..13037a8 --- /dev/null +++ b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/BaseSubPathMatcherTest.php @@ -0,0 +1,79 @@ + + * @covers \ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher + */ +class BaseSubPathMatcherTest extends TestCase +{ + /** + * @var \ZfrRest\Mvc\Router\Http\Matcher\CollectionSubPathMatcher + */ + protected $collectionMatcher; + + /** + * @var \ZfrRest\Mvc\Router\Http\Matcher\AssociationSubPathMatcher + */ + protected $associationMatcher; + + /** + * @var \ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher + */ + protected $baseSubPathMatcher; + + public function setUp() + { + $this->collectionMatcher = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\CollectionSubPathMatcher', array(), array(), '', false); + $this->associationMatcher = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\AssociationSubPathMatcher', array(), array(), '', false); + $this->baseSubPathMatcher = new BaseSubPathMatcher($this->collectionMatcher, $this->associationMatcher); + } + + public function testGetSubMatchIfPathIsEmpty() + { + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $result = $this->baseSubPathMatcher->matchSubPath($resource, '/', new HttpRequest()); + + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\SubPathMatch', $result); + $this->assertEquals('/', $result->getMatchedPath()); + $this->assertSame($resource, $result->getMatchedResource()); + } + + public function testReturnsNullIfNotMatched() + { + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $resource->expects($this->once()) + ->method('isCollection') + ->will($this->returnValue(true)); + + $this->collectionMatcher->expects($this->once()) + ->method('matchSubPath') + ->will($this->returnValue(null)); + + $this->assertNull($this->baseSubPathMatcher->matchSubPath($resource, 'foo', new HttpRequest())); + } + +} From bced324b41fe7876268cad732cfa561ba3c80ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Mon, 14 Oct 2013 17:13:59 +0200 Subject: [PATCH 09/19] Add tests for ResourceGraphRoute --- config/module.config.php | 11 +- .../Factory/BaseSubPathMatcherFactory.php | 41 ++++ .../Factory/ResourceGraphRouteFactory.php | 6 +- .../Mvc/Exception/RuntimeException.php | 17 +- .../Mvc/Router/Http/ResourceGraphRoute.php | 38 ++-- .../Factory/BaseSubPathMatcherFactoryTest.php | 43 ++++ .../Router/Http/ResourceGraphRouteTest.php | 204 ++++++++++++++++++ 7 files changed, 337 insertions(+), 23 deletions(-) create mode 100644 src/ZfrRest/Factory/BaseSubPathMatcherFactory.php create mode 100644 tests/ZfrRestTest/Factory/BaseSubPathMatcherFactoryTest.php create mode 100644 tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php diff --git a/config/module.config.php b/config/module.config.php index 0b23aeb..1210d0c 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -17,7 +17,16 @@ */ return array( - 'service_manager' => array(), + 'service_manager' => array( + 'factories' => array( + 'ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher' => 'ZfrRest\Factory\BaseSubPathMatcherFactory' + ), + + 'invokables' => array( + 'ZfrRest\Mvc\Router\Http\Matcher\AssociationSubPathMatcher' => 'ZfrRest\Mvc\Router\Http\Matcher\AssociationSubPathMatcher', + 'ZfrRest\Mvc\Router\Http\Matcher\CollectionSubPathMatcher' => 'ZfrRest\Mvc\Router\Http\Matcher\CollectionSubPathMatcher', + ) + ), /** * ROUTE PLUGIN MANAGER diff --git a/src/ZfrRest/Factory/BaseSubPathMatcherFactory.php b/src/ZfrRest/Factory/BaseSubPathMatcherFactory.php new file mode 100644 index 0000000..1ebfce6 --- /dev/null +++ b/src/ZfrRest/Factory/BaseSubPathMatcherFactory.php @@ -0,0 +1,41 @@ + + */ +class BaseSubPathMatcherFactory implements FactoryInterface +{ + /** + * {@inheritDoc} + */ + public function createService(ServiceLocatorInterface $serviceLocator) + { + return new BaseSubPathMatcher( + $serviceLocator->get('ZfrRest\Mvc\Router\Http\Matcher\CollectionSubPathMatcher'), + $serviceLocator->get('ZfrRest\Mvc\Router\Http\Matcher\AssociationSubPathMatcher') + ); + } +} diff --git a/src/ZfrRest/Factory/ResourceGraphRouteFactory.php b/src/ZfrRest/Factory/ResourceGraphRouteFactory.php index aacbc39..d4f7856 100644 --- a/src/ZfrRest/Factory/ResourceGraphRouteFactory.php +++ b/src/ZfrRest/Factory/ResourceGraphRouteFactory.php @@ -62,7 +62,7 @@ public function setCreationOptions(array $creationOptions) */ public function createService(ServiceLocatorInterface $serviceLocator) { - if (! $serviceLocator instanceof AbstractPluginManager) { + if (!$serviceLocator instanceof AbstractPluginManager) { throw RuntimeException::pluginManagerExpected($serviceLocator); } @@ -74,9 +74,9 @@ public function createService(ServiceLocatorInterface $serviceLocator) throw RuntimeException::missingResource($this->creationOptions['resource'], $exception); } - /* @var $metadataFactory \Metadata\MetadataFactoryInterface */ $metadataFactory = $parentLocator->get('ZfrRest\Resource\Metadata\MetadataFactory'); + $matcher = $parentLocator->get('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher'); - return new ResourceGraphRoute($metadataFactory, $resource, $this->creationOptions['route']); + return new ResourceGraphRoute($metadataFactory, $matcher, $resource, $this->creationOptions['route']); } } diff --git a/src/ZfrRest/Mvc/Exception/RuntimeException.php b/src/ZfrRest/Mvc/Exception/RuntimeException.php index 088a604..10d2a42 100755 --- a/src/ZfrRest/Mvc/Exception/RuntimeException.php +++ b/src/ZfrRest/Mvc/Exception/RuntimeException.php @@ -32,7 +32,6 @@ class RuntimeException extends BaseRuntimeException implements ExceptionInterfac { /** * @param ClassMetadata $classMetadata - * * @return self */ public static function missingCollectionMetadata(ClassMetadata $classMetadata) @@ -45,9 +44,23 @@ public static function missingCollectionMetadata(ClassMetadata $classMetadata) ); } + /** + * @param mixed $data + * @return self + */ + public static function noValidPaginatorAdapterFound($data) + { + return new self( + sprintf( + 'No paginator adapter can be matched for the data of type "%s"', + is_object($data) ? get_class($data) : gettype($data) + ) + ); + } + + /** * @param mixed $resource - * * @return self */ public static function unsupportedResourceType($resource) diff --git a/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php b/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php index dddab97..020eb95 100644 --- a/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php +++ b/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php @@ -19,7 +19,6 @@ namespace ZfrRest\Mvc\Router\Http; use Doctrine\Common\Collections\Collection; -use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Selectable; use Doctrine\Common\Persistence\ObjectRepository; use DoctrineModule\Paginator\Adapter\Selectable as SelectableAdapter; @@ -27,10 +26,10 @@ use Metadata\MetadataFactoryInterface; use Zend\Mvc\Router\Http\RouteInterface; use Zend\Mvc\Router\Http\RouteMatch; +use Zend\Paginator\Paginator; use Zend\Stdlib\RequestInterface as Request; use Zend\Http\Request as HttpRequest; use ZfrRest\Mvc\Exception; -use ZfrRest\Mvc\Exception\RuntimeException; use ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher; use ZfrRest\Resource\Resource; use ZfrRest\Resource\ResourceInterface; @@ -125,16 +124,15 @@ public function match(Request $request) $subPath = substr($path, strlen($this->route)); - if (! $match = $this->subPathMatcher->matchSubPath($this->getResource(), $subPath, $request)) { + if (!$match = $this->subPathMatcher->matchSubPath($this->getResource(), $subPath, $request)) { return null; } - return $this->buildRouteMatch($match->matchedResource, $path); + return $this->buildRouteMatch($match->getMatchedResource(), $path); } /** - * Build a route match. This function extract the controller from the resource metadata, and does - * optional filtering by query + * Build a route match * * @param ResourceInterface $resource * @param string $path @@ -143,23 +141,30 @@ public function match(Request $request) */ protected function buildRouteMatch(ResourceInterface $resource, $path) { - $metadata = $resource->getMetadata(); - $collectionMetadata = $metadata->getCollectionMetadata(); - $classMetadata = $metadata->getClassMetadata(); - $data = $resource->getData(); + $metadata = $resource->getMetadata(); + $classMetadata = $metadata->getClassMetadata(); + $data = $resource->getData(); // If returned $data is a collection, then we use the controller specified in Collection mapping if ($resource->isCollection()) { - if (null === $collectionMetadata) { + if (!$collectionMetadata = $metadata->getCollectionMetadata()) { throw Exception\RuntimeException::missingCollectionMetadata($classMetadata); } - if ($data instanceof Collection) { - $resource = new Resource(new ResourcePaginator($metadata, new CollectionAdapter($data)), $metadata); - } elseif ($data instanceof Selectable) { - $resource = new Resource(new ResourcePaginator($metadata, new SelectableAdapter($data)), $metadata); + // We wrap the data around a paginator + $paginatorAdapter = null; + + if ($data instanceof Selectable) { + $paginatorAdapter = new SelectableAdapter($data); + } elseif ($data instanceof Collection) { + $paginatorAdapter = new CollectionAdapter($data); + } + + if (null === $paginatorAdapter) { + throw Exception\RuntimeException::noValidPaginatorAdapterFound($data); } + $resource = new Resource(new Paginator($paginatorAdapter), $metadata); $controllerName = $collectionMetadata->getControllerName(); } else { $controllerName = $metadata->getControllerName(); @@ -180,7 +185,6 @@ protected function buildRouteMatch(ResourceInterface $resource, $path) * the resource AND metadata associated to it. This metadata is usually extracted from the entity name * * @throws Exception\RuntimeException - * * @return ResourceInterface */ private function getResource() @@ -199,7 +203,7 @@ private function getResource() } elseif (is_string($resource)) { $metadata = $this->metadataFactory->getMetadataForClass($resource); } else { - throw RuntimeException::unsupportedResourceType($resource); + throw Exception\RuntimeException::unsupportedResourceType($resource); } return $this->resource = new Resource($resource, $metadata->getOutsideClassMetadata()); diff --git a/tests/ZfrRestTest/Factory/BaseSubPathMatcherFactoryTest.php b/tests/ZfrRestTest/Factory/BaseSubPathMatcherFactoryTest.php new file mode 100644 index 0000000..ae320f1 --- /dev/null +++ b/tests/ZfrRestTest/Factory/BaseSubPathMatcherFactoryTest.php @@ -0,0 +1,43 @@ + + * @covers \ZfrRest\Factory\BaseSubPathMatcherFactory + */ +class BaseSubPathMatcherFactoryTest extends TestCase +{ + public function testCanCreateFromFactory() + { + $serviceManager = ServiceManagerFactory::getServiceManager(); + + $object = $serviceManager->get('ZfrRest\Mvc\Router\Http\Matcher\CollectionSubPathMatcher'); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\CollectionSubPathMatcher', $object); + + $object = $serviceManager->get('ZfrRest\Mvc\Router\Http\Matcher\AssociationSubPathMatcher'); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\AssociationSubPathMatcher', $object); + + $object = $serviceManager->get('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher'); + $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', $object); + } +} diff --git a/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php b/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php new file mode 100644 index 0000000..5c3da83 --- /dev/null +++ b/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php @@ -0,0 +1,204 @@ + + * @covers \ZfrRest\Mvc\Router\Http\ResourceGraphRoute + */ +class ResourceGraphRouteTest extends TestCase +{ + public function testReturnsNullIfNotHttpRequest() + { + $resourceGraphRoute = new ResourceGraphRoute( + $this->getMock('Metadata\MetadataFactoryInterface'), + $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', array(), array(), '', false), + $this->getMock('ZfrRest\Resource\ResourceInterface'), + 'route' + ); + + $request = $this->getMock('Zend\Stdlib\RequestInterface'); + + $this->assertNull($resourceGraphRoute->match($request)); + } + + public function testReturnsNullIfUriPathIsNotInRouteParameter() + { + $resourceGraphRoute = new ResourceGraphRoute( + $this->getMock('Metadata\MetadataFactoryInterface'), + $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', array(), array(), '', false), + $this->getMock('ZfrRest\Resource\ResourceInterface'), + 'route' + ); + + $request = new HttpRequest(); + $request->setUri('http://www.example.com/bar'); + + $this->assertNull($resourceGraphRoute->match($request)); + } + + public function testReturnsNullIfCannotFindMatch() + { + $matcher = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', array(), array(), '', false); + + $resourceGraphRoute = new ResourceGraphRoute( + $this->getMock('Metadata\MetadataFactoryInterface'), + $matcher, + $this->getMock('ZfrRest\Resource\ResourceInterface'), + 'route' + ); + + $matcher->expects($this->once()) + ->method('matchSubPath') + ->will($this->returnValue(null)); + + $request = new HttpRequest(); + $request->setUri('http://www.example.com/route'); + + $this->assertNull($resourceGraphRoute->match($request)); + } + + public function testCanBuildSimpleRouteMatch() + { + $matcher = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', array(), array(), '', false); + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + + $resourceGraphRoute = new ResourceGraphRoute( + $this->getMock('Metadata\MetadataFactoryInterface'), + $matcher, + $resource, + 'route' + ); + + $match = new SubPathMatch($resource, 'matchedPath'); + + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + + $resourceMetadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); + $resourceMetadata->expects($this->once()) + ->method('getClassMetadata') + ->will($this->returnValue($classMetadata)); + + $resourceMetadata->expects($this->once()) + ->method('getControllerName') + ->will($this->returnValue('MyController')); + + $resource->expects($this->once()) + ->method('getMetadata') + ->will($this->returnValue($resourceMetadata)); + + $resource->expects($this->once()) + ->method('isCollection') + ->will($this->returnValue(false)); + + $matcher->expects($this->once()) + ->method('matchSubPath') + ->will($this->returnValue($match)); + + $request = new HttpRequest(); + $request->setUri('http://www.example.com/route'); + + $routeMatch = $resourceGraphRoute->match($request); + + $this->assertInstanceOf('Zend\Mvc\Router\Http\RouteMatch', $routeMatch); + $this->assertEquals(strlen('/route'), $routeMatch->getLength()); + $this->assertSame($resource, $routeMatch->getParam('resource')); + $this->assertEquals('MyController', $routeMatch->getParam('controller')); + } + + public function dataTypeForPaginator() + { + return array( + array('Doctrine\Common\Collections\Selectable', 'DoctrineModule\Paginator\Adapter\Selectable'), + array('Doctrine\Common\Collections\Collection', 'DoctrineModule\Paginator\Adapter\Collection') + ); + } + + /** + * @dataProvider dataTypeForPaginator + */ + public function testCanBuildRouteMatchForCollectionAndWrapDataInsidePaginator($dataType, $adapterType) + { + $matcher = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', array(), array(), '', false); + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + + $resourceGraphRoute = new ResourceGraphRoute( + $this->getMock('Metadata\MetadataFactoryInterface'), + $matcher, + $resource, + 'route' + ); + + $match = new SubPathMatch($resource, 'matchedPath'); + + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + + $resourceMetadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); + $resourceMetadata->expects($this->exactly(2)) + ->method('getClassMetadata') + ->will($this->returnValue($classMetadata)); + + $resourceMetadata->expects($this->never()) + ->method('getControllerName') + ->will($this->returnValue('MyController')); + + $collectionResourceMetadata = $this->getMock('ZfrRest\Resource\Metadata\CollectionResourceMetadataInterface'); + + $collectionResourceMetadata->expects($this->once()) + ->method('getControllerName') + ->will($this->returnValue('MyCollectionController')); + + $resourceMetadata->expects($this->once()) + ->method('getCollectionMetadata') + ->will($this->returnValue($collectionResourceMetadata)); + + $resource->expects($this->once()) + ->method('getMetadata') + ->will($this->returnValue($resourceMetadata)); + + $resource->expects($this->once()) + ->method('isCollection') + ->will($this->returnValue(true)); + + $resource->expects($this->once()) + ->method('getData') + ->will($this->returnValue($this->getMock($dataType))); + + $matcher->expects($this->once()) + ->method('matchSubPath') + ->will($this->returnValue($match)); + + $request = new HttpRequest(); + $request->setUri('http://www.example.com/route'); + + $routeMatch = $resourceGraphRoute->match($request); + + $this->assertInstanceOf('Zend\Mvc\Router\Http\RouteMatch', $routeMatch); + $this->assertEquals(strlen('/route'), $routeMatch->getLength()); + $this->assertInstanceOf('ZfrRest\Resource\ResourceInterface', $routeMatch->getParam('resource')); + $this->assertNotSame($resource, $routeMatch->getParam('resource')); + $this->assertInstanceOf('Zend\Paginator\Paginator', $routeMatch->getParam('resource')->getData()); + $this->assertInstanceOf($adapterType, $routeMatch->getParam('resource')->getData()->getAdapter()); + $this->assertEquals('MyCollectionController', $routeMatch->getParam('controller')); + } +} From 150c52e09a4752d051e9f9cebb615e3ab4900064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Mon, 14 Oct 2013 17:52:52 +0200 Subject: [PATCH 10/19] Add support for base path and tests --- .../Mvc/Router/Http/ResourceGraphRoute.php | 12 ++++-- tests/ZfrRestTest/Asset/Request.php | 36 ++++++++++++++++ .../Router/Http/ResourceGraphRouteTest.php | 42 ++++++++++++++++++- 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 tests/ZfrRestTest/Asset/Request.php diff --git a/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php b/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php index 020eb95..6a52a8a 100644 --- a/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php +++ b/src/ZfrRest/Mvc/Router/Http/ResourceGraphRoute.php @@ -78,7 +78,7 @@ public function __construct( $this->metadataFactory = $metadataFactory; $this->subPathMatcher = $matcher; $this->resource = $resource; - $this->route = $route; + $this->route = trim($route, '/'); } /** @@ -117,8 +117,14 @@ public function match(Request $request) $uri = $request->getUri(); $path = $uri->getPath(); - // If the route is not even contained within the URI, we can return early... - if (strpos($path, $this->route) === false) { + // We must omit the basePath + if (method_exists($request, 'getBaseUrl') && $baseUrl = $request->getBaseUrl()) { + $path = substr($path, strlen(rtrim($baseUrl, '/'))); + } + + // If the URI does not begin by the route, we can stop immediately + + if (substr(ltrim($path, '/'), 0, strlen($this->route)) !== $this->route) { return null; } diff --git a/tests/ZfrRestTest/Asset/Request.php b/tests/ZfrRestTest/Asset/Request.php new file mode 100644 index 0000000..8083d07 --- /dev/null +++ b/tests/ZfrRestTest/Asset/Request.php @@ -0,0 +1,36 @@ +baseUrl = $baseUrl; + return $this; + } + + /** + * @return string + */ + public function getBaseUrl() + { + return $this->baseUrl; + } +} diff --git a/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php b/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php index 5c3da83..91e5ceb 100644 --- a/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php +++ b/tests/ZfrRestTest/Mvc/Router/Http/ResourceGraphRouteTest.php @@ -30,13 +30,18 @@ class ResourceGraphRouteTest extends TestCase { public function testReturnsNullIfNotHttpRequest() { + $matcher = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', array(), array(), '', false); + $resourceGraphRoute = new ResourceGraphRoute( $this->getMock('Metadata\MetadataFactoryInterface'), - $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', array(), array(), '', false), + $matcher, $this->getMock('ZfrRest\Resource\ResourceInterface'), 'route' ); + $matcher->expects($this->never()) + ->method('matchSubPath'); + $request = $this->getMock('Zend\Stdlib\RequestInterface'); $this->assertNull($resourceGraphRoute->match($request)); @@ -44,16 +49,49 @@ public function testReturnsNullIfNotHttpRequest() public function testReturnsNullIfUriPathIsNotInRouteParameter() { + $matcher = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', array(), array(), '', false); + $resourceGraphRoute = new ResourceGraphRoute( $this->getMock('Metadata\MetadataFactoryInterface'), - $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', array(), array(), '', false), + $matcher, $this->getMock('ZfrRest\Resource\ResourceInterface'), 'route' ); + $matcher->expects($this->never()) + ->method('matchSubPath'); + $request = new HttpRequest(); $request->setUri('http://www.example.com/bar'); + $this->assertNull($resourceGraphRoute->match($request)); + + // It must also returns null if the "route" param is after in the URI + $request->setUri('http://www.example.com/bar/route'); + $this->assertNull($resourceGraphRoute->match($request)); + } + + /** + * Test if route can match when there is a baseUrl for the application + * @covers \ZfrRest\Mvc\Router\Http\ResourceGraphRoute::match + */ + public function testMatchWithBaseUrl() + { + $matcher = $this->getMock('ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher', array(), array(), '', false); + + $resourceGraphRoute = new ResourceGraphRoute( + $this->getMock('Metadata\MetadataFactoryInterface'), + $matcher, + $this->getMock('ZfrRest\Resource\ResourceInterface'), + '/foo/bar' + ); + + $matcher->expects($this->never()) + ->method('matchSubPath'); + + $request = new \ZfrRestTest\Asset\Request(); + $request->setBaseUrl('/base/'); + $request->setUri('/foo/bar'); $this->assertNull($resourceGraphRoute->match($request)); } From 8f6d4c13ffb8d9fb30c19699c716bde13c4c4270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Mon, 14 Oct 2013 17:56:33 +0200 Subject: [PATCH 11/19] Small CS fix --- src/ZfrRest/Mvc/Exception/RuntimeException.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ZfrRest/Mvc/Exception/RuntimeException.php b/src/ZfrRest/Mvc/Exception/RuntimeException.php index 10d2a42..8de6b55 100755 --- a/src/ZfrRest/Mvc/Exception/RuntimeException.php +++ b/src/ZfrRest/Mvc/Exception/RuntimeException.php @@ -58,7 +58,6 @@ public static function noValidPaginatorAdapterFound($data) ); } - /** * @param mixed $resource * @return self From 8cfbf358e4b8c917c0830a20aa34cf1b20b63366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Mon, 14 Oct 2013 21:11:32 +0200 Subject: [PATCH 12/19] Fix tests on PHP 5.3 --- .travis.yml | 2 +- .../Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 65d3ac9..6f33b2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ before_script: script: - ./vendor/bin/phpunit --coverage-clover ./build/logs/clover.xml --exclude-group Performance - ./vendor/bin/phpunit --group=Functional - - ./vendor/bin/phpcs --standard=PSR2 ./src/ ./tests/ + - ./vendor/bin/phpcs --standard=PSR2 ./src/ after_script: - php vendor/bin/coveralls -v diff --git a/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php index b1f4735..80d0956 100644 --- a/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php +++ b/tests/ZfrRestTest/Mvc/Router/Http/Matcher/CollectionSubPathMatcherTest.php @@ -68,7 +68,7 @@ public function testFilterCollectionIfPathIsEmpty() $eventManager = new EventManager(); $eventManager->attach(CollectionFilteringEvent::EVENT_COLLECTION_FILTERING, function($event) use ($self, &$called) { $called = true; - $this->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\CollectionFilteringEvent', $event); + $self->assertInstanceOf('ZfrRest\Mvc\Router\Http\Matcher\CollectionFilteringEvent', $event); }); $collectionPathMatcher = new CollectionSubPathMatcher(); From 4ed4b7549cf76dffd7be7352141f5860942ec7af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Tue, 15 Oct 2013 12:46:33 +0200 Subject: [PATCH 13/19] Add metadata --- config/module.config.php | 12 +- config/zfr_rest.global.php.dist | 106 ++-------- src/ZfrRest/Factory/ModuleOptionsFactory.php | 39 ++++ .../ResourceMetadataFactoryFactory.php | 80 ++++++++ src/ZfrRest/Options/DriverOptions.php | 76 +++++++ src/ZfrRest/Options/ModuleOptions.php | 106 ++++++++++ .../Annotation/AnnotationInterface.php | 40 ++++ .../Metadata/Annotation/Collection.php | 61 ++++++ .../Metadata/Annotation/ExposeAssociation.php | 42 ++++ .../Resource/Metadata/Annotation/Resource.php | 61 ++++++ .../Metadata/Driver/AnnotationDriver.php | 192 ++++++++++++++++++ .../Factory/ModuleOptionsFactoryTest.php | 43 ++++ .../ResourceMetadataFactoryFactoryTest.php | 40 ++++ .../Driver/AnnotationAsset/Address.php | 24 +++ .../Metadata/Driver/AnnotationAsset/City.php | 25 +++ .../AnnotationAsset/PointOfInterest.php | 25 +++ .../Driver/AnnotationDriverFunctionalTest.php | 41 ++++ tests/testing.config.php | 46 ++--- 18 files changed, 940 insertions(+), 119 deletions(-) create mode 100644 src/ZfrRest/Factory/ModuleOptionsFactory.php create mode 100644 src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php create mode 100644 src/ZfrRest/Options/DriverOptions.php create mode 100644 src/ZfrRest/Options/ModuleOptions.php create mode 100755 src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php create mode 100755 src/ZfrRest/Resource/Metadata/Annotation/Collection.php create mode 100755 src/ZfrRest/Resource/Metadata/Annotation/ExposeAssociation.php create mode 100755 src/ZfrRest/Resource/Metadata/Annotation/Resource.php create mode 100755 src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php create mode 100644 tests/ZfrRestTest/Factory/ModuleOptionsFactoryTest.php create mode 100644 tests/ZfrRestTest/Factory/ResourceMetadataFactoryFactoryTest.php create mode 100644 tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Address.php create mode 100644 tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/City.php create mode 100644 tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/PointOfInterest.php create mode 100644 tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverFunctionalTest.php diff --git a/config/module.config.php b/config/module.config.php index 1210d0c..5c3b4cc 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -17,9 +17,14 @@ */ return array( + /** + * SERVICE MANAGER CONFIG + */ 'service_manager' => array( 'factories' => array( - 'ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher' => 'ZfrRest\Factory\BaseSubPathMatcherFactory' + 'ZfrRest\Mvc\Router\Http\Matcher\BaseSubPathMatcher' => 'ZfrRest\Factory\BaseSubPathMatcherFactory', + 'ZfrRest\Options\ModuleOptions' => 'ZfrRest\Factory\ModuleOptionsFactory', + 'ZfrRest\Resource\Metadata\ResourceMetadataFactory' => 'ZfrRest\Factory\ResourceMetadataFactoryFactory' ), 'invokables' => array( @@ -40,4 +45,9 @@ 'ResourceGraphRoute' => 'ZfrRest\Mvc\Router\Http\ResourceGraphRoute' ), ), + + /** + * ZFR REST CONFIG + */ + 'zfr_rest' => array() ); diff --git a/config/zfr_rest.global.php.dist b/config/zfr_rest.global.php.dist index 0c0f2d5..9129a77 100644 --- a/config/zfr_rest.global.php.dist +++ b/config/zfr_rest.global.php.dist @@ -14,102 +14,22 @@ return array( 'object_manager' => 'doctrine.entitymanager.orm_default', /** - * Listeners options + * FQCN of the Metadata cache provider used for caching the mapping. It must returns an instance of + * \Metadata\Cache\CacheInterface */ - 'listeners' => array( - /** - * If this listener is registered (it is by default), then any exceptions that could be thrown - * in controller is serialized to the proper format extracted from Content-Type header. - */ - // 'register_http_exception' => true, - - /** - * If this listener is registered (it is not by default), it will check if the request contains - * a header "X-HTTP-Method-Override". This header allows to change the HTTP verb. This is useful in - * some contexts (for instance, some companies' proxies only allow GET and POST methods) - */ - // 'register_http_method_override' => false, - - /** - * If this listener is registered, it allows you to return an object (an entity, for instance) from a - * controller's action, and automatically create payload from its content. Please note that if you deactivate - * this listener, you MUST NOT return a resource (entity...) from your controller, but an array or Model - */ - // 'register_create_resource_payload' => true, - - /** - * If this listener is registered (it is by default), please note that if you deactivate this listener, - * then you need to return a concrete ModelInterface object (ViewModel, JsonModel...) from your actions, - * or manually use the AcceptableViewModelSelector to return the right model according to Content-Type - */ - // 'register_select_model' => true, - ), - - /** - * Which behaviours each controller should automatically do for us? - */ - 'controller_behaviours' => array( - /** - * If this is set to true (it is by default), then controller will automatically instantiate the - * input filter specified in resource metadata (if there is one) - from service locator first, or - * directly instantiate it if not found -, and validate data. If data is incorrect, it will return - * a 400 HTTP error (Bad Request) with the failed validation messages in it). - */ - // 'auto_validate' => true, - - /** - * If this is set to true (it is by default), then controller will automatically instantiate the hydrator - * specified in resource metadata (if there is one) - from service locator first, or directly instantiate - * it if not found - and hydrate resource object with previously validated data. - * - * Please note that for security reason, if you activate this option, you SHOULD NOT deactivate auto - * validation UNLESS you validate data yourself. - */ - // 'auto_hydrate' => true - ), - - /** - * Resource metadata options - */ - 'resource_metadata' => array( - /** - * Set the FQCN of the Doctrine cache provider to use. For performance reasons, you should ALWAYS - * set an efficient cache that is persistent across requests (like APC or Memcached caches), but for - * development purposes you can keep the default array cache - */ - // 'cache' => 'Doctrine\Common\Cache\ArrayCache', - - /** - * Specify the resource metadata drivers. This is used by ZfrRest to extract your mapping. Currently two - * methods are supported: PHP files and annotations. In order to keep your code clean, we recommand you - * to add your drivers into each module config. - * - * To add a annotation driver: - * - * 'application_driver' => array( - * 'class' => 'ZfrRest\Resource\Metadata\Driver\AnnotationDriver', - * 'paths' => array(__DIR__ . '/../src/Application/Entity') - * ) - * - * To add a PHP driver: - * - * 'application_driver' => array( - * 'class' => 'ZfrRest\Resource\Metadata\Driver\PhpDriver', - * 'paths' => array(__DIR__ . '/../src/Application/Entity') - * ) - */ - // 'drivers' => array() - ), - - /** - * This configuration allow to add classes to the DecoderPluginManager - */ - 'decoders' => array(), + 'cache' => '', /** - * This configuration allow to add classes to the ModelPluginManager. This works like any other - * plugin manager + * + * Specify the resource metadata drivers. This is used by ZfrRest to extract your mapping. Currently one + * method is supported: annotations. + * + * To add an annotation driver: + * + * 'application_driver' => array( + * 'class' => 'ZfrRest\Resource\Metadata\Driver\AnnotationDriver' + * ) */ - 'models' => array() + 'drivers' => array(), ) ); diff --git a/src/ZfrRest/Factory/ModuleOptionsFactory.php b/src/ZfrRest/Factory/ModuleOptionsFactory.php new file mode 100644 index 0000000..8c8a8e6 --- /dev/null +++ b/src/ZfrRest/Factory/ModuleOptionsFactory.php @@ -0,0 +1,39 @@ + + */ +class ModuleOptionsFactory implements FactoryInterface +{ + /** + * {@inheritDoc} + */ + public function createService(ServiceLocatorInterface $serviceLocator) + { + $config = $serviceLocator->get('Config'); + return new ModuleOptions($config['zfr_rest']); + } +} diff --git a/src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php b/src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php new file mode 100644 index 0000000..020b8cd --- /dev/null +++ b/src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php @@ -0,0 +1,80 @@ + + * @licence MIT + */ +class ResourceMetadataFactoryFactory implements FactoryInterface +{ + /** + * {@inheritDoc} + */ + public function createService(ServiceLocatorInterface $serviceLocator) + { + /** @var $moduleOptions \ZfrRest\Options\ModuleOptions $moduleOptions */ + $moduleOptions = $serviceLocator->get('ZfrRest\Options\ModuleOptions'); + + $objectManager = $moduleOptions->getObjectManager(); + if (!$serviceLocator->has($objectManager)) { + throw Exception\RuntimeException::missingObjectManager($objectManager); + } + + /** @var \Doctrine\Common\Persistence\ObjectManager $objectManager */ + $objectManager = $serviceLocator->get($objectManager); + $doctrineMetadataFactory = $objectManager->getMetadataFactory(); + + $driverChain = new DriverChain(); + $resourceMetadataFactory = new MetadataFactory($driverChain); + + $drivers = $moduleOptions->getDrivers(); + foreach ($drivers as $driverOptions) { + $driver = null; + + switch($driverOptions->getClass()) { + case 'ZfrRest\Resource\Metadata\Driver\AnnotationDriver': + $driver = new AnnotationDriver( + new AnnotationReader(), $resourceMetadataFactory, $doctrineMetadataFactory + ); + break; + default: + throw Exception\RuntimeException::invalidDriverClass($driverOptions->getClass()); + } + + $driverChain->addDriver($driver); + } + + if ($moduleOptions->getCache()) { + $resourceMetadataFactory->setCache($serviceLocator->get($moduleOptions->getCache())); + } + + return $resourceMetadataFactory; + } +} diff --git a/src/ZfrRest/Options/DriverOptions.php b/src/ZfrRest/Options/DriverOptions.php new file mode 100644 index 0000000..47445d3 --- /dev/null +++ b/src/ZfrRest/Options/DriverOptions.php @@ -0,0 +1,76 @@ + + * @licence MIT + */ +class DriverOptions extends AbstractOptions +{ + /** + * FQCN of the driver class + * + * @var string + */ + protected $class; + + /** + * Paths (only for drivers that support it) + * + * @var array + */ + protected $paths = array(); + + /** + * @param string $class + * @return void + */ + public function setClass($class) + { + $this->class = $class; + } + + /** + * @return string + */ + public function getClass() + { + return $this->class; + } + + /** + * @param array $paths + * @return void + */ + public function setPaths(array $paths) + { + $this->paths = $paths; + } + + /** + * @return array + */ + public function getPaths() + { + return $this->paths; + } +} diff --git a/src/ZfrRest/Options/ModuleOptions.php b/src/ZfrRest/Options/ModuleOptions.php new file mode 100644 index 0000000..22a6e61 --- /dev/null +++ b/src/ZfrRest/Options/ModuleOptions.php @@ -0,0 +1,106 @@ + + * @licence MIT + */ +class ModuleOptions extends AbstractOptions +{ + /** + * Key of the object manager fetched from service locator + * + * @var string + */ + protected $objectManager; + + /** + * FQCN of the cache driver to use for the metadata + * + * @var string + */ + protected $cache; + + /** + * Options for all drivers + * + * @var DriverOptions[] + */ + protected $drivers; + + /** + * Set the object manager key + * + * @param string $objectManager + * @return void + */ + public function setObjectManager($objectManager) + { + $this->objectManager = (string) $objectManager; + } + + /** + * Get the object manager key + * + * @return string + */ + public function getObjectManager() + { + return $this->objectManager; + } + + /** + * @param string $cache + * @return void + */ + public function setCache($cache) + { + $this->cache = (string) $cache; + } + + /** + * @return string + */ + public function getCache() + { + return $this->cache; + } + + /** + * @param array $drivers + * @return void + */ + public function setDrivers($drivers) + { + foreach ($drivers as $driverOptions) { + $this->drivers[] = new DriverOptions($driverOptions); + } + } + + /** + * @return DriverOptions[] + */ + public function getDrivers() + { + return $this->drivers; + } +} diff --git a/src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php b/src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php new file mode 100755 index 0000000..9f7fb38 --- /dev/null +++ b/src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php @@ -0,0 +1,40 @@ + + */ +interface AnnotationInterface +{ + /** + * Get the key of the annotation + * + * @return string + */ + public function getKey(); + + /** + * Get the value of the annotation + * + * @return mixed + */ + public function getValue(); +} diff --git a/src/ZfrRest/Resource/Metadata/Annotation/Collection.php b/src/ZfrRest/Resource/Metadata/Annotation/Collection.php new file mode 100755 index 0000000..fe8340a --- /dev/null +++ b/src/ZfrRest/Resource/Metadata/Annotation/Collection.php @@ -0,0 +1,61 @@ + $this->controller, + 'inputFilter' => $this->inputFilter, + 'hydrator' => $this->hydrator + ); + } +} diff --git a/src/ZfrRest/Resource/Metadata/Annotation/ExposeAssociation.php b/src/ZfrRest/Resource/Metadata/Annotation/ExposeAssociation.php new file mode 100755 index 0000000..f0265c6 --- /dev/null +++ b/src/ZfrRest/Resource/Metadata/Annotation/ExposeAssociation.php @@ -0,0 +1,42 @@ + $this->controller, + 'inputFilter' => $this->inputFilter, + 'hydrator' => $this->hydrator + ); + } +} diff --git a/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php b/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php new file mode 100755 index 0000000..b9c87ad --- /dev/null +++ b/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php @@ -0,0 +1,192 @@ + + */ +class AnnotationDriver implements DriverInterface +{ + /** + * @var Reader + */ + protected $annotationReader; + + /** + * @var DoctrineMetadataFactory + */ + protected $doctrineMetadataFactory; + + /** + * @var ResourceMetadataFactory + */ + protected $resourceMetadataFactory; + + /** + * Constructor + * + * @param Reader $reader + * @param MetadataFactoryInterface $resourceMetadataFactory + * @param DoctrineMetadataFactory $doctrineMetadataFactory + */ + public function __construct( + Reader $reader, + MetadataFactoryInterface $resourceMetadataFactory, + DoctrineMetadataFactory $doctrineMetadataFactory + ) { + $this->annotationReader = $reader; + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->doctrineMetadataFactory = $doctrineMetadataFactory; + } + + /** + * {@inheritDoc} + */ + public function loadMetadataForClass(ReflectionClass $class) + { + $classMetadata = $this->doctrineMetadataFactory->getMetadataFor($class->getName()); + + $resourceMetadata = new ResourceMetadata($class->getName()); + $resourceMetadata->classMetadata = $classMetadata; + + // Process class level annotations + $classAnnotations = $this->annotationReader->getClassAnnotations($class); + $this->processMetadata($resourceMetadata, $classAnnotations); + + // Then process properties level annotations (for associations) + $classProperties = $class->getProperties(); + foreach ($classProperties as $classProperty) { + $propertyAnnotations = $this->annotationReader->getPropertyAnnotations($classProperty); + + // We need to have at least the ExposeAssociation annotation, so we loop through all the annotations, + // check if it exists, and remove it so that we can process other annotations + foreach ($propertyAnnotations as $key => $propertyAnnotation) { + if ($propertyAnnotation instanceof Annotation\ExposeAssociation) { + unset($propertyAnnotations[$key]); + + $associationName = $classProperty->getName(); + $targetClass = $classMetadata->getAssociationTargetClass($associationName); + + // We first load the metadata for the entity, and we then loop through the annotations defined + // at the association level so that the user can override some properties + $resourceAssociationMetadata = $this->resourceMetadataFactory + ->getMetadataForClass($targetClass) + ->getOutsideClassMetadata(); + + $this->processMetadata($resourceAssociationMetadata, $propertyAnnotations); + $resourceMetadata->associations[$associationName] = $resourceAssociationMetadata; + + break; + } + } + } + + return $resourceMetadata; + } + + /** + * @param ResourceMetadata $metadata + * @param AnnotationInterface[] $annotations + * @return void + */ + private function processMetadata(ResourceMetadata $metadata, array $annotations) + { + foreach ($annotations as $annotation) { + if (!($annotation instanceof AnnotationInterface)) { + continue; + } + + // Resource annotation + if ($annotation instanceof Annotation\Resource) { + $this->processResourceMetadata($metadata, $annotation); + } + + // Collection annotation + if ($annotation instanceof Annotation\Collection) { + $this->processCollectionMetadata($metadata, $annotation); + } + } + } + + /** + * @param ResourceMetadata $metadata + * @param Annotation\Resource $annotation + * @return void + */ + private function processResourceMetadata(ResourceMetadata $metadata, Annotation\Resource $annotation) + { + $values = $annotation->getValue(); + + foreach ($values as $key => $value) { + // Ignore null values in order to make cascading work as expected + if (null === $value) { + continue; + } + + $propertyMetadata = new PropertyMetadata($metadata, $key); + $propertyMetadata->setValue($metadata, $value); + + $metadata->addPropertyMetadata($propertyMetadata); + } + } + + /** + * @param ResourceMetadata $metadata + * @param Annotation\Collection $annotation + * @return void + */ + private function processCollectionMetadata(ResourceMetadata $metadata, Annotation\Collection $annotation) + { + $values = $annotation->getValue(); + $collectionMetadata = new CollectionResourceMetadata($metadata->name); + + foreach ($values as $key => $value) { + $propertyMetadata = new PropertyMetadata($collectionMetadata, $key); + + // If the value is null, then we reuse the value defined at "resource-level" + if (null === $value && isset($metadata->propertyMetadata[$key])) { + $propertyMetadata->setValue( + $collectionMetadata, + $metadata->propertyMetadata[$key]->getValue($metadata) + ); + } else { + $propertyMetadata->setValue($collectionMetadata, $value); + } + + $collectionMetadata->addPropertyMetadata($propertyMetadata); + } + + $metadata->collectionMetadata = $collectionMetadata; + } +} diff --git a/tests/ZfrRestTest/Factory/ModuleOptionsFactoryTest.php b/tests/ZfrRestTest/Factory/ModuleOptionsFactoryTest.php new file mode 100644 index 0000000..72c0b67 --- /dev/null +++ b/tests/ZfrRestTest/Factory/ModuleOptionsFactoryTest.php @@ -0,0 +1,43 @@ + + * @covers \ZfrRest\Factory\ModuleOptionsFactory + */ +class ModuleOptionsFactoryTest extends TestCase +{ + public function testCanCreateFromFactory() + { + $serviceManager = ServiceManagerFactory::getServiceManager(); + + $object = $serviceManager->get('ZfrRest\Options\ModuleOptions'); + $this->assertInstanceOf('ZfrRest\Options\ModuleOptions', $object); + + $this->assertInternalType('array', $object->getDrivers()); + + foreach ($object->getDrivers() as $driverOptions) { + $this->assertInstanceOf('ZfrRest\Options\DriverOptions', $driverOptions); + } + } +} diff --git a/tests/ZfrRestTest/Factory/ResourceMetadataFactoryFactoryTest.php b/tests/ZfrRestTest/Factory/ResourceMetadataFactoryFactoryTest.php new file mode 100644 index 0000000..58f12a3 --- /dev/null +++ b/tests/ZfrRestTest/Factory/ResourceMetadataFactoryFactoryTest.php @@ -0,0 +1,40 @@ + + * @covers \ZfrRest\Factory\ResourceMetadataFactoryFactory + */ +class ResourceMetadataFactoryFactoryTest extends TestCase +{ + public function testCanCreateFromFactory() + { + $serviceManager = ServiceManagerFactory::getServiceManager(); + + $serviceManager->setService('array_cache', $this->getMock('Metadata\Cache\CacheInterface')); + + $object = $serviceManager->get('ZfrRest\Resource\Metadata\ResourceMetadataFactory'); + + $this->assertInstanceOf('Metadata\MetadataFactoryInterface', $object); + } +} diff --git a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Address.php b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Address.php new file mode 100644 index 0000000..23a1b2b --- /dev/null +++ b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Address.php @@ -0,0 +1,24 @@ + + * @covers \ZfrRest\Resource\Metadata\Driver\AnnotationDriverTest + */ +class AnnotationDriverFunctionalTest extends TestCase +{ + public function testMetadataFromAnnotation() + { + $serviceManager = ServiceManagerFactory::getServiceManager(); + + $serviceManager->setAlias('object_manager', 'doctrine.entitymanager.orm_default'); + $serviceManager->setService('array_cache', $this->getMock('Metadata\Cache\CacheInterface')); + + $resourceFactory = $serviceManager->get('ZfrRest\Resource\Metadata\ResourceMetadataFactory'); + + + } +} diff --git a/tests/testing.config.php b/tests/testing.config.php index 61b9edc..ef921fe 100644 --- a/tests/testing.config.php +++ b/tests/testing.config.php @@ -19,46 +19,42 @@ return array( 'zfr_rest' => array( 'object_manager' => 'doctrine.entitymanager.orm_default', - - 'resource_metadata' => array( - 'drivers' => array( - 'application_driver' => array( - 'class' => 'ZfrRest\Resource\Metadata\Driver\AnnotationDriver' - ) + 'cache' => 'array_cache', + 'drivers' => array( + 'application_driver' => array( + 'class' => 'ZfrRest\Resource\Metadata\Driver\AnnotationDriver' ) ) ), 'doctrine' => array( 'driver' => array( - 'application_driver' => array( + 'annotation_driver' => array( 'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver', 'cache' => 'array', - 'paths' => array(__DIR__ . '/ZfrRestTest/Asset/Annotation') + 'paths' => array(__DIR__ . '/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset') ), 'orm_default' => array( 'drivers' => array( - 'ZfrRestTest\Asset\Annotation' => 'application_driver' + 'ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset' => 'annotation_driver' ) ) ), - ), - 'controllers' => array( - 'invokables' => array( - 'ZfrRestTest\Asset\Controller\UserController' => 'ZfrRestTest\Asset\Controller\UserController', - ), - ), - - 'input_filters' => array( - 'invokables' => array( - 'ZfrRestTest\Asset\InputFilter\UserInputFilter' => 'ZfrRestTest\Asset\InputFilter\UserInputFilter', - ), - ), - - 'hydrators' => array( - 'invokables' => array( - 'ZfrRestTest\Asset\Hydrator\UserHydrator' => 'ZfrRestTest\Asset\Hydrator\UserHydrator', + 'connection' => array( + 'orm_default' => array( + 'params' => array( + 'host' => null, + 'port' => null, + 'user' => null, + 'password' => null, + 'dbname' => null, + 'driver' => 'pdo_sqlite', + 'driverClass' => 'Doctrine\DBAL\Driver\PDOSqlite\Driver', + 'path' => null, + 'memory' => true, + ), + ), ), ), ); From 8afe8c5a2ad1fd59c325b2fc6eb91b42ca0590ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Tue, 15 Oct 2013 17:59:22 +0200 Subject: [PATCH 14/19] Add lot of functional tests for annotations --- composer.json | 2 +- .../Metadata/Driver/AnnotationDriver.php | 26 ++++----- .../Metadata/Driver/AnnotationAsset/City.php | 29 ++++++++++ .../{PointOfInterest.php => Country.php} | 18 +++++- .../{Address.php => Mayor.php} | 13 ++++- .../Driver/AnnotationDriverFunctionalTest.php | 58 ++++++++++++++++++- tests/testing.config.php | 2 +- 7 files changed, 128 insertions(+), 20 deletions(-) rename tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/{PointOfInterest.php => Country.php} (66%) rename tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/{Address.php => Mayor.php} (87%) diff --git a/composer.json b/composer.json index f038501..f15bd1f 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "require": { "php": ">=5.3.3", "symfony/serializer": "~2.2", - "jms/metadata": "~1.4", + "jms/metadata": "dev-master", "zendframework/zendframework": "~2.2", "doctrine/common": "~2.4", "doctrine/doctrine-module": "~0.8" diff --git a/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php b/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php index b9c87ad..66ec40c 100755 --- a/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php +++ b/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php @@ -100,9 +100,9 @@ public function loadMetadataForClass(ReflectionClass $class) // We first load the metadata for the entity, and we then loop through the annotations defined // at the association level so that the user can override some properties - $resourceAssociationMetadata = $this->resourceMetadataFactory - ->getMetadataForClass($targetClass) - ->getOutsideClassMetadata(); + $resourceAssociationMetadata = clone $this->resourceMetadataFactory + ->getMetadataForClass($targetClass) + ->getOutsideClassMetadata(); $this->processMetadata($resourceAssociationMetadata, $propertyAnnotations); $resourceMetadata->associations[$associationName] = $resourceAssociationMetadata; @@ -169,21 +169,19 @@ private function processResourceMetadata(ResourceMetadata $metadata, Annotation\ private function processCollectionMetadata(ResourceMetadata $metadata, Annotation\Collection $annotation) { $values = $annotation->getValue(); - $collectionMetadata = new CollectionResourceMetadata($metadata->name); + $collectionMetadata = $metadata->collectionMetadata + ? clone $metadata->collectionMetadata + : new CollectionResourceMetadata($metadata->name); foreach ($values as $key => $value) { - $propertyMetadata = new PropertyMetadata($collectionMetadata, $key); - - // If the value is null, then we reuse the value defined at "resource-level" - if (null === $value && isset($metadata->propertyMetadata[$key])) { - $propertyMetadata->setValue( - $collectionMetadata, - $metadata->propertyMetadata[$key]->getValue($metadata) - ); - } else { - $propertyMetadata->setValue($collectionMetadata, $value); + // Ignore null values in order to make cascading work as expected + if (null === $value) { + continue; } + $propertyMetadata = new PropertyMetadata($collectionMetadata, $key); + $propertyMetadata->setValue($collectionMetadata, $value); + $collectionMetadata->addPropertyMetadata($propertyMetadata); } diff --git a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/City.php b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/City.php index 223efbd..73ce5ff 100644 --- a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/City.php +++ b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/City.php @@ -18,8 +18,37 @@ namespace ZfrRestTest\Resource\Metadata\Driver\AnnotationAsset; +use Doctrine\ORM\Mapping as ORM; +use ZfrRest\Resource\Metadata\Annotation as REST; +/** + * @ORM\Entity + * @REST\Resource(controller="CityController", inputFilter="CityInputFilter", hydrator="CityHydrator") + * @REST\Collection(controller="CityCollController", inputFilter="CityCollInputFilter", hydrator="CityCollHydrator") + */ class City { + /** + * @ORM\Id + * @ORM\Column + */ + protected $id; + + /** + * @ORM\Column(type="string") + */ + protected $name; + + /** + * @ORM\OneToOne(targetEntity="Country") + * @REST\ExposeAssociation + * @REST\Resource(controller="CityCountryController") + * @REST\Collection(hydrator="CityCountryCollHydrator") + */ + protected $country; + /** + * @ORM\OneToOne(targetEntity="Mayor") + */ + protected $mayor; } diff --git a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/PointOfInterest.php b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Country.php similarity index 66% rename from tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/PointOfInterest.php rename to tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Country.php index 7fe1317..9a7e887 100644 --- a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/PointOfInterest.php +++ b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Country.php @@ -18,8 +18,24 @@ namespace ZfrRestTest\Resource\Metadata\Driver\AnnotationAsset; +use Doctrine\ORM\Mapping as ORM; +use ZfrRest\Resource\Metadata\Annotation as REST; -class PointOfInterest +/** + * @ORM\Entity + * @REST\Resource(controller="CountryController", inputFilter="CountryInputFilter", hydrator="CountryHydrator") + * @REST\Collection(controller="CountryCollController", inputFilter="CountryCollInputFilter", hydrator="CountryCollHydrator") + */ +class Country { + /** + * @ORM\Id + * @ORM\Column + */ + protected $id; + /** + * @ORM\Column(type="string") + */ + protected $name; } diff --git a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Address.php b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Mayor.php similarity index 87% rename from tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Address.php rename to tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Mayor.php index 23a1b2b..f3bbd88 100644 --- a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Address.php +++ b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset/Mayor.php @@ -18,7 +18,16 @@ namespace ZfrRestTest\Resource\Metadata\Driver\AnnotationAsset; -class Address -{ +use Doctrine\ORM\Mapping as ORM; +/** + * @ORM\Entity + */ +class Mayor +{ + /** + * @ORM\Id + * @ORM\Column + */ + protected $id; } diff --git a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverFunctionalTest.php b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverFunctionalTest.php index 26976d0..fe34741 100644 --- a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverFunctionalTest.php +++ b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverFunctionalTest.php @@ -33,9 +33,65 @@ public function testMetadataFromAnnotation() $serviceManager->setAlias('object_manager', 'doctrine.entitymanager.orm_default'); $serviceManager->setService('array_cache', $this->getMock('Metadata\Cache\CacheInterface')); - + + /** @var \Metadata\MetadataFactory $resourceFactory */ $resourceFactory = $serviceManager->get('ZfrRest\Resource\Metadata\ResourceMetadataFactory'); + $cityMetadata = $resourceFactory->getMetadataForClass( + 'ZfrRestTest\Resource\Metadata\Driver\AnnotationAsset\City' + ); + + /** @var \ZfrRest\Resource\Metadata\ResourceMetadata $cityMetadata */ + $cityMetadata = $cityMetadata->getOutsideClassMetadata(); + + $this->assertInstanceOf('ZfrRest\Resource\Metadata\ResourceMetadataInterface', $cityMetadata); + $this->assertEquals('ZfrRestTest\Resource\Metadata\Driver\AnnotationAsset\City', $cityMetadata->name); + + // Test the resource properties + $this->assertEquals('CityController', $cityMetadata->getControllerName()); + $this->assertEquals('CityInputFilter', $cityMetadata->getInputFilterName()); + $this->assertEquals('CityHydrator', $cityMetadata->getHydratorName()); + + // Test the collection properties + $collectionMetadata = $cityMetadata->getCollectionMetadata(); + $this->assertInstanceOf('ZfrRest\Resource\Metadata\CollectionResourceMetadataInterface', $collectionMetadata); + $this->assertEquals('CityCollController', $collectionMetadata->getControllerName()); + $this->assertEquals('CityCollInputFilter', $collectionMetadata->getInputFilterName()); + $this->assertEquals('CityCollHydrator', $collectionMetadata->getHydratorName()); + + // Test if it has associated, exposed resources + $this->assertTrue($cityMetadata->hasAssociation('country')); + $this->assertFalse($cityMetadata->hasAssociation('mayor')); + + // Test that the annotation defined at the association level override the ones defined at the entity level + $countryMetadata = $resourceFactory->getMetadataForClass( + 'ZfrRestTest\Resource\Metadata\Driver\AnnotationAsset\Country' + ); + + /** @var \ZfrRest\Resource\Metadata\ResourceMetadata $countryMetadata */ + $countryMetadata = $countryMetadata->getOutsideClassMetadata(); + + $this->assertInstanceOf('ZfrRest\Resource\Metadata\ResourceMetadataInterface', $countryMetadata); + $this->assertEquals('ZfrRestTest\Resource\Metadata\Driver\AnnotationAsset\Country', $countryMetadata->name); + + $cityCountryMetadata = $cityMetadata->getAssociationMetadata('country'); + + $this->assertNotSame($countryMetadata, $cityCountryMetadata); + $this->assertEquals('CityCountryController', $cityCountryMetadata->getControllerName(), 'Overriden'); + $this->assertEquals('CountryController', $countryMetadata->getControllerName(), 'Origin'); + + $this->assertEquals($cityCountryMetadata->getHydratorName(), $countryMetadata->getHydratorName()); + $this->assertEquals($cityCountryMetadata->getInputFilterName(), $countryMetadata->getInputFilterName()); + + // Test that override can also work on collection + + $cityCountryCollMetadata = $cityCountryMetadata->getCollectionMetadata(); + $countryCollMetadata = $countryMetadata->getCollectionMetadata(); + $this->assertNotSame($cityCountryCollMetadata, $countryCollMetadata); + $this->assertEquals('CityCountryCollHydrator', $cityCountryCollMetadata->getHydratorName(), 'Overriden'); + $this->assertEquals('CountryCollHydrator', $countryCollMetadata->getHydratorName(), 'Origin'); + $this->assertEquals($cityCountryCollMetadata->getControllerName(), $countryCollMetadata->getControllerName()); + $this->assertEquals($cityCountryCollMetadata->getInputFilterName(), $countryCollMetadata->getInputFilterName()); } } diff --git a/tests/testing.config.php b/tests/testing.config.php index ef921fe..ab5be54 100644 --- a/tests/testing.config.php +++ b/tests/testing.config.php @@ -36,7 +36,7 @@ ), 'orm_default' => array( 'drivers' => array( - 'ZfrRestTest/Resource/Metadata/Driver/AnnotationAsset' => 'annotation_driver' + 'ZfrRestTest\Resource\Metadata\Driver\AnnotationAsset' => 'annotation_driver' ) ) ), From 93bcefc0271ac0d8215c45d286d803a0c83e3825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Tue, 15 Oct 2013 18:06:21 +0200 Subject: [PATCH 15/19] Move cloning logic --- src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php | 4 +--- src/ZfrRest/Resource/Metadata/ResourceMetadata.php | 8 ++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php b/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php index 66ec40c..c243cdf 100755 --- a/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php +++ b/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php @@ -169,9 +169,7 @@ private function processResourceMetadata(ResourceMetadata $metadata, Annotation\ private function processCollectionMetadata(ResourceMetadata $metadata, Annotation\Collection $annotation) { $values = $annotation->getValue(); - $collectionMetadata = $metadata->collectionMetadata - ? clone $metadata->collectionMetadata - : new CollectionResourceMetadata($metadata->name); + $collectionMetadata = $metadata->collectionMetadata ?: new CollectionResourceMetadata($metadata->name); foreach ($values as $key => $value) { // Ignore null values in order to make cascading work as expected diff --git a/src/ZfrRest/Resource/Metadata/ResourceMetadata.php b/src/ZfrRest/Resource/Metadata/ResourceMetadata.php index 27cc3b1..0c32959 100755 --- a/src/ZfrRest/Resource/Metadata/ResourceMetadata.php +++ b/src/ZfrRest/Resource/Metadata/ResourceMetadata.php @@ -130,4 +130,12 @@ public function getCollectionMetadata() { return $this->collectionMetadata; } + + /** + * Make sure to clone the collection metadata too + */ + public function __clone() + { + $this->collectionMetadata = clone $this->collectionMetadata; + } } From 2fc5e0c1e12a2a0708360175790a878a6ebcce80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Tue, 15 Oct 2013 18:09:26 +0200 Subject: [PATCH 16/19] Remove useless import --- src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php b/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php index c243cdf..d8a64df 100755 --- a/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php +++ b/src/ZfrRest/Resource/Metadata/Driver/AnnotationDriver.php @@ -26,7 +26,6 @@ use Metadata\MetadataFactoryInterface as ResourceMetadataFactory; use Metadata\PropertyMetadata; use ZfrRest\Resource\Metadata\Annotation; -use ZfrRest\Resource\Metadata\Annotation\AnnotationInterface; use ZfrRest\Resource\Metadata\CollectionResourceMetadata; use ZfrRest\Resource\Metadata\ResourceMetadata; @@ -116,14 +115,14 @@ public function loadMetadataForClass(ReflectionClass $class) } /** - * @param ResourceMetadata $metadata - * @param AnnotationInterface[] $annotations + * @param ResourceMetadata $metadata + * @param Annotation\AnnotationInterface[] $annotations * @return void */ private function processMetadata(ResourceMetadata $metadata, array $annotations) { foreach ($annotations as $annotation) { - if (!($annotation instanceof AnnotationInterface)) { + if (!($annotation instanceof Annotation\AnnotationInterface)) { continue; } From fffe965a1ab32c1e00a1c2f9eacbadb0b72c1268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Tue, 15 Oct 2013 18:17:14 +0200 Subject: [PATCH 17/19] Add coverage block --- .../Metadata/Driver/AnnotationDriverFunctionalTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverFunctionalTest.php b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverFunctionalTest.php index fe34741..2e3b037 100644 --- a/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverFunctionalTest.php +++ b/tests/ZfrRestTest/Resource/Metadata/Driver/AnnotationDriverFunctionalTest.php @@ -23,7 +23,12 @@ /** * @author Michaël Gallego - * @covers \ZfrRest\Resource\Metadata\Driver\AnnotationDriverTest + * @covers \ZfrRest\Resource\Metadata\Driver\AnnotationDriver + * @covers \ZfrRest\Resource\Metadata\ResourceMetadata + * @covers \ZfrRest\Resource\Metadata\CollectionResourceMetadata + * @covers \ZfrRest\Resource\Metadata\Annotation\Collection + * @covers \ZfrRest\Resource\Metadata\Annotation\ExposeAssociation + * @covers \ZfrRest\Resource\Metadata\Annotation\Resource */ class AnnotationDriverFunctionalTest extends TestCase { From 736637c23d149a4ceb44cf4694c7a0c06c11c913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Tue, 15 Oct 2013 18:34:55 +0200 Subject: [PATCH 18/19] Fix CS --- src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php b/src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php index 020b8cd..4c71110 100644 --- a/src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php +++ b/src/ZfrRest/Factory/ResourceMetadataFactoryFactory.php @@ -61,7 +61,9 @@ public function createService(ServiceLocatorInterface $serviceLocator) switch($driverOptions->getClass()) { case 'ZfrRest\Resource\Metadata\Driver\AnnotationDriver': $driver = new AnnotationDriver( - new AnnotationReader(), $resourceMetadataFactory, $doctrineMetadataFactory + new AnnotationReader(), + $resourceMetadataFactory, + $doctrineMetadataFactory ); break; default: From d0239962d4566cf0d3085f3e33a561dc4c625590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Tue, 15 Oct 2013 21:58:39 +0200 Subject: [PATCH 19/19] Remove useless methods --- .../Resource/Metadata/Annotation/AnnotationInterface.php | 7 ------- src/ZfrRest/Resource/Metadata/Annotation/Collection.php | 8 -------- .../Resource/Metadata/Annotation/ExposeAssociation.php | 8 -------- src/ZfrRest/Resource/Metadata/Annotation/Resource.php | 8 -------- 4 files changed, 31 deletions(-) diff --git a/src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php b/src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php index 9f7fb38..05f33a8 100755 --- a/src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php +++ b/src/ZfrRest/Resource/Metadata/Annotation/AnnotationInterface.php @@ -24,13 +24,6 @@ */ interface AnnotationInterface { - /** - * Get the key of the annotation - * - * @return string - */ - public function getKey(); - /** * Get the value of the annotation * diff --git a/src/ZfrRest/Resource/Metadata/Annotation/Collection.php b/src/ZfrRest/Resource/Metadata/Annotation/Collection.php index fe8340a..4580dc3 100755 --- a/src/ZfrRest/Resource/Metadata/Annotation/Collection.php +++ b/src/ZfrRest/Resource/Metadata/Annotation/Collection.php @@ -39,14 +39,6 @@ final class Collection implements AnnotationInterface */ public $hydrator; - /** - * {@inheritDoc} - */ - public function getKey() - { - return 'collection'; - } - /** * {@inheritDoc} */ diff --git a/src/ZfrRest/Resource/Metadata/Annotation/ExposeAssociation.php b/src/ZfrRest/Resource/Metadata/Annotation/ExposeAssociation.php index f0265c6..fde1e58 100755 --- a/src/ZfrRest/Resource/Metadata/Annotation/ExposeAssociation.php +++ b/src/ZfrRest/Resource/Metadata/Annotation/ExposeAssociation.php @@ -24,14 +24,6 @@ */ final class ExposeAssociation implements AnnotationInterface { - /** - * {@inheritDoc} - */ - public function getKey() - { - return 'exposeAssociation'; - } - /** * {@inheritDoc} */ diff --git a/src/ZfrRest/Resource/Metadata/Annotation/Resource.php b/src/ZfrRest/Resource/Metadata/Annotation/Resource.php index b7ffa13..66ba0fa 100755 --- a/src/ZfrRest/Resource/Metadata/Annotation/Resource.php +++ b/src/ZfrRest/Resource/Metadata/Annotation/Resource.php @@ -39,14 +39,6 @@ final class Resource implements AnnotationInterface */ public $hydrator; - /** - * {@inheritDoc} - */ - public function getKey() - { - return 'resource'; - } - /** * {@inheritDoc} */