Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

How Sub-Requests Work

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $9.00

How Sub-Requests Work

To learn how sub request really work, let’s leave this behind for a second and go back to DinosaurController::indexAction. I’m going to create a sub request right here, by hand. To do that, just create a Request object. Next, set the _controller key on its attributes. Set it to AppBundle:Dinosaur:latestTweets:

// AppBundle/Controller/DinosaurController.php
// ...

public function indexAction($isMac)
{
    // ...

    $request = new Request();
    $request->attributes->set(
        '_controller',
        'AppBundle:Dinosaur:_latestTweets'
    );

    // ...
}

Then, I’m going to fetch the http_kernel service. Yep, that’s the same HttpKernel we’ve been talking about, and it lives right in the container.

Now, let’s call the familiar handle function on it: the exact same handle function we’ve been studying. Pass it the $request object and a SUB_REQUEST constant as a second argument. I’m going to talk about that constant in a second:

// AppBundle/Controller/DinosaurController.php
// ...

public function indexAction($isMac)
{
    // ...

    $request = new Request();
    $request->attributes->set(
        '_controller',
        'AppBundle:Dinosaur:_latestTweets'
    );
    $httpKernel = $this->container->get('http_kernel');
    $response = $httpKernel->handle(
        $request,
        HttpKernelInterface::SUB_REQUEST
    );

    // ...
}

Let’s think about this. We’re already right in the middle of an HttpKernel::handle() cycle for the main request. Now, we’re starting another HttpKernel::handle() cycle. And because I’m setting the _controller attribute it knows which controller to execute. That sub request is going to go through that whole process and ultimately call _latestTweetsAction.

I’m not going to do anything with this Response object: I’m just trying to prove a point. If we refresh the browser and click into the Timeline, we now have two sub requests. One of them is the one I just created. If you scroll down you can see that. The other one is coming from the render() call inside the template.

Let’s comment this silliness out. I wanted to show you this because that’s exactly what happens inside base.html.twig when we use the render() function. It creates a brand new request object, sets the _controller key to whatever we have here, then passes it to HttpKernel::handle().

Sub-Requests have a Different Request Object

Now we know why we’re getting the weird isMac behavior in the sub request! The UserAgentSubscriber - in fact all listeners - are called on both requests. But the second time, the request object is not the real request. It’s just some empty-ish Request object that has the _controller set on it. It doesn’t, for example, have the same query parameters as the main request.

That’s why the first time UserAgentSubscriber runs, it reads the ?notMac=1 correctly. But the second time, when this is run for the sub request, there are no query parameters and the override fails.

Properly Handling Sub-Request Data

Here’s the point: when you have a sub request, you need to not rely on the information from the main request. That’s because the request you’re given is not the real request. Internally, Symfony duplicates the main request, so some information remains and some doesn’t. That’s why reading the User-Agent header worked in the sub request. But don’t rely on this: think of the sub-request as a totally independent object.

So whenever you read something off of the request, you need to ask yourself... do you feel lucky?... I mean... you need to make sure you’re working with what’s called the “master” request.

This means that our UserAgentSubscriber can only do its job properly for the master request. On a sub-request, it shouldn’t do anything. So let’s add an if statement and use an isMasterRequest() method on the event object. If this is not the master request, let’s do nothing:

// src/AppBundle/EventListener/UserAgentSubscriber.php
// ...

public function onKernelRequest(GetResponseEvent $event)
{
    if (!$event->isMasterRequest()) {
        return;
    }
    // ...

}

And how does Symfony know if it’s handling a master or sub-requests? That’s because the second argument here. So when you call HttpKernel::handle(), we pass in a constant that says “hey this is a sub request, this is not a master request”. And then your listeners can behave differently if they need to.

When we refresh, we get a huge error! This makes sense. UserAgentSubscriber doesn’t run on the sub-request, so isMac is missing from the request attributes. And because of that, we can no longer have an $isMac controller argument.

Passing Information to a Sub-Request Controller

