Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Sub Requests & Request Data

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

Remember that cool trick that we did a few minutes ago with the argument value resolver that allowed us to have an $isMac argument to any controller in our system? Does that also work in a controller that's called by sub request? Of course! Because there's nothing special about this controller: it was called thanks to a complete cycle through HttpKernel::handle(). All the same listeners and all the same argument value resolvers are called.

So... cool! Let's use that! Add an $isMac argument... then pass it into the template.

... lines 1 - 6
class PartialController extends AbstractController
{
public function trendingQuotes($isMac)
{
... lines 11 - 12
return $this->render('partial/trendingQuotes.html.twig', [
... line 14
'isMac' => $isMac
]);
}
... lines 18 - 38
}

Inside trendingQuotes.html.twig, near the bottom, add {% if isMac %}{% endif %} and inside, put an <hr>, a <small> tag, and then say:

<div class="quote-space pb-2 pt-2">
... lines 2 - 10
{% if isMac %}
<hr>
<small>BTW, you're using a Mac!</small>
{% endif %}
</div>

BTW, you're using a Mac!

Easy enough! Find your browser... navigate back to the homepage... and refresh. On the right, there it is! We're using a Mac.

Just for the heck of it, let's add that same logic to the sidebar above this. This lives in the homepage template, so find ArticleController::homepage... add an $isMac argument and pass this into the template.

... lines 1 - 13
class ArticleController extends AbstractController
{
... lines 16 - 32
public function homepage(ArticleRepository $repository, LoggerInterface $logger, $isMac)
{
... lines 35 - 37
return $this->render('article/homepage.html.twig', [
... line 39
'isMac' => $isMac,
]);
}
... lines 43 - 71
}

Steal the isMac logic from the trending quotes template, open homepage.html.twig and... right below the "Buy Now!" button, paste.

... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
... lines 6 - 45
<div class="col-sm-12 col-md-4 text-center">
<div class="ad-space mx-auto mt-1 pb-2 pt-2">
... lines 48 - 51
{% if isMac %}
<hr>
<small>BTW, you're using a Mac!</small>
{% endif %}
</div>
... lines 57 - 58
</div>
</div>
</div>
{% endblock %}

When we try the page now, no surprise: both places show the message.

Adding the ?mac Override

Since I am using a Mac, it's kind of hard to test whether or not this feature correctly hides for people who are not on a Mac. To make testing easier, let's add a way to override the real logic. I want to be able to add a ?mac=true or ?mac=false to the URL to have full control.

The code for setting the argument is in IsMacArgumentValueResolver. So, if we want to "short-circuit" the real logic, it's no problem. Before we read the User-Agent, add if $request->query->has('mac'), then yield $request->query->getBoolean('mac). getBoolean() is a cool function that grabs the mac query parameter but runs it through PHP's filter_var() function with the FILTER_VALIDATE_BOOLEAN flag. That means a value like a false string will turn into a false boolean. Kinda fun. Anyways, after this, return so the function doesn't continue.

... lines 1 - 8
class IsMacArgumentValueResolver implements ArgumentValueResolverInterface
{
... lines 11 - 15
public function resolve(Request $request, ArgumentMetadata $argument)
{
if ($request->query->has('mac')) {
yield $request->query->getBoolean('mac');
return;
}
... lines 23 - 26
}
}

Ok: if I refresh without changing the URL, it still reads my User-Agent and everything looks right. Now add ?mac=false. And... it works! The message is gone. Oh wait! The first message is gone, but the one coming from the sub-request controller is still there! What the heck?

If you're thinking that somehow the argument value resolver isn't called on a sub request, that's not it. A sub request is handled exactly like the main request. This function is being called twice on this page: once for the main request and again for the sub request. So why do those two calls produce a different result?

The Request in the Sub Request is not the Same

Click into the profiler and go to the Performance section. The Request object that's being processed on top is not the same as the Request object that's being processed down here for the sub-request. Symfony creates two, distinct Request objects. The first Request object represents the data for the real HTTP request that's coming into our app. And so, it contains the query parameter info. But that second Request is kind of a "fake" request. It mainly exists so that the _controller attribute can be set on it. It's not really a representation of the "real" request. And so, it may not have all the same data. It doesn't have the query parameters, for example.

Let's see this: dump($request) inside of the resolve() method... then refresh.

... lines 1 - 15
public function resolve(Request $request, ArgumentMetadata $argument)
{
dump($request);
... lines 19 - 27
}
... lines 29 - 30

