Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Sub Requests

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 $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Before we finish our adventure, I want to talk about a fascinating feature of the request-response process. It's something that we've already seen... but not explored. I want to talk about sub-requests.

Rendering a Controller from a Template

To do that, we need to add a feature! On the homepage, see these trending quotes on the right? I'm going to close a few files... and open this template: templates/article/homepage.html.twig. The trending quotes are hardcoded right here. Let's make this a bit more realistic: let's pretend that these quotes are coming from the database.

That would be simple enough: we could open the homepage controller, query for the quotes and pass them into the template. Except... I'm going to complicate things. Pretend that we want to be able to easily reuse this "trending quotes" sidebar on a bunch of different pages. To do that nicely, we need to somehow encapsulate the markup and the query logic.

There are at least 2 different ways to do this. The first option would be to move the markup to another template and, inside that template, call a custom Twig function that fetches the trending quotes from the database.

The second option - and a particularly interesting one if you want to use HTTP caching - is to use a sub-request. You may have done this before without realizing that you were actually doing something super cool.

Remove this entire section and replace it with {{ render(controller()) }}. Together, these two functions allow you to literally render a controller from inside Twig. The content of that Response will be printed right here.

Let's execute a new controller: App\\Controller - you need 2 slashes because we're inside a string - \\PartialController. For the method, how about, ::trendingQuotes.

... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
... lines 6 - 45
<div class="col-sm-12 col-md-4 text-center">
... lines 47 - 52
{{ render(controller('App\\Controller\\PartialController::trendingQuotes')) }}
</div>
</div>
</div>
{% endblock %}

Creating the Sub-Request Controller

Cool! Let's go make that! Click on Controller/ and create a new PHP class: PartialController. Make it extend the usual AbstractController and create the public function trendingQuotes().

... lines 1 - 4
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class PartialController extends AbstractController
{
public function trendingQuotes()
{
... lines 11 - 15
}
... lines 17 - 37
}

But instead of making a real database query, let's fake it. I'll paste in a new private function called getTrendingQuotes(): it returns an array with the data for the 3 quotes.

... lines 1 - 17
private function getTrendingQuotes()
{
return [
[
'author' => 'Wernher von Braun, Rocket Engineer',
'link' => 'https://en.wikipedia.org/wiki/Wernher_von_Braun',
'quote' => 'Our two greatest problems are gravity and paperwork. We can lick gravity, but sometimes the paperwork is overwhelming.',
],
[
'author' => 'Aaron Cohen, NASA Administrator',
'link' => 'https://en.wikipedia.org/wiki/Aaron_Cohen_(Deputy_NASA_administrator)',
'quote' => 'Let\'s face it, space is a risky business. I always considered every launch a barely controlled explosion.',
],
[
'author' => 'Christa McAuliffe, Challenger Astronaut',
'link' => 'https://en.wikipedia.org/wiki/Christa_McAuliffe',
'quote' => 'If offered a seat on a rocket ship, don\'t ask what seat. Just get on.',
],
];
}
... lines 38 - 39

Above, call this: $quotes = $this->getTrendingQuotes()... and then render the template: return $this->render(), partial/trendingQuotes.html.twig passing in the $quotes variable.

... lines 1 - 8
public function trendingQuotes()
{
$quotes = $this->getTrendingQuotes();
return $this->render('partial/trendingQuotes.html.twig', [
'quotes' => $quotes
]);
}
... lines 17 - 39

Finally add the template: create the new partial/ directory first... then the new trendingQuotes.html.twig inside. Perfect! I'll paste some code here that loops over the quotes and prints them. Remember that you can get any of the code I'm pasting from the code blocks on this page.

<div class="quote-space pb-2 pt-2">
<h3 class="text-center pb-3">Trending Quotes</h3>
<div class="px-5">
{% for quote in quotes %}
<p{{ loop.first ? '' : 'class="pt-4"' }}>
<i class="fa fa-comment"></i> "{{ quote.quote }}" <br><a href="{{ quote.link }}">{{ quote.author }}</a>
</p>
{% endfor %}
</div>
</div>

