Using FOSRest for part of an application with content negotiation #335

Closed
DRvanR opened this Issue Dec 5, 2012 · 34 comments

Projects

None yet

10 participants

@DRvanR

Hello,

we want to use FOSRestBundle for the API part of our application. The API is split into a different bundle.

The "standard" controllers make use of the @Template annotation. For the API controllers, we want to use the @View annotation.

The tricky part comes with the Content Negotiation. For the application itself, only HTML should be returned, for the API only json should be returned. I cannot find a way to set up the content negotation of FOSRestBundle in such a way that it either:

  • applies only to the API controllers, or
  • supports only json for the API, only html for the application

What I've tried so far (all with prefer_extension set to false):

  • disable content-negotiation, set json as fallback. Result:
    • everything is returned as json
  • enable content-negotiation, set json first, html second, fallback to null. Result:
    • application returned as html (Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8)
    • application returned as json (Accept: application/json) where we want a 406
    • api returned as json (Accept: application/json)
    • api returns 500 (template not found) when not having application/json in the accept header, where we want a 406
  • enable content-negotiation, set html first, json second, fallback to null. Results same as above.

Without bloating this, we've also tried it with extensions, with fallback, with returning Views, not using the view and returning a Symfony JsonResponse etc.

There does not seem to be a way to have the FormatListener only apply to particular controllers, at least not while using the @Template and @View annotations. It would seem to work when not using @Template annotation but just returning the response object (as said by W. Durand here). Is there any other way to set up configuration/code so that we can return html only for the application and json only for the api, while using both annotations?

Cheers!

@schmittjoh
@lsmith77
FriendsOfSymfony member

the FormatListener is unfortunately very naive in its current form and i am hoping that with 2.2 we will have routing level support for content negotiation.

that being said .. i dont quite understand your separation of application and api. it seems odd to want to use media types, yet insist on a separate URL scheme for the same resources by media type.

so unless you can convince me that such a separation makes sense, i dont really see a point in trying to change things.

but if you really want this, then you could extend the standard FormatListener or separate application and api via separate kernels.

@DRvanR

The main reason for having the separation is that the application is being used in two different sections.
One is just HTML, and mostly powers crud screens, as well as the login, search, favourites, etc. pages. The second part, what we're going to use the API for, is read-only on a subset of the entities managed by the crudscreens and read/write on entities managed by the api. This second part is a heavy js editing interface, used to graphically design (for some users) and navigate/consult (for all users) complex diagrams. Both sections use a lot of shared business logic for the read-only items, abstracted through a service layer. The two sections cannot exist separately so it makes sense to keep them within the same application but the sections are large enough that it would be useful to automate the api parts using FOSRest.

The read-only entities are basically a resource for the js side, but powering the html side of the application. We're not looking for different representations of the same resource (i.e. html for app, json for api), but we want to expose some data through a json api and other data through crud-screens.

@lsmith77
FriendsOfSymfony member

in that case i would recommend using two separate kernels. you can easily set this up within the same git repo.

@willdurand
FriendsOfSymfony member

@lsmith77 isn't it a common use case? Having one bundle for the API part in a classic application?

I know it would be better to have distinct applications with shared bundles for the common/business logic, but having an ApiBundle may be useful.

@willdurand
FriendsOfSymfony member

Btw you could set the _format to html in your routing definition, for all routes returning HTML only.

@lsmith77
FriendsOfSymfony member

and why shouldn't using two kernels be just as usual?

but see my suggestion here:
#334 (comment)

we could support setting different defaults while importing the same resource twice

@willdurand
FriendsOfSymfony member

Why not, but then it should be documented somewhere.

@lsmith77
FriendsOfSymfony member

not really sure if its the job of FOSRestBundle to teach this .. maybe we should have a cook book entry.

here are some docs (and even a script) about how to setup multiple kernels in one project:
https://github.com/liip/sf2debpkg#application-structure

@lsmith77
FriendsOfSymfony member

but i want to stress once more .. having a separate "api" and html resource urls is not REST. imho the only legitimate use is if you have radically different security setups for the API .. but even then i am not sure if its really necessary. a legitimate use case here would be for example a project where the API was only supposed to be accessed by other machines within the organizations intranet.

btw having separate kernels also has the added benefit if being able to trim down the loaded bundles and configured services.

@willdurand
FriendsOfSymfony member
@DRvanR

Hello,

