Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Forwarding to a REST controller attempts to find a template and fails #310

Closed
CodeOtter opened this Issue · 30 comments

6 participants

Patrick Ryan Lukas Kahwe Smith Markus Bachmann MaksSlesarenko Pablo Martelletti Christophe Coevoet
Patrick Ryan

If I attempt to forward() to an action that is in a controller defined in the routing.yml as type: rest (Via FOSRestBundle), symfony throws a 500 error, saying "Unable to find a template" Is this an expected outcome?

Lukas Kahwe Smith
Owner

you need to ensure that the subrequest will use the format you want as by default it will end up using html which by default requires a template.

Patrick Ryan

I was under the assumption that using the routing.yml type: rest should handle that without additional @View annotation, mostly because there is no mention of @View or templates here: https://github.com/FriendsOfSymfony/FOSRestBundle/blob/master/Resources/doc/5-automatic-route-generation_single-restful-controller.md

IF I BE R TEH DUMB, then how can I undumb? :X

Lukas Kahwe Smith
Owner

well its not mentioned because you can choose one without the other, but you can use the two together.

Patrick Ryan

So could I do

# app/config/config.yml
fos_rest:
    view:
        templating_formats:
            html: false
        default_engine: none

?

Lukas Kahwe Smith
Owner

the result of that would be that for the html format it would use the JMSSerializerBundle.
what are you trying to output? if its html, you just have to make sure that either you are setting a template manually, or you are using the @View annotation .. and obviously that a template exists.

Patrick Ryan

What I'm trying to do is output JSON only. I won't want to have to make a template for every single action. This has caused my problem with forward(), which apparently is looking for a template.

Lukas Kahwe Smith
Owner

so if you want to output json, you have to ensure that the subrequest uses the json format. what format is your main request? what does the code for your forward call look like?

Patrick Ryan

To be very specific, I'm submitting HTTP queries, and I'm getting HTTP queries back with JSON in the body.

The forward request looks like this

$this->forward('CompanyToolsBundle:Account:postWat');

It throws a 500, saying "Unable to find template" My config.yml looks like this:

fos_rest:
    view:
        view_response_listener: force
    routing_loader:
        default_format: json
Patrick Ryan

