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!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0",
        "ext-iconv": "*",
        "antishov/doctrine-extensions-bundle": "^1.4", // v1.4.2
        "aws/aws-sdk-php": "^3.87", // 3.133.20
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^2.0", // 2.0.7
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.1.2
        "doctrine/orm": "^2.5.11", // v2.7.1
        "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.8.1
        "knplabs/knp-paginator-bundle": "^5.0", // v5.1.1
        "knplabs/knp-snappy-bundle": "^1.6", // v1.7.0
        "knplabs/knp-time-bundle": "^1.8", // v1.11.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.3.0
        "nexylan/slack-bundle": "^2.1", // v2.2.2
        "oneup/flysystem-bundle": "^3.0", // 3.4.0
        "php-http/guzzle6-adapter": "^2.0", // v2.0.1
        "sensio/framework-extra-bundle": "^5.1", // v5.5.3
        "symfony/asset": "5.0.*", // v5.0.4
        "symfony/console": "5.0.*", // v5.0.4
        "symfony/dotenv": "5.0.*", // v5.0.4
        "symfony/flex": "^1.9", // v1.9.10
        "symfony/form": "5.0.*", // v5.0.4
        "symfony/framework-bundle": "5.0.*", // v5.0.4
        "symfony/mailer": "5.0.*", // v5.0.4
        "symfony/messenger": "5.0.*", // v5.0.4
        "symfony/monolog-bundle": "^3.5", // v3.5.0
        "symfony/security-bundle": "5.0.*", // v5.0.4
        "symfony/sendgrid-mailer": "5.0.*", // v5.0.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "5.0.*", // v5.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "5.0.*", // v5.0.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.7.3
        "symfony/yaml": "5.0.*", // v5.0.4
        "twig/cssinliner-extra": "^2.12", // v2.12.5
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.3.0
        "fzaninotto/faker": "^1.7", // v1.9.1
        "symfony/browser-kit": "5.0.*", // v5.0.4
        "symfony/debug-bundle": "5.0.*", // v5.0.4
        "symfony/maker-bundle": "^1.0", // v1.14.3
        "symfony/phpunit-bridge": "5.0.*", // v5.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "5.0.*" // v5.0.4
    }
}