Fosrestbundle: how to use uri versioning with a controller implementing the ClassResourceInterface?

Created on 1 Jul 2016  Â·  26Comments  Â·  Source: FriendsOfSymfony/FOSRestBundle

It doesn't seem possible to use uri versioning (/v1/users) with a controller implementing the ClassResourceInterface, is that correct? Or am I missing something?
route:

users:
    type:     rest
    resource: AppBundle\Controller\UserController
    #prefix: {version} // doesn't work

Most helpful comment

The problem is linked with priority of kernel.request event listeners

  • VersionListener (onKernelRequest priority 33) will run before Symfony RouterListener (kernel.request with priority 32). Then $request->attributes->get("version") in VersionListener (priority 33) will never take into account the value of {version} url placeholder, as this will be defined later in Symfony RouterListener (priority 32).
  • VersionListener has to be executed before RouterListener, as the latter will match requested version (parsed with on priority 33) with routes using conditions that have been defined with @Version annotation (matching occurring on priority 32).
  • Even more subtle, as implemented now, @Version annotation cannot work with uri placeholders. In fact, @Version annotations define route conditions with expressions, using request.attributes.get('version') in [...]. These conditions are evaluated by Symfony RouterListener (priority 32), but at the time of evaluation, request.attributes.get('version') is still empty for url placeholders. The RequestMatcherInterface will throw a ResourceNotFoundException before the url placeholder value is stored in request attributes. This explain issue #1491

To resolve all these problems at the same time, I think we need to:

  • keep VersionListener with priority of 33 to resolve all versions but uri {version} placeholder
  • let Symfony RouterListener resolve uri {version} placeholder with priority of 32 and also match route requirements and conditions
  • apply the version exclusion strategy once all versions possibilities are resolved. So delegate this exclusion to a kernel request listener with priority < 32)
  • when a {version} placeholder is used in a path (prefix, path, route annotation, etc.), we should match routes by requirements and not conditions to fix issue #1491

I will propose a PR that fix issues #1491 and #1529

All 26 comments

This should work as expected.

At first, you must uncomment the line prefix. And do you load this controller somewhere else ?

Nope, not loading the controller somewhere else. Good that you're checking, but I used it uncommented :)
My config:

fos_rest:
    routing_loader:
        default_format: json
    format_listener:
        enabled: true
    versioning:
        enabled: true
    view:
        view_response_listener: force
        formats:
            json: true
            jsonp: false
            xml: false
            rss: false

to add: it does work well when using the version as a parameter

On 01 Jul 2016, at 13:16, Guilhem N [email protected] wrote:

This should works as expected.
At first, you must uncomment the line prefix.
Do you load this controller somewhere else ?

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub https://github.com/FriendsOfSymfony/FOSRestBundle/issues/1529#issuecomment-229922098, or mute the thread https://github.com/notifications/unsubscribe/AC-MigxR-7lA68BOZ2SwS9EUIRUMsL1Tks5qRPb2gaJpZM4JC-xX.

In fact, you don't need the versioning feature of this bundle if you only use a path parameter.
What do you mean by not working ? You can't access your controller ?

it gives a "No route found” error
Perhaps you are right that one doesn’t strictly need it. One could just hard code the prefix. It’s not that much work, I’ll just go for that route then.
FYI, this approach of having {version} as a parameter in the path is in the documentation. However, not in combination with the ClassResourceInterface.

On 01 Jul 2016, at 14:04, Guilhem N [email protected] wrote:

In fact, you don't need the versioning feature of this bundle if you use a path parameter.
What do you mean by not working ? You can't access your controller ?

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub https://github.com/FriendsOfSymfony/FOSRestBundle/issues/1529#issuecomment-229929837, or mute the thread https://github.com/notifications/unsubscribe/AC-MiuOvatI-4vVa0df_AtKKDW4ZtKH_ks5qRQJQgaJpZM4JC-xX.

Same issue here

# app/config/routing.yml
api:
    resource: "@ApiBundle/Resources/config/routes.yml"
    type:     rest
    prefix:   /api/{version}

and then

# src/ApiBundle/Resources/config/routes.yml
products:
    type:     rest
    resource: ApiBundle\Controller\ProductController
    name_prefix:  api_

shops:
    type:     rest
    resource: ApiBundle\Controller\ShopController
    name_prefix:  api_

When trying to access /api/v1/shops, a "no route found" exception is thrown. Note that this stopped working after I upgraded from 2.0.0-BETA to 2.0.0. Not to mention it's in the docs, like @roelleor said.

Bumping this issue, as it's happening to me aswell, same case as @tetele

Can you try finding the corresponding route with bin/console debug:router ?

@Ener-Getick

vagrant@mymachine:/vagrant$ app/console debug:router get_categories
+--------------+--------------------------------------------------------------------+
| Property     | Value                                                              |
+--------------+--------------------------------------------------------------------+
| Route Name   | get_categories                                                     |
| Path         | /frontend/api/{tube}/{version}/categories                          |
| Path Regex   | #^/frontend/api/(?P<tube>[^/]++)/(?P<version>[^/]++)/categories$#s |
| Host         | ANY                                                                |
| Host Regex   |                                                                    |
| Scheme       | ANY                                                                |
| Method       | GET                                                                |
| Requirements | NO CUSTOM                                                          |
| Class        | Symfony\Component\Routing\Route                                    |
| Defaults     | _controller: ApiBundle:Frontend\V1\Category:cget                   |
|              | _format: json                                                      |
| Options      | compiler_class: Symfony\Component\Routing\RouteCompiler            |
+--------------+--------------------------------------------------------------------+