having a separate "api" and html resource urls is not REST. imho the only legitimate use is if you have radically different security setups for the API .. but even then i am not sure if its really necessary.

Both are true. We do have radically different security setups for parts of the information, but for the largest part the API uses some of the information from the CRUD section but the API-resource is dramatically different. It really is a different resource, not simply a different representation.

a legitimate use case here would be for example a project where the API was only supposed to be accessed by other machines within the organizations intranet.

This is literally the case here. The API is 100% proprietary, will not be exposed to the outside whatsoever. It will only be consumed by the js part of the application. The application itself (both the crud parts and the js part) will not be publicly available. FOSRest is still very useful for having these sections respond correctly and unlocks useful content negotiation, routing, serializer integration etc.

btw having separate kernels also has the added benefit if being able to trim down the loaded bundles and configured services.

A separate kernel is an interesting idea however the only difference between the two kernels would be the Content Negotiation.

As far as I understand, he just has a web application, and some other routes to expose data.

Exactly. As said before, we're not using the same resource with 2 different representations, we're just exposing parts of the data in our resources.

In my mind, it would be great if the FOSRestBundle had the ability to disable it's listeners if the route detected was not one generated by FOSRest (i.e. starting with the prefix). This could be achieved by using a decisionmanager of some sorts that all listeners use in order to advise them on whether they should act or not?

As a sidenote, a cookbook entry about having two kernels would be great!

@lsmith77
FriendsOfSymfony member

@dvrenterghem-ibuildings what you propose sounds quite complex to me, also the intention of this bundle is to provide independent tools for REST APIs, so i am hesitant to forcing the use of the generated routes for negotiation.

wouldnt my proposal in #334 solve your issue? if so could you work on implementing this?

@jonathaningram

A separate kernel is an interesting idea however the only difference between the two kernels would be the Content Negotiation.

As a sidenote, a cookbook entry about having two kernels would be great!