Ok! Let's see if it works! Move over and refresh. Woo! That was amazing! We just made a sub-request!

Seeing the Sub Request in the Profiler

Oh... you're not as excited as I am? Ok fine. Click any of the icons down on the web debug toolbar to open the profiler and then go to the Performance section. Look closely: it has all the normal stuff right? I see RouterListener and our controller. But, there's a funny shaded background coming from inside the Twig template.

This is indicating that there was a sub-request during this time. And if you scroll down, you can see it! Sub-requests 1 for trendingQuotes().

This will make more sense if you scroll up and set the Threshold input box back down to 0 to show everything.

Look again at the shaded area. This is when the sub-request is being handled, which literally means that another Request object was created and sent into HttpKernel::handle()! Scroll down... and behold!

Symfony didn't just "call" the controller: it went through the entire HttpKernel::handle() process again! It dispatched another kernel.request event, executed all the listeners - including our UserAgentSubscriber - called the controller and dispatched kernel.response. It also dispatched the other normal events too - they're just hard to see.

So... yea! {{ render(controller()) }} sends a second Request object through the HttpKernel process. It's bonkers.

In fact, that second request even gets its own entire profiler! Yep, click the controller link to go to the profiler for that sub-request! Check out the URL: this is a kind of, internal URL that identifies this sub-request. Set the threshold to 0 here to get a big view of that sub-request.

Sub-Requests & _controller

So... how did this work? How does Symfony go through the entire HttpKernel process and render this controller... if there is no route to the controller? How does the routing work for a sub-request?

The truth is: the routing doesn't execute. Click into the "Request / Response" section and scroll down to the request attributes. Check it out: the request attributes have an _controller key set to App\Controller\PartialController::trendingQuotes.

This works a lot like what we saw in ErrorListener, when it rendered ErrorController. Symfony created a Request object and set the _controller on its attributes. Then, when RouterListener was called for this sub-request - because it was called - it saw that the request already had an _controller attribute and returned immediately. The router is never called and the ControllerResolver reads the _controller string that was originally set.

Sub-Requests are Expensive

So this is a sub request. We're going to explore it further and talk about some special properties of it. But before we do, I want to mention one thing. Sub-Requests are awesome if you want to leverage HTTP caching: when you cache the Response of a sub-request. For example, you could cache the trendingQuotes() Response for 1 hour, and then not cache the rest of the page at all. Or you could do the opposite! It's a blazingly fast way to cache.

But if you're not using HTTP caching, be careful not to over-use sub-requests. Remember: they execute an entire HttpKernel::handle() flow. So if you have a lot of them, it will slow down performance.

Next: let's make our sub-request a little bit more interesting. It will uncover something mysterious.

Leave a comment!

5
Login or Register to join the conversation
Kevin B. Avatar
Kevin B. Avatar Kevin B. | posted 3 years ago

