diff --git a/Controller/Annotations/QueryParam.php b/Controller/Annotations/QueryParam.php new file mode 100644 index 000000000..e87c209e2 --- /dev/null +++ b/Controller/Annotations/QueryParam.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Controller\Annotations; + +/** + * QueryParam annotation class. + * + * @Annotation + * @author Alexander + */ +class QueryParam +{ + /** @var string */ + public $name; + /** @var string */ + public $requirements; + /** @var string */ + public $default; + /** @var string */ + public $description; +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index b17f3b321..740865239 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -27,6 +27,8 @@ */ class Configuration implements ConfigurationInterface { + private $forceOptionValues = array(false, true, 'force'); + /** * Generates the configuration tree. * @@ -39,6 +41,12 @@ public function getConfigTreeBuilder() $rootNode ->children() + ->scalarNode('query_fetcher_listener')->defaultFalse() + ->validate() + ->ifNotInArray($this->forceOptionValues) + ->thenInvalid('The query_fetcher_listener option does not support %s. Please choose one of '.json_encode($this->forceOptionValues)) + ->end() + ->end() ->arrayNode('routing_loader') ->addDefaultsIfNotSet() ->children() @@ -111,7 +119,12 @@ private function addViewSection(ArrayNodeDefinition $rootNode) ->defaultValue(array('html' => true)) ->prototype('boolean')->end() ->end() - ->scalarNode('view_response_listener')->defaultValue('force')->end() + ->scalarNode('view_response_listener')->defaultValue('force') + ->validate() + ->ifNotInArray($this->forceOptionValues) + ->thenInvalid('The view_response_listener option does not support %s. Please choose one of '.json_encode($this->forceOptionValues)) + ->end() + ->end() ->scalarNode('failed_validation')->defaultValue(Codes::HTTP_BAD_REQUEST)->end() ->end() ->end() diff --git a/DependencyInjection/FOSRestExtension.php b/DependencyInjection/FOSRestExtension.php index ab1e34138..ec83287a2 100644 --- a/DependencyInjection/FOSRestExtension.php +++ b/DependencyInjection/FOSRestExtension.php @@ -39,6 +39,7 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('view.xml'); $loader->load('routing.xml'); $loader->load('util.xml'); + $loader->load('request.xml'); if (version_compare(FOSRestBundle::getSymfonyVersion(Kernel::VERSION), '2.1.0', '<')) { $container->setParameter('fos_rest.routing.loader.controller.class', $container->getParameter('fos_rest.routing.loader_2_0.controller.class')); @@ -128,6 +129,14 @@ public function load(array $configs, ContainerBuilder $container) } else { $container->setParameter($this->getAlias().'.mime_types', array()); } + + if (!empty($config['query_fetcher_listener'])) { + $loader->load('query_fetcher_listener.xml'); + + if ('force' === $config['query_fetcher_listener']) { + $container->setParameter($this->getAlias().'.query_fetch_listener.set_params_as_attributes', true); + } + } } /** diff --git a/EventListener/QueryFetcherListener.php b/EventListener/QueryFetcherListener.php new file mode 100644 index 000000000..b323e39c8 --- /dev/null +++ b/EventListener/QueryFetcherListener.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\EventListener; + +use Symfony\Component\HttpKernel\Event\FilterControllerEvent, + Symfony\Component\HttpKernel\HttpKernelInterface, + Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * This listener handles various setup tasks related to the query fetcher + * + * Setting the controller callable on the query fetcher + * Setting the query fetcher as a request attribute + * + * @author Lukas Kahwe Smith + */ +class QueryFetcherListener +{ + /** + * @var ContainerInterface + */ + private $container; + + private $setParamsAsAttributes; + + /** + * Constructor. + * + * @param ContainerInterface $container container + */ + public function __construct(ContainerInterface $container, $setParamsAsAttributes = false) + { + $this->container = $container; + $this->setParamsAsAttributes = $setParamsAsAttributes; + } + + /** + * Core controller handler + * + * @param FilterControllerEvent $event The event + */ + public function onKernelController(FilterControllerEvent $event) + { + $request = $event->getRequest(); + $queryFetcher = $this->container->get('fos_rest.request.query_fetcher'); + + $queryFetcher->setController($event->getController()); + $request->attributes->set('queryFetcher', $queryFetcher); + + if ($this->setParamsAsAttributes) { + $params = $queryFetcher->all(); + foreach ($params as $name => $param) { + if ($request->attributes->has($name)) { + $msg = sprintf("QueryFetcher parameter conflicts with a path parameter '$name' for route '%s'", $request->attributes->get('_route')); + throw new \InvalidArgumentException($msg); + } + + $request->attributes->set($name, $param); + } + } + } +} diff --git a/Request/QueryFetcher.php b/Request/QueryFetcher.php new file mode 100644 index 000000000..361463494 --- /dev/null +++ b/Request/QueryFetcher.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Request; + +use Symfony\Component\HttpFoundation\Request; + +/** + * Helper to validate query parameters from the active request. + * + * @author Alexander + * @author Lukas Kahwe Smith + */ +class QueryFetcher implements QueryFetcherInterface +{ + /** + * @var QueryParamReader + */ + private $queryParamReader; + + /** + * @var Request + */ + private $request; + + /** + * @var array + */ + private $params; + + /** + * @var callable + */ + private $controller; + + /** + * Initializes fetcher. + * + * @param QueryParamReader $queryParamReader Query param reader + * @param Request $request Active request + */ + public function __construct(QueryParamReader $queryParamReader, Request $request) + { + $this->queryParamReader = $queryParamReader; + $this->request = $request; + } + + /** + * @abstract + * @param callable $controller + * + * @return void + */ + public function setController($controller) + { + $this->controller = $controller; + } + + /** + * Get a validated query parameter. + * + * @param string $name Name of the query parameter + * @param Boolean $strict If a requirement mismatch should cause an exception + * + * @return mixed Value of the parameter. + */ + public function get($name, $strict = false) + { + if (null === $this->params) { + $this->initParams(); + } + + if (!array_key_exists($name, $this->params)) { + throw new \InvalidArgumentException(sprintf("No @QueryParam configuration for parameter '%s'.", $name)); + } + + $config = $this->params[$name]; + $default = $config->default; + $param = $this->request->query->get($name, $default); + + // Set default if the requirements do not match + if ($param !== $default && !preg_match('#^'.$config->requirements.'$#xs', $param)) { + if ($strict) { + throw new \RuntimeException("Query parameter value '$param', does not match requirements '{$config->requirements}'"); + } + + $param = $default; + } + + return $param; + } + + /** + * Get all validated query parameter. + * + * @param Boolean $strict If a requirement mismatch should cause an exception + * + * @return array Values of all the parameters. + */ + public function all($strict = false) + { + $params = array(); + foreach ($this->params as $name => $config) { + $params[$name] = $this->get($name, $strict); + } + + return $params; + } + + /** + * Initialize the parameters + * + * @throws \InvalidArgumentException + */ + private function initParams() + { + if (empty($this->controller)) { + throw new \InvalidArgumentException('Controller and method needs to be set via setController'); + } + + if (!is_array($this->controller) || empty($this->controller[0]) || !is_object($this->controller[0])) { + throw new \InvalidArgumentException('Controller needs to be set as a class instance (closures/functions are not supported)'); + } + + $this->params = $this->queryParamReader->read(new \ReflectionClass($this->controller[0]), $this->controller[1]); + } +} diff --git a/Request/QueryFetcherInterface.php b/Request/QueryFetcherInterface.php new file mode 100644 index 000000000..fb5c11f9e --- /dev/null +++ b/Request/QueryFetcherInterface.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Request; + +use Symfony\Component\HttpFoundation\Request; + +/** + * Helper interface to validate query parameters from the active request. + * + * @author Alexander + * @author Lukas Kahwe Smith + */ +interface QueryFetcherInterface +{ + /** + * @abstract + * @param callable $controller + * + * @return void + */ + function setController($controller); + + /** + * Get a validated query parameter. + * + * @param string $name Name of the query parameter + * @param Boolean $strict If a requirement mismatch should cause an exception + * + * @return mixed Value of the parameter. + */ + function get($name, $strict = false); + + /** + * Get all validated query parameter. + * + * @param Boolean $strict If a requirement mismatch should cause an exception + * + * @return array Values of all the parameters. + */ + function all($strict = false); +} diff --git a/Request/QueryParamReader.php b/Request/QueryParamReader.php new file mode 100644 index 000000000..ebd013800 --- /dev/null +++ b/Request/QueryParamReader.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Request; + +use Doctrine\Common\Annotations\Reader; +use FOS\RestBundle\Controller\Annotations\QueryParam; + +/** + * Class loading @QueryParameter annotations from methods. + * + * @author Alexander + * @author Lukas Kahwe Smith + */ +class QueryParamReader implements QueryParamReaderInterface +{ + private $annotationReader; + + /** + * Initializes controller reader. + * + * @param Reader $annotationReader annotation reader + */ + public function __construct(Reader $annotationReader) + { + $this->annotationReader = $annotationReader; + } + + /** + * Read annotations for a given method. + * + * @param \ReflectionClass $reflection Reflection class + * @param string $method Method name + * + * @return array QueryParam annotation objects of the method. Indexed by parameter name. + */ + public function read(\ReflectionClass $reflection, $method) + { + if (!$reflection->hasMethod($method)) { + throw new \InvalidArgumentException(sprintf("Class '%s' has no method '%s' method.", $reflection->getName(), $method)); + } + + return $this->getParamsFromMethod($reflection->getMethod($method)); + } + + /** + * Read annotations for a given method. + * + * @param \ReflectionMethod $method Reflection method + * + * @return array QueryParam annotation objects of the method. Indexed by parameter name. + */ + public function getParamsFromMethod(\ReflectionMethod $method) + { + $annotations = $this->annotationReader->getMethodAnnotations($method); + + $params = array(); + foreach ($annotations as $annotation) { + if ($annotation instanceof QueryParam) { + $params[$annotation->name] = $annotation; + } + } + + return $params; + } +} diff --git a/Request/QueryParamReaderInterface.php b/Request/QueryParamReaderInterface.php new file mode 100644 index 000000000..9fe07b7bd --- /dev/null +++ b/Request/QueryParamReaderInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Request; + +/** + * interface for loading query parameters for a method + * + * @author Alexander + * @author Lukas Kahwe Smith + */ +interface QueryParamReaderInterface +{ + /** + * Read annotations for a given method. + * + * @param \ReflectionClass $reflection Reflection class + * @param string $method Method name + * + * @return array QueryParam annotation objects of the method. Indexed by parameter name. + */ + function read(\ReflectionClass $reflection, $method); + + /** + * Read annotations for a given method. + * + * @param \ReflectionMethod $method Reflection method + * + * @return array QueryParam annotation objects of the method. Indexed by parameter name. + */ + function getParamsFromMethod(\ReflectionMethod $method); +} diff --git a/Resources/config/query_fetcher_listener.xml b/Resources/config/query_fetcher_listener.xml new file mode 100644 index 000000000..adba3d846 --- /dev/null +++ b/Resources/config/query_fetcher_listener.xml @@ -0,0 +1,23 @@ + + + + + + + FOS\RestBundle\EventListener\QueryFetcherListener + false + + + + + + + + + %fos_rest.query_fetch_listener.set_params_as_attributes% + + + + diff --git a/Resources/config/request.xml b/Resources/config/request.xml new file mode 100644 index 000000000..ec83fbf62 --- /dev/null +++ b/Resources/config/request.xml @@ -0,0 +1,27 @@ + + + + + + + FOS\RestBundle\Request\QueryFetcher + FOS\RestBundle\Request\QueryParamReader + + + + + + + + + + + + + + + + + diff --git a/Resources/config/routing.xml b/Resources/config/routing.xml index 11c9df76f..54e1aa5ad 100644 --- a/Resources/config/routing.xml +++ b/Resources/config/routing.xml @@ -47,6 +47,7 @@ + diff --git a/Resources/doc/3-listener-support.md b/Resources/doc/3-listener-support.md index 7b49e79ae..43a03436f 100644 --- a/Resources/doc/3-listener-support.md +++ b/Resources/doc/3-listener-support.md @@ -214,3 +214,64 @@ fos_rest: ## That was it! [Return to the index](index.md) or continue reading about [ExceptionController support](4-exception-controller-support.md). + +### Query fetcher listener + +The query fetcher listener simply sets the QueryFetcher instance as a request attribute +configured for the matched controller so that the user does not need to do this manually. + +```yaml +# app/config/config.yml +fos_rest: + query_fetcher_listener: true +``` + +```php +class FooController extends Controller +{ + /** + * Will look for a page query parameters, ie. ?page=XX + * If not passed it will be automatically be set to the default of "1" + * If passed but doesn't match the requirement "\d+" it will be also be set to the default of "1" + * Note that if the value matches the default then no validation is run. + * So make sure the default value really matches your expectations. + * @QueryParam(name="page", requirements="\d+", default="1", description="Page of the overview.") + * + * @param QueryFetcher $queryFetcher + */ + public function getArticlesAction(QueryFetcher $queryFetcher) + { + $page = $queryFetcher->get('page'); + $articles = array('bim', 'bam', 'bingo'); + + return array('articles' => $articles, 'page' => $page); + } +``` + +Note: There is also ``$queryFetcher->all()`` to fetch all configured query parameters at once. And also +both ``$queryFetcher->get()`` and ``$queryFetcher->all()`` support and optional ``$strict`` parameter +to throw a ``\RuntimeException`` on a validation error. + +Optionally the listener can also already set all configured query parameters as request attributes + +```yaml +# app/config/config.yml +fos_rest: + query_fetcher_listener: force +``` + +```php +class FooController extends Controller +{ + /** + * @QueryParam(name="page", requirements="\d+", default="1", description="Page of the overview.") + * + * @param string $page + */ + public function getArticlesAction($page) + { + $articles = array('bim', 'bam', 'bingo'); + + return array('articles' => $articles, 'page' => $page); + } +``` diff --git a/Routing/Loader/Reader/RestActionReader.php b/Routing/Loader/Reader/RestActionReader.php index cbcda78b2..f0b85b6fc 100644 --- a/Routing/Loader/Reader/RestActionReader.php +++ b/Routing/Loader/Reader/RestActionReader.php @@ -12,9 +12,12 @@ namespace FOS\RestBundle\Routing\Loader\Reader; use Doctrine\Common\Annotations\Reader; -use FOS\RestBundle\Util\Pluralization; + use Symfony\Component\Routing\Route; + +use FOS\RestBundle\Util\Pluralization; use FOS\RestBundle\Routing\RestRouteCollection; +use FOS\RestBundle\Request\QueryParamReader; /** * REST controller actions reader. @@ -24,6 +27,7 @@ class RestActionReader { private $annotationReader; + private $queryParamReader; private $routePrefix; private $namePrefix; @@ -37,9 +41,10 @@ class RestActionReader * * @param Reader $annotationReader annotation reader */ - public function __construct(Reader $annotationReader) + public function __construct(Reader $annotationReader, QueryParamReader $queryParamReader) { $this->annotationReader = $annotationReader; + $this->queryParamReader = $queryParamReader; } /** @@ -128,7 +133,8 @@ public function read(RestRouteCollection $collection, \ReflectionMethod $method) } // if we can't get http-method and resources from method name - skip - if (!($httpMethodAndResources = $this->getHttpMethodAndResourcesFromMethod($method))) { + $httpMethodAndResources = $this->getHttpMethodAndResourcesFromMethod($method); + if (!$httpMethodAndResources) { return; } @@ -168,7 +174,8 @@ public function read(RestRouteCollection $collection, \ReflectionMethod $method) $requirements = array('_method' => strtoupper($httpMethod)); $options = array(); - if ($annotation = $this->readRouteAnnotation($method)) { + $annotation = $this->readRouteAnnotation($method); + if ($annotation) { $annoRequirements = $annotation->getRequirements(); if (!isset($annoRequirements['_method'])) { @@ -200,6 +207,7 @@ private function isMethodReadable(\ReflectionMethod $method) if ('_' === substr($method->getName(), 0, 1)) { return false; } + // if method has NoRoute annotation - skip if ($this->readMethodAnnotation($method, 'NoRoute')) { return false; @@ -213,13 +221,13 @@ private function isMethodReadable(\ReflectionMethod $method) * * @param \ReflectionMethod $method * - * @return array + * @return Boolean|array */ private function getHttpMethodAndResourcesFromMethod(\ReflectionMethod $method) { // if method doesn't match regex - skip if (!preg_match('/([a-z][_a-z0-9]+)(.*)Action/', $method->getName(), $matches)) { - return; + return false; } $httpMethod = strtolower($matches[1]); @@ -239,13 +247,29 @@ private function getHttpMethodAndResourcesFromMethod(\ReflectionMethod $method) */ private function getMethodArguments(\ReflectionMethod $method) { - // ignore arguments that are or extend from Symfony\Component\HttpFoundation\Request + // ignore all query params + $params = $this->queryParamReader->getParamsFromMethod($method); + + // ignore type hinted arguments that are or extend from: + // * Symfony\Component\HttpFoundation\Request + // * FOS\RestBundle\Request\QueryFetcher + $ignoreClasses = array( + 'Symfony\Component\HttpFoundation\Request', + 'FOS\RestBundle\Request\QueryFetcherInterface', + ); + $arguments = array(); foreach ($method->getParameters() as $argument) { - if ($argumentClass = $argument->getClass()) { - if ($argumentClass->getName() === 'Symfony\Component\HttpFoundation\Request' - || $argumentClass->isSubclassOf('Symfony\Component\HttpFoundation\Request')) { - continue; + if (isset($params[$argument->getName()])) { + continue; + } + + $argumentClass = $argument->getClass(); + if ($argumentClass) { + foreach ($ignoreClasses as $class) { + if ($argumentClass->getName() === $class || $argumentClass->isSubclassOf($class)) { + continue 2; + } } } @@ -300,8 +324,7 @@ private function generateUrlParts(array $resources, array $arguments) strtolower(Pluralization::pluralize($resource)) .'/{'.$arguments[$i]->getName().'}'; } else { - $urlParts[] = - '{'.$arguments[$i]->getName().'}'; + $urlParts[] = '{'.$arguments[$i]->getName().'}'; } } elseif (null !== $resource) { $urlParts[] = strtolower($resource); @@ -326,13 +349,15 @@ private function getCustomHttpMethod($httpMethod, array $resources, array $argum // allow hypertext as the engine of application state // through conventional GET actions return 'get'; - } elseif (count($arguments) < count($resources)) { + } + + if (count($arguments) < count($resources)) { // resource collection return 'get'; - } else { - //custom object - return 'patch'; } + + //custom object + return 'patch'; } /** @@ -347,8 +372,6 @@ private function readRouteAnnotation(\ReflectionMethod $reflection) foreach (array('Route','Get','Post','Put','Patch','Delete','Head') as $annotationName) { if ($annotation = $this->readMethodAnnotation($reflection, $annotationName)) { return $annotation; - - break; } } } diff --git a/Tests/Request/QueryFetcherTest.php b/Tests/Request/QueryFetcherTest.php new file mode 100644 index 000000000..2549ab71e --- /dev/null +++ b/Tests/Request/QueryFetcherTest.php @@ -0,0 +1,175 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Tests\Request; + +use FOS\RestBundle\Controller\Annotations\NamePrefix; +use FOS\RestBundle\Controller\Annotations\QueryParam; +use FOS\RestBundle\Request\QueryParamReader; +use FOS\RestBundle\Request\QueryFetcher; +use Symfony\Component\HttpFoundation\Request; + +/** + * QueryParamReader test. + * + * @author Alexander + */ +class QueryFetcherTest extends \PHPUnit_Framework_TestCase +{ + private $controller; + private $queryParamReader; + + /** + * Test setup. + */ + public function setup() + { + $this->controller = array(new \stdClass(), 'indexAction'); + + $this->queryParamReader = $this->getMockBuilder('\FOS\RestBundle\Request\QueryParamReader') + ->disableOriginalConstructor() + ->getMock(); + + $annotations = array(); + $annotations['foo'] = new QueryParam; + $annotations['foo']->name = 'foo'; + $annotations['foo']->requirements = '\d+'; + $annotations['foo']->default = '1'; + $annotations['foo']->description = 'The foo'; + + $annotations['bar'] = new QueryParam; + $annotations['bar']->name = 'bar'; + $annotations['bar']->requirements = '\d+'; + $annotations['bar']->default = '1'; + $annotations['bar']->description = 'The bar'; + + $this->queryParamReader + ->expects($this->any()) + ->method('read') + ->will($this->returnValue($annotations)); + } + + /** + * Get a query fetcher. + * + * @param array $query Query parameters for the request. + * @param array $attributes Attributes for the request. + * + * @return QueryFetcher + */ + public function getQueryFetcher($query = array(), $attributes = null) + { + $attributes = $attributes ?: array('_controller' => __CLASS__.'::stubAction'); + + $request = new Request($query, array(), $attributes); + + return new QueryFetcher($this->queryParamReader, $request); + } + + /** + * Test valid parameters. + * + * @param string $expected Expected query parameter value. + * @param string $expectedAll Expected query parameter values. + * @param array $query Query parameters for the request. + * + * @dataProvider validatesConfiguredQueryParamDataProvider + */ + public function testValidatesConfiguredQueryParam($expected, $expectedAll, $query) + { + $queryFetcher = $this->getQueryFetcher($query); + $queryFetcher->setController($this->controller); + $this->assertEquals($expected, $queryFetcher->get('foo')); + $this->assertEquals($expectedAll, $queryFetcher->all()); + } + + /** + * Data provider for the valid parameters test. + * + * @return array Data + */ + public static function validatesConfiguredQueryParamDataProvider() + { + return array( + array('1', array('foo' => '1', 'bar' => '1'), array('foo' => '1')), + array('42', array('foo' => '42', 'bar' => '1'), array('foo' => '42')), + array('1', array('foo' => '1', 'bar' => '1'), array('foo' => 'bar')), + ); + } + + /** + * Throw exception on invalid parameters. + */ + public function testExceotionOnValidatesFailure() + { + $queryFetcher = $this->getQueryFetcher(array('foo' => 'bar')); + $queryFetcher->setController($this->controller); + + try { + try { + $queryFetcher->get('foo', true); + $this->fail('Fetching get() in strict mode did not throw an exception'); + } catch (\RuntimeException $e) { + try { + $queryFetcher->all(true); + $this->fail('Fetching all() in strict mode did not throw an exception'); + } catch (\RuntimeException $e) { + return; + } + } + } catch (\Exception $e) { + $this->fail('Fetching in strict mode did not throw an \RuntimeException'); + } + } + + /** + * @expectedException LogicException + * @expectedExceptionMessage Controller and method needs to be set via setController + */ + public function testExceptionOnRequestWithoutController() + { + $queryFetcher = new QueryFetcher($this->queryParamReader, new Request()); + $queryFetcher->get('qux', '42'); + } + + /** + * @expectedException LogicException + * @expectedExceptionMessage Controller and method needs to be set via setController + */ + public function testExceptionOnNoController() + { + $queryFetcher = $this->getQueryFetcher(); + $queryFetcher->setController(array()); + $queryFetcher->get('qux', '42'); + } + + /** + * @expectedException LogicException + * @expectedExceptionMessage Controller needs to be set as a class instance (closures/functions are not supported) + */ + public function testExceptionOnNonController() + { + $queryFetcher = $this->getQueryFetcher(); + $queryFetcher->setController(array('foo', 'bar')); + $queryFetcher->get('qux', '42'); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage No @QueryParam configuration for parameter 'qux'. + */ + public function testExceptionOnNonConfiguredQueryParameter() + { + $queryFetcher = $this->getQueryFetcher(); + $queryFetcher->setController($this->controller); + $queryFetcher->get('qux', '42'); + } +} diff --git a/Tests/Request/QueryParamReaderTest.php b/Tests/Request/QueryParamReaderTest.php new file mode 100644 index 000000000..4f9fbb3a3 --- /dev/null +++ b/Tests/Request/QueryParamReaderTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Tests\Request; + +use FOS\RestBundle\Controller\Annotations\NamePrefix; +use FOS\RestBundle\Controller\Annotations\QueryParam; +use FOS\RestBundle\Request\QueryParamReader; + +/** + * QueryParamReader test. + * + * @author Alexander + */ +class QueryParamReaderTest extends \PHPUnit_Framework_TestCase +{ + private $queryParamReader; + + /** + * Test setup. + */ + public function setup() + { + $annotationReader = $this->getMock('\Doctrine\Common\Annotations\Reader'); + + $annotations = array(); + $foo = new QueryParam; + $foo->name = 'foo'; + $foo->requirements = '\d+'; + $foo->description = 'The foo'; + $annotations[] = $foo; + + $bar = new QueryParam; + $bar->name = 'bar'; + $bar->requirements = '\d+'; + $bar->description = 'The bar'; + $annotations[] = $bar; + + $annotations[] = new NamePrefix(array()); + + $annotationReader + ->expects($this->any()) + ->method('getMethodAnnotations') + ->will($this->returnValue($annotations)); + + $this->queryParamReader = new QueryParamReader($annotationReader); + } + + /** + * Test that only QueryParam annotations are returned. + */ + public function testReadsOnlyQueryParamAnnotations() + { + $annotations = $this->queryParamReader->read(new \ReflectionClass(__CLASS__), 'setup'); + + $this->assertCount(2, $annotations); + + foreach ($annotations as $name => $annotation) { + $this->assertThat($annotation, $this->isInstanceOf('FOS\RestBundle\Controller\Annotations\QueryParam')); + $this->assertEquals($annotation->name, $name); + } + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Class 'FOS\RestBundle\Tests\Request\QueryParamReaderTest' has no method 'foo' method. + */ + public function testExceptionOnNonExistingMethod() + { + $this->queryParamReader->read(new \ReflectionClass(__CLASS__), 'foo'); + } +} diff --git a/Tests/Routing/Loader/LoaderTest.php b/Tests/Routing/Loader/LoaderTest.php index cb1736eef..343cf18c3 100644 --- a/Tests/Routing/Loader/LoaderTest.php +++ b/Tests/Routing/Loader/LoaderTest.php @@ -18,6 +18,7 @@ use FOS\RestBundle\Routing\Loader\RestRouteLoader; use FOS\RestBundle\Routing\Loader\Reader\RestControllerReader; use FOS\RestBundle\Routing\Loader\Reader\RestActionReader; +use FOS\RestBundle\Request\QueryParamReader; /** * Base Loader testing class. @@ -51,8 +52,9 @@ protected function getControllerLoader() ->getMock(); $annotationReader = $this->getAnnotationReader(); + $queryParamReader = new QueryParamReader($annotationReader); - $ar = new RestActionReader($annotationReader); + $ar = new RestActionReader($annotationReader, $queryParamReader); $cr = new RestControllerReader($ar, $annotationReader); return new RestRouteLoader($c, $p, $cr, 'html');