Hover over the target icon on the web debug toolbar. Yep, two dumps. If we look at the query parameters for the first Request... it's got it! mac=false. But down on the second request, it has some _path query parameter, but no mac.

The point is: there are two different requests. And the fact that they don't all contain the same data is on purpose. Because of this, whenever you're handling a sub-request, it's not a good idea to read information from the request... because you're not really reading data from the correct request!

So how can we correctly read the mac query parameter from a sub-request? To learn how, let's get crazy and make our own sub-request directly in PHP.

Leave a comment!

7
Login or Register to join the conversation

Hi @Rayan ^^!, is there a real case that we can use ArgumentValueResolver, the isMac-case is not really eloquent ..

Reply

Hey @Houssem Zitoun!

Definitely! But the best examples are probably specific to your project. For example, suppose you frequently talk to an API to fetch data. You could set up a system where you can type-hint the object you need from the API in your controller and use an argument value resolver to actually make the API request and pass in that value.

Some projects might not have any use for custom argument value resolvers and that may be in part because the popular ones have already been implemented. For example, if you use Doctrine to fetch data, there’s no reason to implement an argument value resolver to automatically query for entity objects because that already exists.

Anyways, let me know if this helped :).

Cheers!

Reply

Hey @Rayan,

Yes ^^ !, if we had doctrine no reasons to implement a custom resolver, totally agree :)
Otherwise, I liked the use-case you gave me! Cool! (with your accent in the videos) so it reminds me of: assuming that to increase performance you need less code to run, is it better to consume the API "classically" or use a resolver ^ ^?

Cheers!

Reply

Hey Houssem!

> Cool! (with your accent in the videos) so it reminds me of

😂

> assuming that to increase performance you need less code to run, is it better to consume the API "classically" or use a resolver

The performance is identical, except for one important detail. Regardless of how you make the API call, you want to make sure that you *only* make the API call when you actually *need* that data.

So, for example: suppose you are have an argument value resolver that looks for a Product type-hint in an argument. If an argument has this type-thing, it makes an API call and passes this object to your controller.

If you had this situation, and had a "Product $product" argument on a controller... but then never actually *used* that argument, then you're unnecessarily making that API call. But... that's probably not very common: why would you have a "Product $product" argument to your controller if you didn't need it? Well, one legitimate case is if you have some if conditional...and you only need it under certain situations. That's probably not very common, but that would be an example where making the API call manually in your controller (only *exactly* if/when you need it) is better than the resolver.

But mostly, they're identical. If the current page will execute a controller that does NOT have a "Product $product" argument, then the resolver will do nothing and no API call will be made :).

However, to take this to the *next* crazy level of thinking: one thing you need to be aware of is that an argument resolver is instantiated on every request, even if it ultimately doesn't "resolve" any arguments. That's not really a big deal... unless the constructor of your resolver requires *other* objects.... and those require other objects... and suddenly you're instantiating 20 objects just so that your argument resolver can do nothing on 99% of the requests :). As similar thing happens with Twig extensions (which are always instantiated when you use Twig, even if you don't use any of the functions/filters provided by it), event subscribers and voters. This is an entirely different conversation, but since this is a "deep dive" course, I thought you might like to chat about it ;). Here's some more info about that: https://symfonycasts.com/sc...

Cheers!

Reply

OK! it's clear for me ^^!
Big thanks for fhe "deep dive course", I did not notice that it exist. I did instead this deep dive course :)
Last thing, but in other subject, I did 6 courses (6 certifacations) and I noticed that your did not talk about the sessions ^^, is there a reason?

Reply

Hey Houssem!

> and I noticed that your did not talk about the sessions ^^, is there a reason?

We get this question occasionally... and I really should cover it somewhere. The reason I don't talk about it much is that session are super easy. If you want to store something in the session, you can type-hint SessionInterface to get the service. Then it's a simple key-value store: use $session->set() to put something in and $session->get() to get it back out. All the session data is saved behind the scenes automatically. The only other complication is if you wanted/needed to change your session storage - e.g. to store in the database instead of on the filesystem. We don't have a video on that, but in part because the docs cover it pretty well - https://symfony.com/doc/cur...

But if you have any specific session questions, let me know :).

Cheers!

Reply

Hey hey hey @Rayan ^^!

We get this question occasionally... and I really should cover it somewhere.

Yes and please ^^.There are also a lot of simple concepts out there and you've covered it really well so that you have a lot of added value ^^, sometimes better than the documentation!

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