I was definitely in the "over use" camp at one point. Even with http caching (not vanish, just Symfony's PHP implementation) I experienced quite a speed boost once I removed all fragments and turned off http caching.

Obviously "your mileage may vary" but that was my experience.

Reply
Cristian T. Avatar
Cristian T. Avatar Cristian T. | Kevin B. | posted 3 years ago

So don't you recommend using subrequest?

Another question I have ... in each sub-request, does it open a new connection to the database?

Thanks a lot

Reply
Kevin B. Avatar

In my app, I used an ESI to display a user toolbar while http caching the "outer" html. Every "page load" had to do a full request/response cycle for just the user toolbar. So I saved on the database queries/rendering of the outer html. Later, I wanted to have other areas of the "outer html" that were specific to the user so I had to create another ESI... and another... In the end, removing all the ESI and rendering the entire page on each page load as a single request/response was faster. It also made my app much less complex.

So I wouldn't say I don't recommend it but I consider it a premature optimization. I suggest waiting until you actually have performance issues with your site before reaching for it.

Regarding database connections, I think if the sub-request happens within the current request (not using ESI/HTTP caching), the sub-request would share the doctrine connection (but I'm not 100% sure on this).

Reply

Thanks for sharing you real-world experience Kevin B.!

I'm also 99% sure that the database connection is shared in the sub-request. A sub-request is ultimately nothing fancy: it's just more work inside the same PHP process. There is a concept of "resetting the container" in Symfony, but that does not happen between sub-requests.

Cheers!

Reply

Hey Kevin B.

Thanks for sharing your experience with us, I hope this will help to many others!

Cheers!

Reply
Cat in space

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

This tutorial also works well for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-iconv": "*",
        "antishov/doctrine-extensions-bundle": "^1.4", // v1.4.3
        "aws/aws-sdk-php": "^3.87", // 3.133.20
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.12.1
        "doctrine/doctrine-bundle": "^2.0", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.2.2
        "doctrine/orm": "^2.5.11", // 2.8.2
        "easycorp/easy-log-handler": "^1.0", // v1.0.9
        "http-interop/http-factory-guzzle": "^1.0", // 1.0.0
        "knplabs/knp-markdown-bundle": "^1.7", // 1.9.0
        "knplabs/knp-paginator-bundle": "^5.0", // v5.4.2
        "knplabs/knp-snappy-bundle": "^1.6", // v1.7.1
        "knplabs/knp-time-bundle": "^1.8", // v1.16.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.24
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "league/html-to-markdown": "^4.8", // 4.9.1
        "liip/imagine-bundle": "^2.1", // 2.5.0
        "oneup/flysystem-bundle": "^3.0", // 3.7.0
        "php-http/guzzle6-adapter": "^2.0", // v2.0.2
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.1", // v5.6.1
        "symfony/asset": "5.0.*", // v5.0.11
        "symfony/console": "5.0.*", // v5.0.11
        "symfony/dotenv": "5.0.*", // v5.0.11
        "symfony/flex": "^1.9", // v1.17.5
        "symfony/form": "5.0.*", // v5.0.11
        "symfony/framework-bundle": "5.0.*", // v5.0.11
        "symfony/mailer": "5.0.*", // v5.0.11
        "symfony/messenger": "5.0.*", // v5.0.11
        "symfony/monolog-bundle": "^3.5", // v3.6.0
        "symfony/property-access": "5.0.*|| 5.1.*", // v5.1.11
        "symfony/property-info": "5.0.*|| 5.1.*", // v5.1.10
        "symfony/routing": "5.1.*", // v5.1.11
        "symfony/security-bundle": "5.0.*", // v5.0.11
        "symfony/sendgrid-mailer": "5.0.*", // v5.0.11
        "symfony/serializer": "5.0.*|| 5.1.*", // v5.1.10
        "symfony/twig-bundle": "5.0.*", // v5.0.11
        "symfony/validator": "5.0.*", // v5.0.11
        "symfony/webpack-encore-bundle": "^1.4", // v1.11.1
        "symfony/yaml": "5.0.*", // v5.0.11
        "twig/cssinliner-extra": "^2.12", // v2.14.3
        "twig/extensions": "^1.5", // v1.5.4
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.0
        "twig/inky-extra": "^2.12", // v2.14.3
        "twig/twig": "^2.12|^3.0" // v2.14.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.4.0
        "fakerphp/faker": "^1.13", // v1.13.0
        "symfony/browser-kit": "5.0.*", // v5.0.11
        "symfony/debug-bundle": "5.0.*", // v5.0.11
        "symfony/maker-bundle": "^1.0", // v1.29.1
        "symfony/phpunit-bridge": "5.0.*", // v5.0.11
        "symfony/stopwatch": "^5.1", // v5.1.11
        "symfony/var-dumper": "5.0.*", // v5.0.11
        "symfony/web-profiler-bundle": "^5.0" // v5.0.11
    }
}
userVoice