Skip to content
This repository

Query param fetcher #185

Merged
merged 49 commits into from almost 2 years ago

11 participants

Lukas Kahwe Smith Christophe Coevoet Victor Berchet Gordon Franke Olmo Maldonado Tobias Schultze Alexander Grayson Koonce Even André Fiskvik Markus Tacker Johannes
Lukas Kahwe Smith
Owner
Christophe Coevoet
Owner

does the QueryParam stuff depend on FrameworkExtraBundle currently ?

Lukas Kahwe Smith
Owner

no .. currently it does not. but i don't really like having to use a listener to set the request attribute.

Request/QueryFetcher.php
((52 lines not shown))
  52
+    public function __construct(ContainerInterface $container, QueryParamReader $queryParamReader, Request $request)
  53
+    {
  54
+        $this->container = $container;
  55
+        $this->queryParamReader = $queryParamReader;
  56
+        $this->request = $request;
  57
+    }
  58
+
  59
+    private function initParams()
  60
+    {
  61
+        $_controller = $this->request->attributes->get('_controller');
  62
+
  63
+        if (null === $_controller) {
  64
+            throw new \InvalidArgumentException('No _controller for request.');
  65
+        }
  66
+
  67
+        if (false !== strpos($_controller, '::')) {
10
Victor Berchet
vicb added a note February 07, 2012

Could you leverage the ControllerResolver ?

Lukas Kahwe Smith Owner

yeah maybe ..

Lukas Kahwe Smith Owner

looking at the code it seems like it would be inefficient for the case when the controller is not implemented as a service. but it looks like we are lacking support for a:b:c notation atm.

Victor Berchet
vicb added a note February 07, 2012

That would be more dry and future proof

Lukas Kahwe Smith Owner

ok .. we should now cover all the cases

Lukas Kahwe Smith Owner

yeah it would .. but i don't think the overhead makes this legitimate.

Victor Berchet
vicb added a note February 07, 2012

could the controller event help with efficiency ?

Lukas Kahwe Smith Owner

hmm it could be a possibility. in that case we would move the other listener from a request listener to a controller listener as well. in that case of course the parameters would no longer be available in any listeners run before the controller, but i guess that would be ok.

Lukas Kahwe Smith Owner

ok .. i have an implementation using a controller event .. will push that into a new branch

Lukas Kahwe Smith Owner

see #186

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Request/QueryFetcher.php
((88 lines not shown))
  88
+     */
  89
+    public function getParameter($name)
  90
+    {
  91
+        if (!isset($this->params)) {
  92
+            $this->initParams();
  93
+        }
  94
+
  95
+        if (!isset($this->params[$name])) {
  96
+            throw new \InvalidArgumentException(sprintf("No @QueryParam configuration for parameter '%s'.", $name));
  97
+        }
  98
+
  99
+        $param = $this->request->query->get($name, $this->params[$name]->default);
  100
+
  101
+        // Set default if the requirements do not match
  102
+        if ($param !== $this->params[$name]->default
  103
+            && !preg_match('/^' . $this->params[$name]->requirements . '/xs', $param)
1
Victor Berchet
vicb added a note February 07, 2012

is '$' missing on purpose ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Request/QueryFetcher.php
((90 lines not shown))
  90
+    {
  91
+        if (!isset($this->params)) {
  92
+            $this->initParams();
  93
+        }
  94
+
  95
+        if (!isset($this->params[$name])) {
  96
+            throw new \InvalidArgumentException(sprintf("No @QueryParam configuration for parameter '%s'.", $name));
  97
+        }
  98
+
  99
+        $param = $this->request->query->get($name, $this->params[$name]->default);
  100
+
  101
+        // Set default if the requirements do not match
  102
+        if ($param !== $this->params[$name]->default
  103
+            && !preg_match('/^' . $this->params[$name]->requirements . '/xs', $param)
  104
+        ) {
  105
+            $param = $this->params[$name]->default;
1
Victor Berchet
vicb added a note February 07, 2012

factorize $default

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Request/QueryFetcher.php
((84 lines not shown))
  84
+     *
  85
+     * @param string $name    Name of the query parameter
  86
+     *
  87
+     * @return mixed Value of the parameter.
  88
+     */
  89
+    public function getParameter($name)
  90
+    {
  91
+        if (!isset($this->params)) {
  92
+            $this->initParams();
  93
+        }
  94
+
  95
+        if (!isset($this->params[$name])) {
  96
+            throw new \InvalidArgumentException(sprintf("No @QueryParam configuration for parameter '%s'.", $name));
  97
+        }
  98
+
  99
+        $param = $this->request->query->get($name, $this->params[$name]->default);
3
Victor Berchet
vicb added a note February 07, 2012

using has() would allow always validating against the requirement (thinking of the default value here but not sure of what is the desired behavior)

Lukas Kahwe Smith Owner

that $param !== $default further down is just a performance optimization to avoid the regexp if it isn't needed. so i think the code is ok as is.

Victor Berchet
vicb added a note February 07, 2012

just wanted to make sure you were aware of the edge case here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
EventListener/QueryFetcherListener.php
((33 lines not shown))
  33
+     * @param   ContainerInterface $container container
  34
+     */
  35
+    public function __construct(ContainerInterface $container)
  36
+    {
  37
+        $this->container = $container;
  38
+    }
  39
+
  40
+    /**
  41
+     * Core request handler
  42
+     *
  43
+     * @param   GetResponseEvent   $event    The event
  44
+     */
  45
+    public function onKernelRequest(GetResponseEvent $event)
  46
+    {
  47
+        $request = $event->getRequest();
  48
+        $request->attributes->set('queryFetcher', $this->container->get('fos_rest.request.query_fetcher'));
2
Victor Berchet
vicb added a note February 07, 2012

do we really need a listener, can't it be lazy loaded (just a thought, too late to check for now)

Lukas Kahwe Smith Owner

we need a listener to set the request attribute, so that it can be injected via the action signature, which is imperative to controllers that do not inject the DIC ..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Christophe Coevoet

closures and fonctions won't be catched here as they are not arrays. The first if should only check for null (or eventually for is_callable if you want)

Victor Berchet

Some thoughts:

Is the queryFetcher a good idea ? Let's imagine I do some processing in my controller based on the query parameters and then I call doSomeGreatStuff($request). This function would get the query parameters as $request->query->... and then we have a mismatch. Would it be possible to drop the queryFetcher and replace (or update) the query parameter bag instead so that the API does not get changed ?

I can imagine some day we have a router component that is aware of the query parameters. This PR could not work as the parameters are processed in the controller only. So I would prefer if the changes are moved to the router layer.

Lukas Kahwe Smith
Owner

Overriding the Request parameter bag would remove the need to set the request attribute. How would it behave in that case though? I guess it would then only overload get() to set the default from the annotation (what would happen if a default is passed?) and execute the given requirements checks from the annotation?

Lukas Kahwe Smith
Owner

the other question would be where and when do we override the default parameter bag? would this require a custom Request class or a listener again?

Victor Berchet

@lsmith77

  • What I can imagine is to update the parameters in the bag and then use the bag as before.
  • I would go for the router layer for the reason explained in my previous message. I had first thought of a custom bag class but I don't even think it is necessary (see my point above).
Lukas Kahwe Smith
Owner

Well I see some major issues here:
1) it would mean we do a lot of work that may not become necessary
2) if we do it in the router, then we will have the issue with determining the class/method again

Victor Berchet

Well maybe the best place is the controller event. IMO the most important thing is to drop the $queryFetcher and be able to use doSomeGreatStuff($request). I let you work out the details.

Lukas Kahwe Smith
Owner

If we do this, then we would best wrap the standard ParameterBag inside one that automatically sets the defaults and checks the requirements when any of the get*() methods are called. However this may also lead to a bit of unclearity as to when these requirements are applied. Aka sometimes when you use the request it would apply the requirements .. but for example inside a request listener they would not. I think this makes things more obscure, so I prefer the current approach.

Victor Berchet

That might indeed be a bad idea but the current implementation is even worse then. The solution would be to change to router to support matching query params.

Lukas Kahwe Smith
Owner

I don't see the current implementation as worse at all. It just means that adopting this approach requires to change some API calls.

Now changing the router is of course another option. But I am not sure if this is possible without either not supporting all the dumpers or increasing the scope of this PR to require a gigantic amount of work. Then again I guess we will get this once we have uri-template support:
http://tools.ietf.org/html/draft-gregorio-uritemplate-08
symfony/symfony#3227

Lukas Kahwe Smith
Owner

Btw .. in this case we would of course get around the entire controller setting business, since we would add this information into the routes. Thinking about it .. maybe we could do this today as well? Aka instead of reading the annotations at runtime, we could integrate the query param annotations into the rest route loader and write this information in there somewhere (not sure if its possible) and then read it out in the query fetcher ..

Victor Berchet

I think you mentioned that the code should work after re-factoring. Keeping the $queryFetcher prevent this, right ? Passing it as a parameter does not solve the solution either: it would introduce coupling and legacy (/3rd party) services would not work.

Lukas Kahwe Smith
Owner

Prevents what? Which refactoring do you mean specifically?

Victor Berchet

Moving some code from an action to a service (i.e. doSomeGreatStuff($request)). Wasn't that you were speaking about.
Anyway the other part doSomeLegacyStuff($request), doSome3rdPartyStuff($request) is still valid.

Lukas Kahwe Smith
Owner

Ah yes. Any service after the controller listener can inject the query fetcher as it is now and things will work cleanly. Any server before the controller listener will get an exception. So no surprises.

Gordon Franke

any news?

Lukas Kahwe Smith
Owner

basically we have decided we want to move the matching of the query parameters to the routing layer, which would be the case once we have full support for uri-templates. now the question is if we still merge the current state and then simply break BC once uri-template support is available. or one of us works on uri-template support. should not be toooo hard and would be a huge boost for Symfony2 in general.

Olmo Maldonado

Could you piggy back on @Route?

Lukas Kahwe Smith
Owner

i have been thinking some more about this over the weekend also in regards to uri-templates. one thing that i finally realized is that path parameters requirements will need to be handled differently than query parameters requirements. especially for the later it should not prevent a route match and it will usually require custom code to handle errors.

CC @Tobion

Lukas Kahwe Smith
Owner

ok .. unless someone brings up a new concern .. i will soon merge this PR

Controller/Annotations/QueryParam.php
((10 lines not shown))
  10
+ */
  11
+
  12
+namespace FOS\RestBundle\Controller\Annotations;
  13
+
  14
+/**
  15
+ * QueryParam annotation class.
  16
+ *
  17
+ * @Annotation
  18
+ * @author Alexander <iam.asm89@gmail.com>
  19
+ */
  20
+class QueryParam
  21
+{
  22
+    public $name;
  23
+    public $requirements;
  24
+    public $default;
  25
+    public $description;
2
Christophe Coevoet Owner
stof added a note May 05, 2012

you should probably add the annotations allowing the AnnotationReader to validate annotations as of Common 2.2 (see the ORM or @schmittjoh's bundles for instance)

Lukas Kahwe Smith Owner
lsmith77 added a note May 16, 2012

fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Christophe Coevoet stof commented on the diff May 05, 2012
DependencyInjection/FOSRestExtension.php
@@ -128,6 +129,10 @@ public function load(array $configs, ContainerBuilder $container)
128 129
         } else {
129 130
             $container->setParameter($this->getAlias().'.mime_types', array());
130 131
         }
  132
+
  133
+        if (!empty($config['query_fetcher_listener'])) {
3
Christophe Coevoet Owner
stof added a note May 05, 2012

you could use if ($config['query_fetcher_listener']) { as you will always have a boolean (the node is a booleanNode and it has a default value)

Lukas Kahwe Smith Owner
lsmith77 added a note May 16, 2012

this would mean i need to write a lot more code into the tests .. its fine like it is.

Christophe Coevoet Owner
stof added a note May 16, 2012

well, my comment is not really accurate anymore as it is not a boolean anymore (because of force)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Request/QueryFetcher.php
((67 lines not shown))
  67
+            throw new \InvalidArgumentException('Controller needs to be set as a class instance (closures/functions are not supported)');
  68
+        }
  69
+
  70
+        $this->params = $this->queryParamReader->read(new \ReflectionClass($this->controller[0]), $this->controller[1]);
  71
+    }
  72
+
  73
+    /**
  74
+     * Get a validated query parameter.
  75
+     *
  76
+     * @param string $name    Name of the query parameter
  77
+     *
  78
+     * @return mixed Value of the parameter.
  79
+     */
  80
+    public function getParameter($name)
  81
+    {
  82
+        if (!isset($this->params)) {
2
Christophe Coevoet Owner
stof added a note May 05, 2012

I would use if (null === $this->params) {

Lukas Kahwe Smith Owner
lsmith77 added a note May 16, 2012

fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Request/QueryParamReader.php
((7 lines not shown))
  7
+ *
  8
+ * For the full copyright and license information, please view the LICENSE
  9
+ * file that was distributed with this source code.
  10
+ */
  11
+
  12
+namespace FOS\RestBundle\Request;
  13
+
  14
+use Doctrine\Common\Annotations\Reader;
  15
+use FOS\RestBundle\Controller\Annotations\QueryParam;
  16
+
  17
+/**
  18
+ * Class loading @QueryParameter annotations from methods.
  19
+ *
  20
+ * @author Alexander <iam.asm89@gmail.com>
  21
+ */
  22
+class QueryParamReader
2
Christophe Coevoet Owner
stof added a note May 05, 2012

I think you should have an interface for the QueryParamReader, allowing people to have another implementation reading from another source than annotations

Lukas Kahwe Smith Owner
lsmith77 added a note May 16, 2012

fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Request/QueryFetcher.php
((45 lines not shown))
  45
+     *
  46
+     * @param QueryParamReader      $queryParamReader Query param reader
  47
+     * @param Request               $request          Active request
  48
+     */
  49
+    public function __construct(QueryParamReader $queryParamReader, Request $request)
  50
+    {
  51
+        $this->queryParamReader = $queryParamReader;
  52
+        $this->request = $request;
  53
+    }
  54
+
  55
+    public function setController($controller)
  56
+    {
  57
+        $this->controller = $controller;
  58
+    }
  59
+
  60
+    private function initParams()
2
Christophe Coevoet Owner
stof added a note May 05, 2012

private methods should be declared after public ones

Lukas Kahwe Smith Owner
lsmith77 added a note May 16, 2012

fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Tobias Schultze
Tobion commented May 05, 2012

I agree that query param requirements shouldn't prevent route matching. As far as I see the requirements for params have two use cases:

  • I like that the default value is returned if a requirement for a param doesn't match.
  • I guess specifiying the requirement for a param is also used for API documentation.

But I don't see what this PR has to do with URI templates, that was also discussed here. And I thought we might plan to support query params in the routing component directly. There I would implement this quite differently (without using QueryFetcher etc).

Lukas Kahwe Smith
Owner

See my comment above. Imho the handling of what is supposed to happen in case of a mismatch of a query parameter will usually require specific logic that cannot be sensible defined via configuration in the routing. As such imho it should be handled in the controller. All this PR does is make it easier to do so.

As for uri-templates, the point again wasn't full uri-template support, but simply query parameter support. However as I just noted I don't think that the routing configuration is the right place to handle query parameters entirely. F.e. adding a requirement in the routing would cause the route to not match. Setting defaults makes a bit more sense via the routing layer, but without being able to handle setting a default on a mismatch etc its again not flexible enough.

Tobias Schultze
Tobion commented May 05, 2012

I think this is all easiliy possible in the routing, even without breaking BC. I image a new option, besides pattern, for specifying query params: let's call it query. Example:

blog_show:
    pattern:   /blog/{slug}
    query: 
        param1: {param1}
        param2: {param2}
    defaults:  { param1: 1 }
    requirements:
        param1:  \d+

As you can see, we can simply use the existing default and requirements sections and introduce a new option for queries (similar to what is planned to be merged for host requirements). But a requirement for a param in the query section will of course not be used for matching. The query params will be passed as arguements to the controller action (only the ones specified, not all). And for validating them and returning the default on mismatch can be achieved by introducing a new method in Request (in addition to the standard $request->get()). This will allow a developer to implement any specific logic in the controller he wants.

That way, it will also make the implementation independent of where the parameter comes from (path, hostname or query). So one can simply change the route without needing to adjust any code. For example we decide to change the location of param2 in the above route and we are done:

blog_show:
    pattern:   /blog/{slug}/{param2}
    query: 
        param1: {param1}
    defaults:  { param1: 1 }
    requirements:
        param1:  \d+
Lukas Kahwe Smith
Owner

yeah .. but isn't that super confusing that the behavior of requirements changes based on where it matched? seems like a huge security risk.

Tobias Schultze
Tobion commented May 05, 2012

I don't think so. Moving a param from query to path is even more restrictive. And moving from path to query will validate the requirement and pass the default to the action if it doesnt match. And if you have a non-matching requirement but not configured a default neither in the route nor in the action (fooAction($param1)) it will create a 404 (or a specific exception). You can of course specify a default in the action to circumvent the 404 and do your custom validation and logic in the controller.

Lukas Kahwe Smith
Owner

its still a change in behavior that isn't very transparent to the user

Alexander
Owner
asm89 commented May 05, 2012

The code hasn't changed a lot from the original PoC I send. I think that if this would get merged it would be supporting the original idea? #178 In short the idea was to support the developer with checking and documenting query parameters used in api controller actions.

Tobias Schultze
Tobion commented May 05, 2012

I don't see any other reasonable way to implement query params in the routing component. And it's basically the same configuration style as with the upcoming hostname-pattern option. So quite straightforward. How would you like to see it implemented in core?

Lukas Kahwe Smith
Owner

well imho setting a requirement on a query param should cause an exception. which is why i was considering merging this PR. however maybe we could have a "validation" section that behaves like what we described above for query parameters.

Tobias Schultze
Tobion commented May 05, 2012

I think having both a requirements (for path params and hostname params) and a validation (for query params) option makes it even more confusing. People would think: Are requirements not validated? Do I need to place a param in both sections to be sure?
Btw, in your current implementation your option is called requirement, too. So also the same option name with different meaning (in @Route and in @QueryParam

Lukas Kahwe Smith
Owner

but its not the same configuration. the difference is that people now know how requirements in the route work. we can't just have a different behavior depending on if its a path or query parameter. the only thing we could do is disallow using requirements for query parameters. but then we would still need a way to define such validation rules. now handling of default values could indeed be done via the routing.

however right now the routing is focused on the matching process only. while i was the one suggesting to add query support to the routing layer initially .. i am not so sure it makes sense now after all.

Tobias Schultze
Tobion commented May 05, 2012

My concern is, that the main point of configuring routes is that it hides the implementation. So currently you can change the order of path params or change a prefix etc. without touching code for url matching and url generation. It will still work.
Same with the hostname PR: You will be able to change the locale from a hostname {_locale}.example.org to the path without problems.
But this implementation for query params can only be used for query params. You cannot even change the param name without modifying all code. E.g. you introcude a typo for a query name or simply want to change it, like ?query=value to ?q=value. It's not possible without rewriting code.

With my idea you can simply reconfigure it like

query: 
    q: {query}

and you're done. No need to touch the templates etc.

Btw query: [param1, param2, param3] should be a shorthand for query: { param1: {param1}, param2: {param2}, param3: {param3} }. And one could do stuff like query: { price: {amout}{currency} }.

Lukas Kahwe Smith
Owner

well sure that could be nice in order to deal with legacy or 3rd party controllers. but i don't really think that the ability to alias query param names is something we really need in core.

what i do think we need to provide for either in core or in this bundle is the ability to:

  • define the query parameters supported by a controller action
  • define the default values for this parameters if they are not supplied
  • define validation rules for these parameters (note that here in most causes falling back to a default is the right thing, but in other cases i might also want some assistance in determine what doesn't match so that i can return a custom error message).
Grayson Koonce
breerly commented May 13, 2012

this is wicked sexy, btw.

Even André Fiskvik
grEvenX commented May 14, 2012

This rocks @lsmith77 , what's does the fortuneteller say about when this stuff is ready to be merged into master? And what's left before that can happen?

Lukas Kahwe Smith
Owner

as the example shows .. it basically works ..
now .. the main question is if we agree its the right approach ..
or should this rather be done inside the routing config?

Even André Fiskvik
grEvenX commented May 14, 2012

From my personal view, I think this approach is just fine. But I'm not a Symfony advocate, thus my points might/should not count for much in this case. But as you said, this works, and isn't that good enough for a first version.
Couldn't the possibility to configure it through routing config be added as an option at a later stage if there is actually need for it?

Lukas Kahwe Smith
Owner

sure it could be added later .. or replaced with .. would just like to avoid needless confusion

Even André Fiskvik
grEvenX commented May 14, 2012

Yes, but isn't that the same as for Route configuration. The developer has a choice, either through routing config or annotations?

Even André Fiskvik
grEvenX commented May 14, 2012

Btw, trying to test this out, but I get "<CLASS> requires that you provide a value for the "$queryFetcher" argument (because there is no default value or because there is a non optional argument after this one)." when I use first parameter with QueryFetcher. I've also added the QueryParam annotation to the method. It's based on the same as your example but using annotations to build the Route etc.
What is needed in order for the QueryFetcher to be injected when the method is called?

Even André Fiskvik
grEvenX commented May 14, 2012

Thanks for pointing that out, works now :) now off to do some real testing

Markus Tacker

I also don't think the query parameter configuration should be part of the routing configuration as this tends to change quite often and would add a lot of noise to the routing config and add more ways to introduce errors to that file.

I'd favour a more expressive solution than having one QueryParamReader which behaves essentially like the Request.

If one decides to mark up his desired query params he should get them nicely laid out for him:

/**
 * Get the list of articles
 *
 * @param int $page
 * @QueryParam(name="page", requirements="\d+", default="1", description="Page of the overview.")
 */
public function getArticlesAction($page)
{

This perfectly aligns to the request parameter configuration where you also get to use the named parameter from the routing configuration.

Lukas Kahwe Smith
Owner

We can easily create a request listener that does that or rather extend the current one to optionally do that. The reason I say optionally is that with what you describe all parameters would always have to be parsed and validated even if they are not used, but in most cases most of the configured query params will likely be used anyway.

Markus Tacker

Of course one could image getting some query params only conditinally:

if ($search = $queryFetcher->getParameter('search')) {
    $term = $queryFetcher->getParameter('term');
}

but I personally would just fetch all the queryparams in one block anyway and deal with them conditionally later.

$search = $queryFetcher->getParameter('search');
$term = $queryFetcher->getParameter('term');
…
if ($search) { … }
Even André Fiskvik
grEvenX commented May 15, 2012

I agree on the expressability part of injecting the query params.
If one defines a route with parameters and also uses queryparam, how will it affect the method signature and which (if any) would take precedence?

E.g:

/*
 * @Route("/{id}", requirements={"id" = "\d+"})
 * @QueryParam(name="id",requirements={"id" = "\d+"})
 */
public function getCustomerAction($id) 
Christophe Coevoet
Owner
stof commented May 15, 2012

@grEvenX The bundle does not change the request attributes, and so will not change the params available for the method arguments.

Lukas Kahwe Smith
Owner

@stof but we could ..

Markus Tacker

@grEvenX I think a conflict should raise an exception.

Christophe Coevoet
Owner
stof commented May 15, 2012

it would duplicate the param in 2 parameter bags (query and attributes). I don't think it is a good idea

Alexander
Owner
asm89 commented May 15, 2012

Honestly I think it doesn't make sense to inject the query parameters in your method signature. That would make your code even less portable. My original idea was that the query param thing could help you with checking very basic requirements on the query parameters (like &page should be \d+) and return a default value (1 for page) or null otherwise. The upside of this would be less checking of stuff like that "by hand", but also having controller methods annotated with the appropriate query parameters which is very nice for api documentation etc.

Markus Tacker

That would make your code even less portable.

Can you elaborate?

Do you mean, by hard coding the query params in the method signature one limits possibility of extending the method in a subclass with a different implementation which would require other params?

Lukas Kahwe Smith
Owner

ok .. i have implemented the option of forcing the query params to be set as attributes. on conflict it throws an exception. however we also need to improve the route generation to skip any configured query parameter.

Lukas Kahwe Smith
Owner

btw .. just FYI .. you can always fork this branch and submit PRs back to this branch if you have ideas for improvements. for example it would be cool of someone could fix the above noted issue with using query params in the method signature causing them to be put into the route.

Lukas Kahwe Smith
Owner

nevermind .. already took care of it ..
anything else people think should be done?

Lukas Kahwe Smith
Owner

main thing missing now are docs ..

Christophe Coevoet
Owner
stof commented May 15, 2012

@lsmith77 As I already said a while ago, I think you should add an interface for the QueryParamReader and use it in the typehints, to allow people to replace the implementation if they want.

Lukas Kahwe Smith
Owner

QueryParamReader or QueryFetcher? Anyway, I will leave that to who ever wants to make the first alternative implementation :)

Christophe Coevoet
Owner
stof commented May 15, 2012

probably both :)
But I was talking about the reader first, thus allowing people to write an implementation reading from elsewhere than annotations if they don't like annotations

DependencyInjection/Configuration.php
@@ -39,6 +39,7 @@ public function getConfigTreeBuilder()
39 39
 
40 40
         $rootNode
41 41
             ->children()
  42
+                ->scalarNode('query_fetcher_listener')->defaultValue(false)->end()
5
Christophe Coevoet Owner
stof added a note May 16, 2012

shouldn't it be a booleanNode ? And you could use defaultFalse()

Christophe Coevoet Owner
stof added a note May 16, 2012

ok, it is not a boolean. But you should probably limit the possible values

Lukas Kahwe Smith Owner
lsmith77 added a note May 16, 2012

whats the syntax for that?

Lukas Kahwe Smith Owner
lsmith77 added a note May 16, 2012

fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
DependencyInjection/Configuration.php
@@ -39,6 +41,12 @@ public function getConfigTreeBuilder()
39 41
 
40 42
         $rootNode
41 43
             ->children()
  44
+                ->scalarNode('query_fetcher_listener')->defaultFalse()
  45
+                    ->validate()
  46
+                    ->ifNotInArray($this->forceOptionValues)
  47
+                    ->thenInvalid('The query_fetcher_listener option does not support %s. Please choose one of '.json_encode($this->forceOptionValues))
1
Christophe Coevoet Owner
stof added a note May 16, 2012

missing one indentation level

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
DependencyInjection/Configuration.php
@@ -111,7 +119,12 @@ private function addViewSection(ArrayNodeDefinition $rootNode)
111 119
                             ->defaultValue(array('html' => true))
112 120
                             ->prototype('boolean')->end()
113 121
                         ->end()
114  
-                        ->scalarNode('view_response_listener')->defaultValue('force')->end()
  122
+                        ->scalarNode('view_response_listener')->defaultValue('force')
  123
+                            ->validate()
  124
+                            ->ifNotInArray($this->forceOptionValues)
  125
+                            ->thenInvalid('The view_response_listener option does not support %s. Please choose one of '.json_encode($this->forceOptionValues))
1
Christophe Coevoet Owner
stof added a note May 16, 2012

same here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Lukas Kahwe Smith lsmith77 merged commit 0aa7534 into from May 16, 2012
Lukas Kahwe Smith lsmith77 closed this May 16, 2012
Johannes
Owner

About silently setting the default value on validation failure, would it not make more sense to redirect to the URL with the default value? Also in terms of SEO, and duplicate content, this seems more sensible, no?

Lukas Kahwe Smith
Owner

i thought about that too .. we could provide such an option as part of the listener eventually. it should of course only be down for proper GET requests. however i don't think that query parameters are that important for SEO, but it is relevant for caches i guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 49 unique commits by 2 authors.

Jan 31, 2012
Alexander Proof-of-concept for @QueryParam validation 631fe13
Feb 01, 2012
Alexander Add tests 8c04a84
Feb 07, 2012
Alexander requires -> requirements to be consistent with sf2 192f3ae
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept bfa891e
Lukas Kahwe Smith added support for defaults via annotations e32e078
Lukas Kahwe Smith added support for controllers as a service, moved logic out of the co…
…nstructor
80d0da4
Lukas Kahwe Smith added support for passing the QueryFetcher as an action parameter 7cfa318
Lukas Kahwe Smith fix tests 25517a6
Lukas Kahwe Smith cosmetic tweaks fd1ae02
Lukas Kahwe Smith fixed regexp plus a few cosmetics b03414b
Lukas Kahwe Smith cover all possible controller definition cases 937e273
Feb 08, 2012
Lukas Kahwe Smith use a controller listener e7fe284
Lukas Kahwe Smith tweaked error handling df59842
Lukas Kahwe Smith Merge pull request #186 from FriendsOfSymfony/query_param-proof-of-co…
…ncept2

Query param proof of concept with a controller listener
ebbfb5c
Lukas Kahwe Smith tweaked error handling a52e85d
Feb 22, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept 1cc4ccc
Mar 02, 2012
Lukas Kahwe Smith Merge branch 'use_rest_lib' into query_param-proof-of-concept e430694
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept bf75439
Mar 24, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept 8e19e8f
Lukas Kahwe Smith updated config reference 370e806
Mar 29, 2012
Lukas Kahwe Smith Merge remote-tracking branch 'origin/master' into query_param-proof-o…
…f-concept
e41dd47
Mar 30, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept 8dfb9c4
Apr 02, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept f81bac8
Apr 03, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept f39ea93
Apr 10, 2012
Lukas Kahwe Smith Merge remote-tracking branch 'origin/master' into query_param-proof-o…
…f-concept
b784c43
Apr 11, 2012
Lukas Kahwe Smith Merge remote-tracking branch 'origin/master' into query_param-proof-o…
…f-concept
49f68b2
Apr 12, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept 89ece6e
Apr 13, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept f07bd7a
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept ed4aaae
Apr 15, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept a6d434f
Apr 18, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept e65bb81
Apr 19, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept ddcc3fa
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept 3dc09ba
May 13, 2012
Lukas Kahwe Smith Merge branch 'master' into query_param-proof-of-concept 9d4c66a
May 15, 2012
Lukas Kahwe Smith renamed QueryFetcher::getParameter() to QueryFetcher::get() and added…
… QueryFetcher::all()
39da182
Lukas Kahwe Smith made it possible to set the query parameters as request attributes 9cbb25d
Lukas Kahwe Smith ignore query params when generating routes 0671c11
Lukas Kahwe Smith cosmetics 199f42e
Lukas Kahwe Smith added documentation 3406ebd
Lukas Kahwe Smith fix tests 87ccc2b
May 16, 2012
Lukas Kahwe Smith added strict mode 3535e86
Lukas Kahwe Smith some more docs 4137c3e
Lukas Kahwe Smith validate parameters 07ae8fe
Lukas Kahwe Smith simplified code f6d019d
Lukas Kahwe Smith added interfaces af142df
Lukas Kahwe Smith validate configs 37661de
Lukas Kahwe Smith undo previous commit .. no need to make things harder 822af06
Lukas Kahwe Smith cosmetics 3e20eac
Lukas Kahwe Smith ws tweak 5c88435
This page is out of date. Refresh to see the latest.
30  Controller/Annotations/QueryParam.php
... ...
@@ -0,0 +1,30 @@
  1
+<?php
  2
+
  3
+/*
  4
+ * This file is part of the FOSRestBundle package.
  5
+ *
  6
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
  7
+ *
  8
+ * For the full copyright and license information, please view the LICENSE
  9
+ * file that was distributed with this source code.
  10
+ */
  11
+
  12
+namespace FOS\RestBundle\Controller\Annotations;
  13
+
  14
+/**
  15
+ * QueryParam annotation class.
  16
+ *
  17
+ * @Annotation
  18
+ * @author Alexander <iam.asm89@gmail.com>
  19
+ */
  20
+class QueryParam
  21
+{
  22
+    /** @var string */
  23
+    public $name;
  24
+    /** @var string */
  25
+    public $requirements;
  26
+    /** @var string */
  27
+    public $default;
  28
+    /** @var string */
  29
+    public $description;
  30
+}
15  DependencyInjection/Configuration.php
@@ -27,6 +27,8 @@
27 27
  */
28 28
 class Configuration implements ConfigurationInterface
29 29
 {
  30
+    private $forceOptionValues = array(false, true, 'force');
  31
+
30 32
     /**
31 33
      * Generates the configuration tree.
32 34
      *
@@ -39,6 +41,12 @@ public function getConfigTreeBuilder()
39 41
 
40 42
         $rootNode
41 43
             ->children()
  44
+                ->scalarNode('query_fetcher_listener')->defaultFalse()
  45
+                    ->validate()
  46
+                        ->ifNotInArray($this->forceOptionValues)
  47
+                        ->thenInvalid('The query_fetcher_listener option does not support %s. Please choose one of '.json_encode($this->forceOptionValues))
  48
+                    ->end()
  49
+                ->end()
42 50
                 ->arrayNode('routing_loader')
43 51
                     ->addDefaultsIfNotSet()
44 52
                     ->children()
@@ -111,7 +119,12 @@ private function addViewSection(ArrayNodeDefinition $rootNode)
111 119
                             ->defaultValue(array('html' => true))
112 120
                             ->prototype('boolean')->end()
113 121
                         ->end()
114  
-                        ->scalarNode('view_response_listener')->defaultValue('force')->end()
  122
+                        ->scalarNode('view_response_listener')->defaultValue('force')
  123
+                            ->validate()
  124
+                                ->ifNotInArray($this->forceOptionValues)
  125
+                                ->thenInvalid('The view_response_listener option does not support %s. Please choose one of '.json_encode($this->forceOptionValues))
  126
+                            ->end()
  127
+                        ->end()
115 128
                         ->scalarNode('failed_validation')->defaultValue(Codes::HTTP_BAD_REQUEST)->end()
116 129
                     ->end()
117 130
                 ->end()
9  DependencyInjection/FOSRestExtension.php
@@ -39,6 +39,7 @@ public function load(array $configs, ContainerBuilder $container)
39 39
         $loader->load('view.xml');
40 40
         $loader->load('routing.xml');
41 41
         $loader->load('util.xml');
  42
+        $loader->load('request.xml');
42 43
 
43 44
         if (version_compare(FOSRestBundle::getSymfonyVersion(Kernel::VERSION), '2.1.0', '<')) {
44 45
             $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)
128 129
         } else {
129 130
             $container->setParameter($this->getAlias().'.mime_types', array());
130 131
         }
  132
+
  133
+        if (!empty($config['query_fetcher_listener'])) {
  134
+            $loader->load('query_fetcher_listener.xml');
  135
+
  136
+            if ('force' === $config['query_fetcher_listener']) {
  137
+                $container->setParameter($this->getAlias().'.query_fetch_listener.set_params_as_attributes', true);
  138
+            }
  139
+        }
131 140
     }
132 141
 
133 142
     /**
71  EventListener/QueryFetcherListener.php
... ...
@@ -0,0 +1,71 @@
  1
+<?php
  2
+
  3
+/*
  4
+ * This file is part of the FOSRestBundle package.
  5
+ *
  6
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
  7
+ *
  8
+ * For the full copyright and license information, please view the LICENSE
  9
+ * file that was distributed with this source code.
  10
+ */
  11
+
  12
+namespace FOS\RestBundle\EventListener;
  13
+
  14
+use Symfony\Component\HttpKernel\Event\FilterControllerEvent,
  15
+    Symfony\Component\HttpKernel\HttpKernelInterface,
  16
+    Symfony\Component\DependencyInjection\ContainerInterface;
  17
+
  18
+/**
  19
+ * This listener handles various setup tasks related to the query fetcher
  20
+ *
  21
+ * Setting the controller callable on the query fetcher
  22
+ * Setting the query fetcher as a request attribute
  23
+ *
  24
+ * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  25
+ */
  26
+class QueryFetcherListener
  27
+{
  28
+    /**
  29
+     * @var ContainerInterface
  30
+     */
  31
+    private $container;
  32
+
  33
+    private $setParamsAsAttributes;
  34
+
  35
+    /**
  36
+     * Constructor.
  37
+     *
  38
+     * @param   ContainerInterface $container container
  39
+     */
  40
+    public function __construct(ContainerInterface $container, $setParamsAsAttributes = false)
  41
+    {
  42
+        $this->container = $container;
  43
+        $this->setParamsAsAttributes = $setParamsAsAttributes;
  44
+    }
  45
+
  46
+    /**
  47
+     * Core controller handler
  48
+     *
  49
+     * @param   FilterControllerEvent   $event    The event
  50
+     */
  51
+    public function onKernelController(FilterControllerEvent $event)
  52
+    {
  53
+        $request = $event->getRequest();
  54
+        $queryFetcher = $this->container->get('fos_rest.request.query_fetcher');
  55
+
  56
+        $queryFetcher->setController($event->getController());
  57
+        $request->attributes->set('queryFetcher', $queryFetcher);
  58
+
  59
+        if ($this->setParamsAsAttributes) {
  60
+            $params = $queryFetcher->all();
  61
+            foreach ($params as $name => $param) {
  62
+                if ($request->attributes->has($name)) {
  63
+                    $msg = sprintf("QueryFetcher parameter conflicts with a path parameter '$name' for route '%s'", $request->attributes->get('_route'));
  64
+                    throw new \InvalidArgumentException($msg);
  65
+                }
  66
+
  67
+                $request->attributes->set($name, $param);
  68
+            }
  69
+        }
  70
+    }
  71
+}
135  Request/QueryFetcher.php
... ...
@@ -0,0 +1,135 @@
  1
+<?php
  2
+
  3
+/*
  4
+ * This file is part of the FOSRestBundle package.
  5
+ *
  6
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
  7
+ *
  8
+ * For the full copyright and license information, please view the LICENSE
  9
+ * file that was distributed with this source code.
  10
+ */
  11
+
  12
+namespace FOS\RestBundle\Request;
  13
+
  14
+use Symfony\Component\HttpFoundation\Request;
  15
+
  16
+/**
  17
+ * Helper to validate query parameters from the active request.
  18
+ *
  19
+ * @author Alexander <iam.asm89@gmail.com>
  20
+ * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  21
+ */
  22
+class QueryFetcher implements QueryFetcherInterface
  23
+{
  24
+    /**
  25
+     * @var QueryParamReader
  26
+     */
  27
+    private $queryParamReader;
  28
+
  29
+    /**
  30
+     * @var Request
  31
+     */
  32
+    private $request;
  33
+
  34
+    /**
  35
+     * @var array
  36
+     */
  37
+    private $params;
  38
+
  39
+    /**
  40
+     * @var callable
  41
+     */
  42
+    private $controller;
  43
+
  44
+    /**
  45
+     * Initializes fetcher.
  46
+     *
  47
+     * @param QueryParamReader      $queryParamReader Query param reader
  48
+     * @param Request               $request          Active request
  49
+     */
  50
+    public function __construct(QueryParamReader $queryParamReader, Request $request)
  51
+    {
  52
+        $this->queryParamReader = $queryParamReader;
  53
+        $this->request = $request;
  54
+    }
  55
+
  56
+    /**
  57
+     * @abstract
  58
+     * @param callable $controller
  59
+     *
  60
+     * @return void
  61
+     */
  62
+    public function setController($controller)
  63
+    {
  64
+        $this->controller = $controller;
  65
+    }
  66
+
  67
+    /**
  68
+     * Get a validated query parameter.
  69
+     *
  70
+     * @param string $name    Name of the query parameter
  71
+     * @param Boolean $strict If a requirement mismatch should cause an exception
  72
+     *
  73
+     * @return mixed Value of the parameter.
  74
+     */
  75
+    public function get($name, $strict = false)
  76
+    {
  77
+        if (null === $this->params) {
  78
+            $this->initParams();
  79
+        }
  80
+
  81
+        if (!array_key_exists($name, $this->params)) {
  82
+            throw new \InvalidArgumentException(sprintf("No @QueryParam configuration for parameter '%s'.", $name));
  83
+        }
  84
+
  85
+        $config = $this->params[$name];
  86
+        $default = $config->default;
  87
+        $param = $this->request->query->get($name, $default);
  88
+
  89
+        // Set default if the requirements do not match
  90
+        if ($param !== $default && !preg_match('#^'.$config->requirements.'$#xs', $param)) {
  91
+            if ($strict) {
  92
+                throw new \RuntimeException("Query parameter value '$param', does not match requirements '{$config->requirements}'");
  93
+            }
  94
+
  95
+            $param = $default;
  96
+        }
  97
+
  98
+        return $param;
  99
+    }
  100
+
  101
+    /**
  102
+     * Get all validated query parameter.
  103
+     *
  104
+     * @param Boolean $strict If a requirement mismatch should cause an exception
  105
+     *
  106
+     * @return array Values of all the parameters.
  107
+     */
  108
+    public function all($strict = false)
  109
+    {
  110
+        $params = array();
  111
+        foreach ($this->params as $name => $config) {
  112
+            $params[$name] = $this->get($name, $strict);
  113
+        }
  114
+
  115
+        return $params;
  116
+    }
  117
+
  118
+    /**
  119
+     * Initialize the parameters
  120
+     *
  121
+     * @throws \InvalidArgumentException
  122
+     */
  123
+    private function initParams()
  124
+    {
  125
+        if (empty($this->controller)) {
  126
+            throw new \InvalidArgumentException('Controller and method needs to be set via setController');
  127
+        }
  128
+
  129
+        if (!is_array($this->controller) || empty($this->controller[0]) || !is_object($this->controller[0])) {
  130
+            throw new \InvalidArgumentException('Controller needs to be set as a class instance (closures/functions are not supported)');
  131
+        }
  132
+
  133
+        $this->params = $this->queryParamReader->read(new \ReflectionClass($this->controller[0]), $this->controller[1]);
  134
+    }
  135
+}
50  Request/QueryFetcherInterface.php
... ...
@@ -0,0 +1,50 @@
  1
+<?php
  2
+
  3
+/*
  4
+ * This file is part of the FOSRestBundle package.
  5
+ *
  6
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
  7
+ *
  8
+ * For the full copyright and license information, please view the LICENSE
  9
+ * file that was distributed with this source code.
  10
+ */
  11
+
  12
+namespace FOS\RestBundle\Request;
  13
+
  14
+use Symfony\Component\HttpFoundation\Request;
  15
+
  16
+/**
  17
+ * Helper interface to validate query parameters from the active request.
  18
+ *
  19
+ * @author Alexander <iam.asm89@gmail.com>
  20
+ * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  21
+ */
  22
+interface QueryFetcherInterface
  23
+{
  24
+    /**
  25
+     * @abstract
  26
+     * @param callable $controller
  27
+     *
  28
+     * @return void
  29
+     */
  30
+    function setController($controller);
  31
+
  32
+    /**
  33
+     * Get a validated query parameter.
  34
+     *
  35
+     * @param string $name    Name of the query parameter
  36
+     * @param Boolean $strict If a requirement mismatch should cause an exception
  37
+     *
  38
+     * @return mixed Value of the parameter.
  39
+     */
  40
+    function get($name, $strict = false);
  41
+
  42
+    /**
  43
+     * Get all validated query parameter.
  44
+     *
  45
+     * @param Boolean $strict If a requirement mismatch should cause an exception
  46
+     *
  47
+     * @return array Values of all the parameters.
  48
+     */
  49
+    function all($strict = false);
  50
+}
74  Request/QueryParamReader.php
... ...
@@ -0,0 +1,74 @@
  1
+<?php
  2
+
  3
+/*
  4
+ * This file is part of the FOSRestBundle package.
  5
+ *
  6
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
  7
+ *
  8
+ * For the full copyright and license information, please view the LICENSE
  9
+ * file that was distributed with this source code.
  10
+ */
  11
+
  12
+namespace FOS\RestBundle\Request;
  13
+
  14
+use Doctrine\Common\Annotations\Reader;
  15
+use FOS\RestBundle\Controller\Annotations\QueryParam;
  16
+
  17
+/**
  18
+ * Class loading @QueryParameter annotations from methods.
  19
+ *
  20
+ * @author Alexander <iam.asm89@gmail.com>
  21
+ * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  22
+ */
  23
+class QueryParamReader implements QueryParamReaderInterface
  24
+{
  25
+    private $annotationReader;
  26
+
  27
+    /**
  28
+     * Initializes controller reader.
  29
+     *
  30
+     * @param Reader $annotationReader annotation reader
  31
+     */
  32
+    public function __construct(Reader $annotationReader)
  33
+    {
  34
+        $this->annotationReader = $annotationReader;
  35
+    }
  36
+
  37
+    /**
  38
+     * Read annotations for a given method.
  39
+     *
  40
+     * @param \ReflectionClass $reflection Reflection class
  41
+     * @param string           $method     Method name
  42
+     *
  43
+     * @return array QueryParam annotation objects of the method. Indexed by parameter name.
  44
+     */
  45
+    public function read(\ReflectionClass $reflection, $method)
  46
+    {
  47
+        if (!$reflection->hasMethod($method)) {
  48
+            throw new \InvalidArgumentException(sprintf("Class '%s' has no method '%s' method.", $reflection->getName(), $method));
  49
+        }
  50
+
  51
+        return $this->getParamsFromMethod($reflection->getMethod($method));
  52
+    }
  53
+
  54
+    /**
  55
+     * Read annotations for a given method.
  56
+     *
  57
+     * @param \ReflectionMethod $method     Reflection method
  58
+     *
  59
+     * @return array QueryParam annotation objects of the method. Indexed by parameter name.
  60
+     */
  61
+    public function getParamsFromMethod(\ReflectionMethod $method)
  62
+    {
  63
+        $annotations = $this->annotationReader->getMethodAnnotations($method);
  64
+
  65
+        $params = array();
  66
+        foreach ($annotations as $annotation) {
  67
+            if ($annotation instanceof QueryParam) {
  68
+                $params[$annotation->name] = $annotation;
  69
+            }
  70
+        }
  71
+
  72
+        return $params;
  73
+    }
  74
+}
40  Request/QueryParamReaderInterface.php
... ...
@@ -0,0 +1,40 @@
  1
+<?php
  2
+
  3
+/*
  4
+ * This file is part of the FOSRestBundle package.
  5
+ *
  6
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
  7
+ *
  8
+ * For the full copyright and license information, please view the LICENSE
  9
+ * file that was distributed with this source code.
  10
+ */
  11
+
  12
+namespace FOS\RestBundle\Request;
  13
+
  14
+/**
  15
+ * interface for loading query parameters for a method
  16
+ *
  17
+ * @author Alexander <iam.asm89@gmail.com>
  18
+ * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  19
+ */
  20
+interface QueryParamReaderInterface
  21
+{
  22
+    /**
  23
+     * Read annotations for a given method.
  24
+     *
  25
+     * @param \ReflectionClass $reflection Reflection class
  26
+     * @param string           $method     Method name
  27
+     *
  28
+     * @return array QueryParam annotation objects of the method. Indexed by parameter name.
  29
+     */
  30
+    function read(\ReflectionClass $reflection, $method);
  31
+
  32
+    /**
  33
+     * Read annotations for a given method.
  34
+     *
  35
+     * @param \ReflectionMethod $method     Reflection method
  36
+     *
  37
+     * @return array QueryParam annotation objects of the method. Indexed by parameter name.
  38
+     */
  39
+    function getParamsFromMethod(\ReflectionMethod $method);
  40
+}
23  Resources/config/query_fetcher_listener.xml
... ...
@@ -0,0 +1,23 @@
  1
+<?xml version="1.0" ?>
  2
+
  3
+<container xmlns="http://symfony.com/schema/dic/services"
  4
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5
+    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
  6
+
  7
+    <parameters>
  8
+
  9
+        <parameter key="fos_rest.query_fetch_listener.class">FOS\RestBundle\EventListener\QueryFetcherListener</parameter>
  10
+        <parameter key="fos_rest.query_fetch_listener.set_params_as_attributes">false</parameter>
  11
+
  12
+    </parameters>
  13
+
  14
+    <services>
  15
+
  16
+        <service id="fos_rest.query_fetch_listener" class="%fos_rest.query_fetch_listener.class%">
  17
+            <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController"/>
  18
+            <argument type="service" id="service_container"/>
  19
+            <argument>%fos_rest.query_fetch_listener.set_params_as_attributes%</argument>
  20
+        </service>
  21
+
  22
+    </services>
  23
+</container>
27  Resources/config/request.xml
... ...
@@ -0,0 +1,27 @@
  1
+<?xml version="1.0" ?>
  2
+
  3
+<container xmlns="http://symfony.com/schema/dic/services"
  4
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5
+    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
  6
+
  7
+    <parameters>
  8
+
  9
+        <parameter key="fos_rest.request.query_fetcher.class">FOS\RestBundle\Request\QueryFetcher</parameter>
  10
+        <parameter key="fos_rest.request.query_fetcher.reader.query_param.class">FOS\RestBundle\Request\QueryParamReader</parameter>
  11
+
  12
+    </parameters>
  13
+
  14
+    <services>
  15
+
  16
+        <service id="fos_rest.request.query_fetcher" class="%fos_rest.request.query_fetcher.class%" scope="request">
  17
+            <argument type="service" id="fos_rest.request.query_fetcher.reader.query_param"/>
  18
+            <argument type="service" id="request"/>
  19
+        </service>
  20
+
  21
+        <service id="fos_rest.request.query_fetcher.reader.query_param" class="%fos_rest.request.query_fetcher.reader.query_param.class%">
  22
+            <argument type="service" id="annotation_reader"/>
  23
+        </service>
  24
+
  25
+    </services>
  26
+
  27
+</container>
1  Resources/config/routing.xml
@@ -47,6 +47,7 @@
47 47
 
48 48
         <service id="fos_rest.routing.loader.reader.action" class="%fos_rest.routing.loader.reader.action.class%">
49 49
             <argument type="service" id="annotation_reader" />
  50
+            <argument type="service" id="fos_rest.request.query_fetcher.reader.query_param" />
50 51
         </service>
51 52
 
52 53
     </services>
61  Resources/doc/3-listener-support.md
Source Rendered
@@ -214,3 +214,64 @@ fos_rest:
214 214
 
215 215
 ## That was it!
216 216
 [Return to the index](index.md) or continue reading about [ExceptionController support](4-exception-controller-support.md).
  217
+
  218
+### Query fetcher listener
  219
+
  220
+The query fetcher listener simply sets the QueryFetcher instance as a request attribute
  221
+configured for the matched controller so that the user does not need to do this manually.
  222
+
  223
+```yaml
  224
+# app/config/config.yml
  225
+fos_rest:
  226
+    query_fetcher_listener: true
  227
+```
  228
+
  229
+```php
  230
+class FooController extends Controller
  231
+{
  232
+    /**
  233
+     * Will look for a page query parameters, ie. ?page=XX
  234
+     * If not passed it will be automatically be set to the default of "1"
  235
+     * If passed but doesn't match the requirement "\d+" it will be also be set to the default of "1"
  236
+     * Note that if the value matches the default then no validation is run.
  237
+     * So make sure the default value really matches your expectations.
  238
+     * @QueryParam(name="page", requirements="\d+", default="1", description="Page of the overview.")
  239
+     *
  240
+     * @param QueryFetcher $queryFetcher
  241
+     */
  242
+    public function getArticlesAction(QueryFetcher $queryFetcher)
  243
+    {
  244
+        $page = $queryFetcher->get('page');
  245
+        $articles = array('bim', 'bam', 'bingo');
  246
+
  247
+        return array('articles' => $articles, 'page' => $page);
  248
+    }
  249
+```
  250
+
  251
+Note: There is also ``$queryFetcher->all()`` to fetch all configured query parameters at once. And also
  252
+both ``$queryFetcher-&g