Manually Making a Sub Request

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

To understand more about sub requests, let's create one by hand! Because, it's not super obvious what these two Twig functions are really doing behind-the-scenes.

Insides our homepage controller, let's execute a sub request right here. How? It's simpler than you might think. Step 1: create a new request object: $request = new Request(). This is a totally empty Request object: it basically has nothing in it.

... lines 1 - 15
class ArticleController extends AbstractController
{
... lines 18 - 34
public function homepage(ArticleRepository $repository, LoggerInterface $logger, $isMac, HttpKernelInterface $httpKernel)
{
... lines 37 - 39
// manual sub-request example
$request = new Request();
... lines 42 - 54
}
... lines 56 - 84
}

It's not like the Request::createFromGlobals() method that we saw earlier. That method pre-populates the object with all the current request information. This does not do that. To render the partial controller, set the request attribute: $request->attributes->set('_controller') and set that to the same string we have inside our Twig template. I'll copy that... and paste it here: 'App\\Controller\\PartialController::trendingQuotes'.

... lines 1 - 34
public function homepage(ArticleRepository $repository, LoggerInterface $logger, $isMac, HttpKernelInterface $httpKernel)
{
... lines 37 - 41
$request->attributes->set('_controller', 'App\\Controller\\PartialController::trendingQuotes');
... lines 43 - 54
}
... lines 56 - 86

We now have a Request object with nothing in it except for an _controller attribute. And... that's all we need! Well, to work around some internal validation that checks for a valid IP address, we also need to say $request->server->set('REMOTE_ADDR', '127.0.0.1').

... lines 1 - 34
public function homepage(ArticleRepository $repository, LoggerInterface $logger, $isMac, HttpKernelInterface $httpKernel)
{
... lines 37 - 42
$request->server->set('REMOTE_ADDR', '127.0.0.1');
... lines 44 - 54
}
... lines 56 - 86

To send this into HttpKernel, we can fetch that service. Yes, even the mighty HttpKernel is a service in the container. Add another argument: HttpKernelInterface $httpKernel. Then, down here, we can say $response = $httpKernel->handle(). We're going to pass this two arguments. We already know from index.php that the first argument is the Request. So, pass $request. But there is also an optional second argument: the request "type". This allows you to pass a flag that indicates if this is a "master" request - that's the default - or if this is a sub-request - some request that is happening inside the main one. That's our situation, so pass: HttpKernelInterface::SUB_REQUEST.

... lines 1 - 10
use Symfony\Component\HttpKernel\HttpKernelInterface;
... lines 12 - 34
public function homepage(ArticleRepository $repository, LoggerInterface $logger, $isMac, HttpKernelInterface $httpKernel)
{
... lines 37 - 44
$response = $httpKernel->handle(
$request,
HttpKernelInterface::SUB_REQUEST
);
... lines 49 - 54
}
... lines 56 - 86

What difference will that make? Not much. But listeners to almost every event that we've seen are passed this flag on the event object and can behave differently based on whether or not a master or sub request is being handled. We'll see that in a few minutes.

To check if this works, dump($response).

... lines 1 - 34
public function homepage(ArticleRepository $repository, LoggerInterface $logger, $isMac, HttpKernelInterface $httpKernel)
{
... lines 37 - 48
dump($response);
... lines 50 - 54
}
... lines 56 - 86

Um... ok! Let's try this! We added this to the homepage... so refresh. Everything looks normal on this main request. Now hover over the target icon on the web debug toolbar. There it is! A dumped Response with the trending quotes content inside.

And, yes, if we click the time icon on the web debug toolbar to get to the Performance section of the profiler, we can see our sub request! Heck, now we have two sub requests: our "manual" sub-request and then the one from the template.

Set the threshold back down to 0 milliseconds. Way down on the main profiler, the sub-request shows up as this strange __section__.child thing.

Go back to the homepage controller and comment out the sub request logic.

... lines 1 - 34
public function homepage(ArticleRepository $repository, LoggerInterface $logger, $isMac, HttpKernelInterface $httpKernel)
{
... lines 37 - 39
/*
// manual sub-request example
$request = new Request();
$request->attributes->set('_controller', 'App\\Controller\\PartialController::trendingQuotes');
$request->server->set('REMOTE_ADDR', '127.0.0.1');
$response = $httpKernel->handle(
$request,
HttpKernelInterface::SUB_REQUEST
);
dump($response);
*/
... lines 52 - 56
}
... lines 58 - 88

I wanted you to see that this is all that really happens to trigger a sub request.

Listeners and the isMasterRequest() Flag

As we talked about, many listeners will use this SUB_REQUEST flag to change their behavior. Because sometimes, it only makes sense for a listener to do its work on the main, master request. For example - if you wrote a custom listener that checked the URL and denied access based on some custom logic, that listener only needs to do that check on the main request. It either denies access or allows access initially, and then the rest of the page should render normally.

Our UserAgentSubscriber is a perfect example of this. It makes no sense to read the User-Agent off of a sub request. It might work - because, in reality, sub-requests copy some of the data from the main request, but trying to read real information off of the request in a sub-request is asking for trouble. I really want you to think of the master and sub requests as totally independent objects.

So, what can we do? At the very top of our listener, if not $event->isMasterRequest(), simply return.

... lines 1 - 10
class UserAgentSubscriber implements EventSubscriberInterface
{
... lines 13 - 19
public function onKernelRequest(RequestEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
... lines 25 - 37
}
... lines 39 - 45
}

The isMasterRequest() method is a shortcut to check the flag that was originally passed to HttpKernel::handle(). Our listener will still be called on a sub-request, but now it will do nothing. And that makes sense: this class is doing nothing more than logging the User-Agent. We didn't realize it before, but thanks to our sub-request, each page refresh was logging the User-Agent twice: one for the main request and once for the sub-request.

Ok, but! We still haven't fixed our original problem: when we add ?mac=false to the URL, this is correctly read on the master request but incorrectly on the sub request. That's because we're trying to read that query parameter from inside the sub request... which doesn't work.

How can we fix that? The answer leverages an old friend of ours and will also touch on the proper way to pass data from the main request to the sub-request if you want to use HTTP caching with edge side includes. That's next.

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
    }
}