Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

How does the Controller Access the Container?

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

Our 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.

Leave a comment!

7
Login or Register to join the conversation
Pierre Avatar

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?

Reply

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!

Reply
Volodymyr T. Avatar
Volodymyr T. Avatar Volodymyr T. | posted 2 years ago

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?

Reply

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/sc....

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

Reply
Thomas A. Avatar
Thomas A. Avatar Thomas A. | posted 2 years ago

Why there is a '?' in method getSubscribedServices ?

Reply

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!

Reply
Thomas A. Avatar

OK, thanks!

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": "^7.4.0 || ^8.0.0",
        "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
    }
}