I change ViewResponseListener.php to do the following:

    public function onKernelController(FilterControllerEvent $event)
    {
        // Add this
        var_dump(__METHOD__  . ': ' . $event->getRequest()->getRequestUri() . ' - ' . $event->getRequest()->getMethod());
        $request = $event->getRequest();

        if ($configuration = $request->attributes->get('_view')) {
            $request->attributes->set('_template', $configuration);
        }

        parent::onKernelController($event);
    }

    /**
     * Renders the parameters and template and initializes a new response object with the
     * rendered content.
     *
     * @param GetResponseForControllerResultEvent $event A GetResponseForControllerResultEvent instance
     */
    public function onKernelView(GetResponseForControllerResultEvent $event)
    {
        // Add this
        var_dump(__METHOD__ . ': ' .$event->getRequest()->getRequestUri() . ' - ' . $event->getRequest()->getMethod());

And on a normal OPTION call to an optionStuffAction, I get the following:

string(115) "FOS\RestBundle\EventListener\ViewResponseListener::onKernelController: /blah/web/app_dev.php/api/stuff - OPTIONS"
string(109) "FOS\RestBundle\EventListener\ViewResponseListener::onKernelView: /blah/web/app_dev.php/api/stuff - OPTIONS"

But if optionStuffAction has this:

$this->forward('CompanyBlahBundle:StuffApi:getTest');

I get this

string(115) "FOS\RestBundle\EventListener\ViewResponseListener::onKernelController: /eddm/web/app_dev.php/api/campaign - OPTIONS"
string(115) "FOS\RestBundle\EventListener\ViewResponseListener::onKernelController: /eddm/web/app_dev.php/api/campaign - OPTIONS"
string(109) "FOS\RestBundle\EventListener\ViewResponseListener::onKernelView: /eddm/web/app_dev.php/api/campaign - OPTIONS"
string(111) "FOS\RestBundle\EventListener\ViewResponseListener::onKernelController: /eddm/web/app_dev.php/api/test - GET"

When what I'm expecting is this:

string(115) "FOS\RestBundle\EventListener\ViewResponseListener::onKernelController: /eddm/web/app_dev.php/api/campaign - OPTIONS"
string(111) "FOS\RestBundle\EventListener\ViewResponseListener::onKernelController: /eddm/web/app_dev.php/api/test - GET"
string(109) "FOS\RestBundle\EventListener\ViewResponseListener::onKernelView: /eddm/web/app_dev.php/api/test - GET"
string(109) "FOS\RestBundle\EventListener\ViewResponseListener::onKernelView: /eddm/web/app_dev.php/api/campaign - OPTIONS"
Markus Bachmann

AFAIK forward is always a GET request. You can see it here

Lukas Kahwe Smith
Owner

If you could setup a fork of the Symfony2 SE illustrating the issue, it would help in getting this issue fixed. Most users I guess are not using this Bundle in subrequests.

Lukas Kahwe Smith
Owner

i setup a test case .. for a subrequest the original request is "duplicated" meaning that the HTTP methods remains the same regardless of which method you call. if you POST .. you can do a subreqest to a POST method and it loads the template just fine.

now if you want to change the method type you need to create the request manually and pass it into the method. however then you will also by-pass quite a bit if listener infrastructure and therefore you would manually need to set the request attributes that SensioFrameworkExtraBundle bundle uses sets ..

so the lesson learned .. RAD tools like SensioFrameworkExtraBundle work until you leave the "standard path" .. if you stick to making everything explicit .. then things would still work ..

Lukas Kahwe Smith lsmith77 closed this
Patrick Ryan

For posterity, those who encounter this problem again, it's best to invoke the controller-as-service solution to get what you need.

http://stackoverflow.com/questions/9542293/symfony2-passing-data-between-bundles-controllers

MaksSlesarenko

If it was fixed what was the solution? Have same problem now. I had to specify format to make it work.

$this->forward('...', array(), array('_format' => 'json'));
Lukas Kahwe Smith
Owner

@MaksSlesarenko this is what you have to do .. every subrequest is a new request, so anything you want to carry over to the new request you have to explicitly pass to the subrequest.

Pablo Martelletti

Hi! I'm having the same issue here, but I cannot figure out how to solve it.

Imagine I have a fosrest call, that has an $id parameter, let's say, for route /orders/{id}/remove.

Then, in some other controller, I want to do a forward to that method and, if the status code is, let's say, 201, redirect to new page. But, when trying to do something like:

public function removeOrderAction($id) {
    $response = $this->forward('JustmoovFrontBundle:Order:remove', array(), array('_format' => 'json'));
    if ( $response->getStatusCode() ) $this->redirect(...)
}

I thought I was writting the forward parameters wrong, but I've tried both ways I found in google:

$response = $this->forward('...', array(), array('_format' => 'json'));

In this case i get the error that the $id parameter for the forward action is missing, altough it should be on the request. Or even if I try doing it explicityly, it won't work:

$response = $this->forward('JustmoovFrontBundle:Order:remove', array(), array('id' => $id, '_format' => 'json'));

And in the other hand, I've also tryied doing:

$response = $this->forward('JustmoovFrontBundle:Order:remove', array('_format' => 'json'));

But in this case it complains about that the html template was not found (because it assumes, from the request, that the format is html).

Any ideas?

Lukas Kahwe Smith
Owner

you cannot use the forward() method in this case. you need to set the id as a request attribute on the new request object, which is impossible with this method as you can see:

    // Symfony\Bundle\FrameworkBundle\Controller\Controller
    public function forward($controller, array $path = array(), array $query = array())
    {
        $path['_controller'] = $controller;
        $subRequest = $this->container->get('request')->duplicate($query, null, $path);

        return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
    }
Lukas Kahwe Smith
Owner

i hope you can figure it out with this information. if you do .. please open a PR to improve the documentation

Pablo Martelletti

Well, if forward does not work in my case, then how could I call an rest action from inside a controller and manage the response from there and, if succesfully, make some redirect?

The only way I've figured it right now is via ajax, if the status code was OK, then I do the redirect on the client side. But I don't know if That's ok ... :cry:

Anyway, is there a way to do redirections in rest api? Or really does not make sense? I should process the response on the server or client and then do the redirect?

Thanks!

Christophe Coevoet
Owner

@pmartelletti your use case seems weird to me. If your controller wants to call a rest action to do the work, you are probably putting the logic in the wrong location. You should move the shared logic to service called by both controllers

Pablo Martelletti

@stof , actually it is! The subrequest is just a call to the a service that, for example, marks a booking as canceled. So, extenernal users can do it with an api call.

So, then I wanted to use that same api call from inside my application, and that's why I forward the action to the api controller insted of reusing the service, to avoid code duplication.. Or should I use the service in both controllers, and in one only return a status code, and in the other one, handle that operation and if success, redirect to somewhere? That's what you're saying? :smile:

Thanks!

Christophe Coevoet
Owner

@pmartelletti yes, that's what I'm suggesting. Both controllers should call the service to handle the business logic

Pablo Martelletti

:+1:

Thanks!

Lukas Kahwe Smith
Owner

aside from this .. please look at the code I pasted for what forward() does .. it clones the request and then uses the kernel to do a sub-request. what i told you was that you need to manually configure the request which this method does not support. but of course you could duplicate the logic from the forward method to be able to add what you need. its just 3 lines of codes so no big deal.

for Example:

    public function myForward($controller, $foo, array $path = array(), array $query = array())
    {
        $path['_controller'] = $controller;
        $subRequest = $this->container->get('request')->duplicate($query, null, $path);
        $subRequest->attributes->set('id', $foo);

        return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
    }
Pablo Martelletti

@lsmith77 but that won't work only for the case I need just the $id as parameter? What happens if my call has extra parameters? For example /partner/{slug}/deals/{id}/orders has two parameters, and I should create a method for that specific option too... :worried:

So, if having two methods that do the same thing is not that bad (one for the api call, one for the application use), I'll then follow that path. But if duplication is bad, and I have to do for most of all my api calls and internal uses.. then where would you go to?

Lukas Kahwe Smith
Owner

well I am sure you can come up with a creative way to make $subRequest->attributes->set('id', $foo); more flexible. again the key point is that you need to set any pattern placeholders as request attributes when you want to do a sub-request.

Pablo Martelletti

Yes, I've just realize that my argument was silly. I could do a foreach .. on the second paramenter and set all the parameters I want.

So my question really is a matter of opinion or what is the 'best practice' in this case? I mean: I'm sure I'm not the only one who has the same action from inside an application and from an api call (Let's say, ebay, to delete a product in the web or in its android app, I'm sure it uses the same method...). So what are they doing? Creating the API first and then calling / forwarding the action from inside the application controllers, or they just use the same service they used in the api call from inside the application?

Lukas Kahwe Smith
Owner

Generally a shared service is the better approach.
I would not use a sub-request.

There might be cases where it could make sense to call the external API from your server though when you are using a reverse proxy and there is a high probability that revers proxy can serve the request from its cache.

Christophe Coevoet
Owner

@lsmith77 But then, it should still not be done using a subrequest passed to the kernel (it would not trigger your reverse proxy as it stays behind it). If you want to benefit from the cache of your API, you have to do a real HTTP request to it (which means you consider your API as an external webservice and build your app in a distributed way)

Lukas Kahwe Smith
Owner

that is why i said could make sense to call the external API from your server

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.