But wait! I do want to know if the user is on a Mac from my sub request. What’s the solution?

The answer is really simple: just pass it to the controller. The second argument to the controller() function is an array of items that you want to make available as arguments to the controller. Behind the scenes, these are put onto the attributes of the sub request. So we can add a userOnMac key and set its value to the true isMac attribute stored on the master request. So, app.request.attributes.get('isMac'):

{# app/Resources/views/base.html.twig #}
{# ... #}

{{ render(controller('AppBundle:Dinosaur:_latestTweets', {
    'userOnMac': app.request.attributes.get('isMac')
})) }}

Inside of the controller, add a userOnMac variable and pass it into the template:

// src/AppBundle/Controller/DinosaurController.php
// ...

public function _latestTweetsAction($userOnMac)
{
    // ...

    return $this->render('dinosaurs/_latestTweets.html.twig', [
        'tweets' => $tweets,
        'isMac' => $userOnMac
    ]);
}

Now when we refresh, we still have the ?notMac=1, so the Mac message is missing from the master request part at the top. And if we scroll down, the sub request also knows we’re not on a mac because we’re passing that information through.

When we take off the query parameter, it looks like we’re on a mac up top on the bottom. Brilliant!

The lesson is that you need to be careful not to read outside request information, like query parameters from the URL, from inside a sub-request. This also ties into Http caching and ESI which are topics we’ll cover later. If we follow this rule and you do want to cache this latest tweets fragment, it’s going to be super easy.

Seeya next time!

Leave a comment!

10
Login or Register to join the conversation
Default user avatar
Default user avatar Andrew Grudin | posted 5 years ago

It's been lovely!
One question, what if there would be an inheritance mechanism for sub-Request , all bags and keys populated from master at the very beginning (btw, it really is done in some part (User-Agent)), then changing by schedule?
All stuff, which are to be added\overridden would be added\overridden(_controller, may be something else), as sub-Request goes with different events through different listeners. Who can prevent? Why should we get rid of information, which already have and which may be useful and utilized by sub-Request without extra manipulation? In context of our case, for example, we wouldn't need to put in this redundant:

{'userOnMac': app.request.attributes.get('isMac').

3 Reply

Hi Andrew!

Wow, this is a *very* insightful question! In truth, *some* information is shared from the master request to the sub-request: https://github.com/symfony/.... I'm honestly not sure why *some* things are copied to the sub-request, but other things are not. However, there is a very good reason why information should *not* be shared from the master request to the sub-request (and why you should ignore any information that *is* shared). The main purpose of sub-requests is to enable ESI HTTP Caching (which is not something we cover in this tutorial). With ESI, instead of having a master request and a sub-request, there are literally *two* completely independent, externally-originated master requests made (one for the whole page, and one for just the "embedded" controller part). By not sharing information between the master and sub requests, you allow yourself to take advantage of ESI caching (which is very powerful) if you want to (without really changing anything in your code).

Does that help? You were very smart to think about this!

1 Reply
Default user avatar
Default user avatar breerly | posted 5 years ago

You've mentioned how to work with sub-requests, but you haven't described why it is useful to distinguish master requests from sub requets - why does the HttpKernelInterface have this distinction at all?

1 Reply

In that case, shame on me! Yes, this is a very good question.

Basically, there are certain things (event listeners) that only make sense on the master request. For example: suppose you have a listener that changes that if the URL starts with /admin, and the user doesn't have ROLE_ADMIN, deny them access (there are easier ways to do this than a listener, but you get the idea). This *only* needs to be done on the master request. It may be "ok" to check this on the sub-request as well, but its redundant: there's no way someone could have made it into your site at all if this check failed on the master request (so why check it again). More abstractly, anything that checks some information on the real request probably shouldn't run on the sub-request.

There's also one other (practical situation). Sometimes people execute listeners in order to "setup" something in their system - e.g. add some analytics because a "mobile" user is accessing the site. In this case, if you run this listener again on sub-requests, you may double-count the analytic. So again, abstractly: when your listener does something that *must* only happens once per "real" request, it's important to know if you're a master/sub-request.

I work with a company that users a lot of listeners, and for a long time they didn't know about this master/sub-request thing (their listeners ran in all cases). Eventually, some edge-cases started causing weird bugs because they didn't expect the listeners to be called multiple times.

I hope that helps!

1 Reply
Default user avatar
Default user avatar Radu Murzea | posted 5 years ago

I keep looking for an answer to this but I can't find any.

Is there a significant difference between master/sub requests strictly from a performance point-of-view ? When making a sub-request, my thinking is that it should be significantly faster because the bundles are already loaded, the container already instatiated most services that are needed and-so-on. I'm curious if you know of some benchmark published somewhere about this.

Reply

Hi Radu!

Yes, you're right - a sub-request is faster than a master request because of the exact reason you just said: the container is instantiated and so are most services. I don't know of any benchmarks about this, but *how* much faster will vary based on how many new (previously uninstantiated) services you request on a sub-request. Also, even though all the listeners are called again, most of them do not actually do anything on sub-requests and return very quickly.

If you look at the Performance / Timeline tab of the profiler on a page that has sub-requests, you'll be able to easily see how much faster each sub-request is than the main request :).

Anyways - good question, and you already are thinking correctly about the answer! But, sub-requests are still fairly "heavy", so use them cautiously, unless you're planning on caching them with ESI (then use them liberally!)

Cheers!

Reply
Default user avatar

I think I still need to do some research but I think with subrequests I can solve the following problem (of course using the classic blog example):

Assume I have an API that listens to '/users/{id}/posts' to return the posts from user with given id. Now if I want to get the posts of the currently authenticated user I would need to send a request to '/users/{id}/posts' where id is the id of the currently authenticated user. I think this is weird. I'm already authenticated and the webserver should be able to figure out my id. (do you agree?)

So, I think I would want an endpoint '/posts' that returns all the posts from the currently authenticated user (agree?). So, in this second controller method I could create a new subrequest pointing to '/users/{id}/posts' where {id} is the id of the currently authenticated user (derived from the JWT token for example). Then I can return the response of that subrequest as the response of the main request. This way I don't need to duplicate the logic used in the controller method listening to '/users/{id}/posts'.

What do you think?

Reply
Default user avatar
Default user avatar Johan | Johan | posted 5 years ago | edited

Ok, I think I found a simpler solution. I found out you can just map multiple URLs to one controller action so I can do something like this:


/**
 * @Route("/users/{id}/posts")
 * @Route("/posts")
 *
 * @Method("GET")
 */
public function listAction($id = null)
{
    if($id === null) {
        $id = $this->getUser()->getId();
    }

    return new Response("List of posts for user with id=$id");
}
Reply

Hey Johan!

Yes, I like your simpler solution a lot :). And this is a fairly common type of thing to do. Technically, it's not RESTful... because it means that if I authenticate as user A and go to /posts I will get a different "resource" than if I authenticate as user B and also go to /posts... but I think that's really not that important :). Facebook's API does this a lot - you can go to "/me" to get info about "my" user.

To summarize: I love your solution for this in Symfony, and I also support what you're doing in the API, even if some people will grip that it's not technically RESTful. If it's useful and sane, go for it.

Cheers!

1 Reply
Default user avatar

Alright, thank you. Ye, I think I would prefer this simple but not so RESTful approach over the more complicated but RESTful approach.

Rules are made to be broken, right? ;)

Thank you for response, it is very much appreciated.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.x-dev", // 2.6.x-dev
        "doctrine/orm": "~2.2,>=2.2.3", // v2.4.6
        "doctrine/doctrine-bundle": "~1.2", // v1.2.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.5.0
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.7
        "symfony/monolog-bundle": "~2.4", // v2.6.1
        "sensio/distribution-bundle": "~3.0", // v3.0.9
        "sensio/framework-extra-bundle": "~3.0", // v3.0.3
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "~0.2" // 0.2
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3" // v2.4.0
    }
}
userVoice