@dvrenterghem-ibuildings if it helps, we are using two separate kernels (one for WWW and one for api [under /api/*]), and it works quite good (so far). We also keep them in separate git projects, which has the advantage that the API can be developed separately from WWW. There's other pros and cons, obviously, which I won't go into.

@mvrhov

We are also using separate kernels for api and www, but both applications are under the same git.

@marcospassos

I've a another case here. We are working on a generic ecommerce system, with generic bundles. So, we have a generic controller (ResourceController) that have the basic REST methods, all context dependent (resources, managers, templates, etc). This controller is inherited by all others controllers. The systems that use theses bundles may support or not REST controller, but it must be ready for this. So, we have a router file like this:

backend_product_list:
    pattern: /products
    defaults:
        _controller: assortment.controller.product:indexAction
        _template: AcmeBundle:Backend/Product:list.html.twig
        _sortable: true
        _sorting:
            updatedAt: desc

api_backend_product_list
    pattern: /api/products
    defaults:
        _controller: assortment.controller.product:indexAction
        _format: json
        _sortable: true
        _sorting:
            updatedAt: desc

The controller gets these parameteres and does the job. I can render a nice template or just a json response. I can also declare two routes, one api/product and other /product using the same controller, but for two different proposes.

The problem is, once you have been registered the FOSRestBundle you cannot decide if a controller will do a Content Negotiation or not, all controllers will do. If I decide that backend_product_list must be not a REST controller I've no ways to do that. Is not enough do not return a View instance, still is possible sends a request header to change the response type. So would be great if was a way to enable (or disable) the "REST behavior" in some areas. Why you cannot have a application that provides REST operations in some areas but not in entire project?

Thanks,
Marcos

@lsmith77
FriendsOfSymfony member

@marcospassos I do not disagree with the use case. Like I said I want to support it .. but imho it requires proper support for this on the routing level .. anything else is just another hack that we will eventually need to break. So I encourage anyone interested in this to help on symfony/symfony#5711 so that we can get core support for content negotiation and then we can revisit this topic to give control over the content negotiation on a controller level.

now if this seems too elaborate and you can make the case that adding such configuration to the current annotations we already support will be sufficiently forwards compatible with the work done in that PR, then I would be willing to consider it ..

@marcospassos

Hi Lucas,

Thanks for your reply. What you think of just add a configuration to set where the rest is required? By default it would be ^/ keeping backward compatibility. I think it is a flexible solution that has nothing to do with content negotiation.

fos_rest:
    coverage:
        - ^/api
        - ^/backoffice/api
@lsmith77
FriendsOfSymfony member

so you are proposing a setting on the route pattern to decide if the format listener should be active or not? imho this is exactly a setting we would not need if we could configure the content negotiation as part of the actual route definition.

@marcospassos

You are right, but with this PR merged, the formats option will not be valid anymore, right?

@lsmith77
FriendsOfSymfony member

i think we still might keep this option to make it possible to set global defaults.

@marcospassos

imho it should be set by paths, otherwise will be necessary to set _format: html in all routes that do not support rest.

fos_rest:
    coverage:
        - {path: ^/, formats: {}, templating_formats: {html: true}}
        - {path: ^/api, formats: {json: true, xml: true}, templating_formats: {}}

What you think?

@lsmith77
FriendsOfSymfony member

once we can define it on routes, we will support the mappings to set this via the fos rest routing. at this point i dont see a reason to add this feature as one will then either set this on a per controller/action or globally.

it just seems like a lot of code and runtime overhead to do this with the approach you mention. but if you can get some support from other fos rest bundle users for this and a PR then i might be able to be convinced ..

@lsmith77
FriendsOfSymfony member

btw i just tweeted a link to this discussion https://twitter.com/lsmith/status/289714694569218050

@mvrhov

IMO: we should push to get the negotiation into the core for symfony 2.3

@marcospassos

@lsmith77 Figure out that you have a part of your application using REST. Without this approach you have two choices:

1 - Do not map globally and map each rest route with all formats accepted:

route_api_1:
    pattern api/1
    _format: json|xml
route_api_2:
    pattern: api/2
    _format: json|xml
route_api_n:
    pattern: api/n
    _format: json|xml

2 - Map globally and override each non rest router

route_site_1:
    pattern: site/1
    _format: html
route_site_2:
    pattern: site/2
    _format: html
route_site_n:
    pattern: site/n
    _format: html

So, as you can see, will require a lot of work to use the FOSRest together with non rest controllers. With the approach that I suggested you wont worry about in define in each route, reducing a lot the work and improving the maintainability.

fos_rest:
    coverage:
        - {path: ^/, formats: {}, templating_formats: {html: true}}
        - {path: ^/api, formats: {json: true, xml: true}, templating_formats: {}}
route_site_1:
    pattern: site/1
route_site_2:
    pattern: site/2
route_site_n:
    pattern: site/n

route_api_1:
    pattern api/1
route_api_2:
    pattern: api/2
route_api_n:
    pattern: api/n
@stof
FriendsOfSymfony member

@marcospassos third option, define your non-REST routes in a different file than your REST route, and put the default format on the import (works as of Symfony 2.1)

@marcospassos

@stof Sorry, I didn't know about this new feature. I agree, it sounds the best solution.

@marcospassos

@stof I haven't found anywhere that talks about it, could you point me one?

@bmeynell

👍 for easy separation of an application-level FooApiBundle with a segregated route prefix (e.g., /api/*) that encapsulates all FOSRest functionality.

And what is all this talk about separate kernels? Where's the documentation on this what-sounds-like super useful strategy?

@lsmith77
FriendsOfSymfony member

there is some documentation inside the docs for the tool we build to generate Debian packages:
https://github.com/liip/sf2debpkg#application-structure

@luishdez

Currently a project that I'm working on has

api.domain.com/ (Only Rest API) Loads a different kernel …
app.domain.com/ (Web UI) that uses REST for backbone also

Practically all my controllers for web app fit a REST pattern. I don't want to duplicate all of them to define a FooApiBundle… (I got multiple bundles based on functionality also, and doesn't make sense put all apis in one) define all security annotations etc when I can just have one to maintain.

Finally I've decided to drop FOSRest and add a Listener to the kernel check the content negotiation and use JMSSerializer with groups … with a few lines I have a REST api and web App using the same controllers.

@mvrhov +1 About the negotiation to the core.

@lsmith77
FriendsOfSymfony member

ages ago I also started on a listener based solution but never finished it because it felt like the wrong place to do this:
#136

However as 2.3 is now released without support for content negotiation in the routing component, it might be worth it to finish this approach to at least have something that works for some use cases. however i personally do not have time to push this.

@lsmith77
FriendsOfSymfony member

see #552

@lsmith77 lsmith77 closed this in #552 Sep 15, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment