Chapters
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
How does the Controller Access the Container?
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOur controller is a beautiful, boring service. I love boring things. This means that, if we need to access some other service from here, we need to "inject" it - either through the constructor or by autowiring it as an argument to the controller method - a special superpower of controllers that we'll talk about soon.
The point is: we can't just "grab" a service out of thin air that we haven't injected.... which is why I'm wondering: how the heck does this render()
shortcut method work? Certainly that uses the twig
service... but we have not injected that into our class
Let's go digging! Hold command or control and click render()
to jump into our parent class: AbstractController
. This method basically just calls renderView()
, which is right above us.
Hmm: renderView()
apparently fetches the twig
service directly from the container. But, hold on a second. How did our controller service get access to the container? Because, it's not like we're injecting it via autowiring or any other way. So... who is populating the $this->container
property?
Oh, but it's even more mysterious than this. Search for parameter_bag
in AbstractController
to find a method called getParameter()
. This method fetches a service directly from the container called parameter_bag
.
Let's get some info on this service. Find your terminal and run:
php bin/console debug:container parameter_bag
Woh. It's public false! This is not a service that you should be able to fetch out of the container directly by saying $this->container->get('parameter_bag')
. It should give us an error! So what the heck is going on?
Service Subscriber Magic
Here's the answer: our controller extends AbstractController
. And AbstractController
implements a special interface called ServiceSubscriberInterface
. This is actually something we talk about in one of our Doctrine tutorials.
When you implement ServiceSubscriberInterface
, it forces you to have a method called getSubscribedServices()
where you return an array that says which services you need inside of this class. Then, Symfony will pass you a "mini" container that holds all of these services.
At the top of AbstractController
, see this setContainer()
method with a ContainerInterface
type-hint? That will not be the real container. Nope, that will be the "mini-container" that holds all the services from our getSubscribedServices()
method. And because our controller is an autowired service... and this method has @required
above it, Symfony knows to call setContainer()
immediately after instantiating this object.
This is what gives our controller the ability to fetch all those services that we didn't inject directly. It also fetches them lazily: none of the services are instantiated unless we need to use them.
So... our controller is not just a boring, normal service: it has this extra superpower. But... this is actually something that... any service in our system can implement - it's not special to controllers. So once again, our controller is beautifully boring and normal.
Next: we now have a callable controller! Let's keep going through HttpKernel
to see what happens next. Because... one big thing we're still missing is what arguments we should pass to the controller.
7 Comments
Hey Pierre!
Ah, I can see how that is confusing! Before I explain WHO is setting $this->container, I start talking about the "parameter_bag" service to try to show you how "strange" this container property is. But I'm not YET explaining who sets it... and "parameter_bag" and $this->container are two different topics. The MOST important thing is really $this->container, which is set thanks to the service subscriber magic. The "parameter_bag" is simply ONE service that is in this mini-container.
Let me know if that make sense - I can totally see how that was confusing... I went on a tangent!
Cheers!
I'm fascinated by @required
annotation for AbstractController::setContainer
method. It basically removes the need to declare the constructor in AbstractController class. When extending AbstractController in my child controller class, I have an opportunity to have my own constructor without the parent::__construct(ContrainerInterface $container);
call. Convenient!
I wonder, and this is my question, what are other possible situations when @required annotation may come in handy? It's a cool tool but I don't really feel how to properly use it. E.g. is it possible to use @required
with service factory when creating individual services with preset configuration values?
Hey Volodymyr T.!
I'm fascinated by @required annotation for AbstractController::setContainer method.
It's neat, isn't it!
I wonder, and this is my question, what are other possible situations when @required annotation may come in handy? It's a cool tool but I don't really feel how to properly use it.
It does need to be used with caution. The reason is that, if you allow a dependency to be set via a setter method, then from an object-oriented level, it's not actually required. What I mean is, if someone tried to use your class outside of Symfony, then they might forget to call the setter and your code might explode (because it expects that property to be set). That is probably not a very realistic situation... but it's kind of a sign of "code smell": if a dependency is needed by your class, then you're sorta supposed to "force" it to be passed via a required constructor argument. So for me, the most common place I use this is to inject the logger because that's really an optional dependency - https://symfonycasts.com/screencast/symfony4-fundamentals/logger-trait.
is it possible to use @required with service factory when creating individual services with preset configuration values?
Ummm, I'm not sure! If you added @required
above a method of a service factory, then Symfony would call that method before instantiating the factory (and so before it calls the method to create the real service). But I don't think I understand what you mean by "with preset configuration values". What trick are you thinking of? :)
Cheers!
Why there is a '?' in method getSubscribedServices ?
Hey Thomas A.
It means that such dependency is optional. In other words, if for any reason you don't have, let's say, the Serializer component installed, then your app won't explode but you should not ask for such service anyways
Cheers!
OK, thanks!
"Houston: no signs of life"
Start the conversation!
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.21.6
"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
}
}
I don't understand the passage at 1:10, when you say: who's populating the this->container property?,
and then you talk about parameter_bag.
Is there a link between how the container is set and the parameter_bag? Or are they two different topics?