Should be noted aswell that in the profiler the symfony matcher complains about attributes.get('version') not being equal to v1 (my version).

@Lumbendil What is the exact path that you are trying? What output do you get when using the router:match command with that path?

@xabbuh answer from memory.

The path I tried was /frontend/api/something/v1/categories, there was no match, and on symfony debug, when inspecting, it said something along "get_categories" route is a close match but version doesn't match the requirements (the route version was being overriden by the listener).

It should be noted that adding ?version=v1 did make the route match.

I am sorry to bump this issue, but was this ever resolved or a hotfix/bypass found? I'm currently running into this problem myself, well the prefix part at least.

Observation:
The reason this seems to go wrong because the Request given to match-function of the RedirectableUrlMatcher-class does not contain any attributes.

Reproduce:
I tested this by accessing a route like /api/{version}/emails where {version} is v1 and dumped the incoming request in appDevDebugProjectContainerUrlMatcher. The request contained no attributes, request or query values, thus making the matching of a route with a prefix impossible.

Strange behaviour:
When the Request is passed to one of my controllers, $request->attributes->get('version') does return v1, which seems to suggest that the URL hasn't been fully parsed when it's given to the class extending RedirectableUrlMatcher.

If someone of you could provide a small project that makes it possible to reproduce your issue, we could look into where the issue actually lies and try to solve it. That would be great. :)

@xabbuh I'll get a small example up and running after lunch. ;)

@xabbuh Here you go: https://github.com/hipio/FosRestBundleFoc


  • Adding echo "Version: " . $request->attributes->get('version', 'null'); on line 109 in appDevDebugProjectContainerUrlMatcher (once generated) shows that the Request object really is empty.
  • If you remove the # from routing.yml on line 5 you'll be able to see that the Request object that the controller receives does contain the version parameter.

0 === strpos($pathinfo, '/api') && preg_match('#^/api/(?P[^/]++)/test(?:\.(?P<_format>json|xml|html))?$#s', $pathinfo, $matches)

Seems to work fine so it really is in_array($request->attributes->get("version"), array(0 => "v1")) that fails in appDevDebugProjectContainerUrlMatcher. Which brings me back to my previous comment stating that somehow the URL isn't fully parsed (if that makes sense) when it's passed to appDevDebugProjectContainerUrlMatcher.


I hope this helps a bit. When you find the issue, could you let me know?

The problem is linked with priority of kernel.request event listeners

  • VersionListener (onKernelRequest priority 33) will run before Symfony RouterListener (kernel.request with priority 32). Then $request->attributes->get("version") in VersionListener (priority 33) will never take into account the value of {version} url placeholder, as this will be defined later in Symfony RouterListener (priority 32).
  • VersionListener has to be executed before RouterListener, as the latter will match requested version (parsed with on priority 33) with routes using conditions that have been defined with @Version annotation (matching occurring on priority 32).
  • Even more subtle, as implemented now, @Version annotation cannot work with uri placeholders. In fact, @Version annotations define route conditions with expressions, using request.attributes.get('version') in [...]. These conditions are evaluated by Symfony RouterListener (priority 32), but at the time of evaluation, request.attributes.get('version') is still empty for url placeholders. The RequestMatcherInterface will throw a ResourceNotFoundException before the url placeholder value is stored in request attributes. This explain issue #1491

To resolve all these problems at the same time, I think we need to:

  • keep VersionListener with priority of 33 to resolve all versions but uri {version} placeholder
  • let Symfony RouterListener resolve uri {version} placeholder with priority of 32 and also match route requirements and conditions
  • apply the version exclusion strategy once all versions possibilities are resolved. So delegate this exclusion to a kernel request listener with priority < 32)
  • when a {version} placeholder is used in a path (prefix, path, route annotation, etc.), we should match routes by requirements and not conditions to fix issue #1491

I will propose a PR that fix issues #1491 and #1529

Are there any workarounds for URI versioning? It still seems not to work, only query and header versioning works

Yes, still not working URI versioning. Why it happens and how I can help to fix this (maybe, we need new resolver for this)?

You can simply add your own onKernelRequest listener to work around the problem described by @juillerat.

namespace AppBundle\EventListener;

use FOS\RestBundle\View\ConfigurableViewHandlerInterface;
use FOS\RestBundle\View\ViewHandlerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;

class VersionListener
{
    private $viewHandler;

    public function __construct(ViewHandlerInterface $viewHandler)
    {
        $this->viewHandler = $viewHandler;
    }

    private function resolveVersion(Request $request)
    {
        $version = $request->attributes->get('version');

        return is_scalar($version) ? $version : strval($version);
    }

    public function onKernelRequest(GetResponseEvent $event)
    {

        $request = $event->getRequest();
        $version = str_replace('v', '', $this->resolveVersion($request));

        $request->attributes->set('version', $version);

        if ($this->viewHandler instanceof ConfigurableViewHandlerInterface) {
            $this->viewHandler->setExclusionStrategyVersion($version);
        }
    }
}
app.version_listener:
        class: Acme\AppBundle\EventListener\VersionListener
        tags:
          - { name: kernel.event_listener, event: kernel.request, priority: 30 }

+1

Was this problem ever resolved? I'm running into the same issue as @roelleor. I'm going to try the workaround but would like to configure it as documented.

+1

+1

+1

Was this page helpful?
0 / 5 - 0 ratings