diff --git a/config/module.config.php b/config/module.config.php index 5a2f76f..0aa795a 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -26,10 +26,12 @@ /* Factories that map to a class */ 'ZfrRest\Mvc\Controller\MethodHandler\MethodHandlerPluginManager' => 'ZfrRest\Factory\MethodHandlerPluginManagerFactory', 'ZfrRest\Options\ModuleOptions' => 'ZfrRest\Factory\ModuleOptionsFactory', - 'ZFrRest\Router\Http\Matcher\AssociationSubPathMatcher' => 'ZfrRest\Factory\AssociationSubPathMatcherFactory', + 'ZfrRest\Router\Http\Matcher\AssociationSubPathMatcher' => 'ZfrRest\Factory\AssociationSubPathMatcherFactory', + 'ZfrRest\Router\Http\Matcher\BaseSubPathMatcher' => 'ZfrRest\Factory\BaseSubPathMatcherFactory', 'ZfrRest\View\Renderer\ResourceRenderer' => 'ZfrRest\ResourceRendererFactory', 'ZfrRest\View\Strategy\ResourceStrategy' => 'ZfrRest\ResourceStrategyFactory' ], + 'invokables' => [ 'ZfrRest\Mvc\CreateResourceModelListener' => 'ZfrRest\Mvc\CreateResourceModelListener', 'ZfrRest\Mvc\HttpExceptionListener' => 'ZfrRest\Mvc\HttpExceptionListener', @@ -38,6 +40,16 @@ ] ], + 'route_manager' => [ + 'factories' => [ + 'ZfrRest\Router\Http\ResourceGraphRoute' => 'ZfrRest\Factory\ResourceGraphRouteFactory' + ], + + 'aliases' => [ + 'ResourceGraphRoute' => 'ZfrRest\Router\Http\ResourceGraphRoute' + ], + ], + 'view_manager' => [ 'strategies' => [ 'ZfrRest\View\Strategy\ResourceStrategy' diff --git a/src/ZfrRest/Factory/BaseSubPathMatcherFactory.php b/src/ZfrRest/Factory/BaseSubPathMatcherFactory.php new file mode 100644 index 0000000..c368219 --- /dev/null +++ b/src/ZfrRest/Factory/BaseSubPathMatcherFactory.php @@ -0,0 +1,41 @@ + + * @licence MIT + */ +class BaseSubPathMatcherFactory implements FactoryInterface +{ + /** + * {@inheritDoc} + */ + public function createService(ServiceLocatorInterface $serviceLocator) + { + return new BaseSubPathMatcher( + $serviceLocator->get('ZfrRest\Router\Http\Matcher\CollectionSubPathMatcher'), + $serviceLocator->get('ZfrRest\Router\Http\Matcher\AssociationSubPathMatcher') + ); + } +} diff --git a/src/ZfrRest/Factory/ResourceGraphRouteFactory.php b/src/ZfrRest/Factory/ResourceGraphRouteFactory.php new file mode 100755 index 0000000..44b1905 --- /dev/null +++ b/src/ZfrRest/Factory/ResourceGraphRouteFactory.php @@ -0,0 +1,74 @@ + + * @licence MIT + */ +class ResourceGraphRouteFactory implements FactoryInterface, MutableCreationOptionsInterface +{ + /** + * @var array + */ + protected $creationOptions; + + /** + * @param array $creationOptions + * @throws RuntimeException + */ + public function setCreationOptions(array $creationOptions) + { + if (!isset($creationOptions['resource'])) { + throw new RuntimeException('No resource option specified for ResourceGraphRoute'); + } + + if (!isset($creationOptions['route'])) { + throw new RuntimeException('No route option specified for ResourceGraphRoute'); + } + + $this->creationOptions = $creationOptions; + } + + /** + * {@inheritDoc} + * @return ResourceGraphRoute + * @throws RuntimeException + */ + public function createService(ServiceLocatorInterface $serviceLocator) + { + $parentLocator = $serviceLocator->getServiceLocator(); + $resource = $parentLocator->get($this->creationOptions['resource']); + $metadataFactory = $parentLocator->get('ZfrRest\Resource\Metadata\ResourceMetadataFactory'); + $matcher = $parentLocator->get('ZfrRest\Router\Http\Matcher\BaseSubPathMatcher'); + + return new ResourceGraphRoute( + $metadataFactory, + $matcher, + $resource, + $this->creationOptions['route'] + ); + } +} diff --git a/src/ZfrRest/Router/Http/ResourceGraphRoute.php b/src/ZfrRest/Router/Http/ResourceGraphRoute.php new file mode 100755 index 0000000..cfa8256 --- /dev/null +++ b/src/ZfrRest/Router/Http/ResourceGraphRoute.php @@ -0,0 +1,226 @@ + + * @author Michaël Gallego + */ +class ResourceGraphRoute implements RouteInterface +{ + /** + * @var MetadataFactory + */ + protected $metadataFactory; + + /** + * @var mixed + */ + protected $resource; + + /** + * @var BaseSubPathMatcher + */ + protected $subPathMatcher; + + /** + * @var string + */ + protected $route; + + /** + * Constructor + * + * @param MetadataFactory $metadataFactory + * @param BaseSubPathMatcher $matcher + * @param mixed $resource + * @param string $route + */ + public function __construct(MetadataFactory $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: not sure about how to do this correctly... + + throw new RuntimeException('ResourceGraphRoute does not support yet assembling route'); + } + + /** + * {@inheritDoc} + */ + public static function factory($options = array()) + { + throw new RuntimeException('Not supported'); + } + + /** + * {@inheritDoc} + */ + public function match(RequestInterface $request) + { + if (!$request instanceof HttpRequest) { + return null; + } + + $uri = $request->getUri(); + $path = trim($uri->getPath(), '/'); + + // We must omit the basePath + if (method_exists($request, 'getBaseUrl') && $baseUrl = $request->getBaseUrl()) { + $path = substr($path, strlen(trim($baseUrl, '/'))); + } + + // If the URI does not begin by the route, we can stop immediately + if (substr($path, 0, strlen($this->route)) !== $this->route) { + return null; + } + + // If we have only one segment (for instance "users"), then the next path to analyze is in fact + // an empty string, hence the ternary condition + $pathParts = explode('/', $path, 2); + $subPath = count($pathParts) === 1 ? '' : end($pathParts); + + if (!$match = $this->subPathMatcher->matchSubPath($this->getResource(), $subPath, $request)) { + return null; + } + + return $this->buildRouteMatch($match->getMatchedResource(), $this->route); + } + + /** + * Build a route match + * + * @param ResourceInterface $resource + * @param string $route + * @throws RuntimeException + * @return RouteMatch + */ + protected function buildRouteMatch(ResourceInterface $resource, $route) + { + $metadata = $resource->getMetadata(); + + // If returned $data is a collection, then we use the controller specified in Collection mapping + if ($resource->isCollection()) { + if (!$collectionMetadata = $metadata->getCollectionMetadata()) { + throw new RuntimeException( + 'No collection metadata could be found. Did you make sure you added the Collection annotation?' + ); + } + + // We wrap the data around a paginator + $paginator = $this->wrapDataInPaginator($resource); + $resource = new Resource($paginator, $metadata); + + $controllerName = $collectionMetadata->getControllerName(); + } else { + $controllerName = $metadata->getControllerName(); + } + + return new RouteMatch( + array( + 'resource' => $resource, + 'controller' => $controllerName + ), + strlen($route) + ); + } + + /** + * Wrap a data around a paginator + * + * @param ResourceInterface $resource + * @return Paginator + * @throws RuntimeException If no paginator adapter is found + */ + protected function wrapDataInPaginator(ResourceInterface $resource) + { + $data = $resource->getData(); + $paginatorAdapter = null; + + if ($data instanceof Selectable) { + $paginatorAdapter = new SelectableAdapter($data); + } elseif ($data instanceof Collection) { + $paginatorAdapter = new CollectionAdapter($data); + } + + if (null === $paginatorAdapter) { + throw new RuntimeException(sprintf( + 'No paginator adapter could be found for resource of type "%s"', + is_object($data) ? get_class($data) : gettype($data) + )); + } + + return new Paginator($paginatorAdapter); + } + + /** + * 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 RuntimeException + * @return ResourceInterface + */ + private function getResource() + { + // Don't initialize twice + if ($this->resource instanceof ResourceInterface) { + return $this->resource; + } + + if ($this->resource instanceof ObjectRepository) { + $metadata = $this->metadataFactory->getMetadataForClass($this->resource->getClassName()); + } elseif (is_string($this->resource)) { + $metadata = $this->metadataFactory->getMetadataForClass($this->resource); + } else { + throw new RuntimeException(sprintf( + 'Resource "%s" is not supported: either specify an ObjectRepository instance, or an entity class name', + is_object($this->resource) ? get_class($this->resource) : gettype($this->resource) + )); + } + + return $this->resource = new Resource($this->resource, $metadata); + } +} diff --git a/tests/ZfrRestTest/Factory/BaseSubPathMatcherFactoryTest.php b/tests/ZfrRestTest/Factory/BaseSubPathMatcherFactoryTest.php new file mode 100755 index 0000000..a30b4b4 --- /dev/null +++ b/tests/ZfrRestTest/Factory/BaseSubPathMatcherFactoryTest.php @@ -0,0 +1,51 @@ + + * + * @group Coverage + * @covers \ZfrRest\Factory\BaseSubPathMatcherFactory + */ +class BaseSubPathMatcherFactoryTest extends PHPUnit_Framework_TestCase +{ + public function testCreateFromFactory() + { + $serviceManager = new ServiceManager(); + $serviceManager->setService( + 'ZfrRest\Router\Http\Matcher\CollectionSubPathMatcher', + $this->getMock('ZfrRest\Router\Http\Matcher\CollectionSubPathMatcher', [], [], '', false) + ); + $serviceManager->setService( + 'ZfrRest\Router\Http\Matcher\AssociationSubPathMatcher', + $this->getMock('ZfrRest\Router\Http\Matcher\AssociationSubPathMatcher', [], [], '', false) + ); + + $factory = new BaseSubPathMatcherFactory(); + $result = $factory->createService($serviceManager); + + $this->assertInstanceOf('ZfrRest\Router\Http\Matcher\BaseSubPathMatcher', $result); + } +}