First steps with the container

Adding definitions to the container

Let’s register our voter and manager into the container, so we can access them from the ContainerInterface::get() method.

public/index.php

The $containerBuilder->get('access_manager') will return the AccessManager instance. However, this way of adding services into the container has a drawback: each time the container is built, the $postVoter and $manager objects are instantiated, even if we don't request them. It’s a waste of time and memory. To fix this, we will teach the container how to instantiate our services, through Definitions. Definitions are a way to set how the container configures and instantiates services.

public/index.php

If you run your app right now, you’ll get the following error:

Fatal error: Uncaught ArgumentCountError: Too few arguments to function App\Authorization\AccessManager::__construct(), 0 passed and exactly 1 expected [...]

That’s OK, don’t panic 😱!

The AccessManager needs a collection of Voters in order to be instantiated. By default, the container creates services without any argument passed to the constructor. Let’s improve the AccessManager definition to tell the container how to construct the manager.

We call the Definition::addArgument method in order to add arguments to the constructor of the AccessManager. Here the passed argument is an array of References, which is an object that represents a service reference. A reference takes the service's ID as argument. In our case: post_voter . Because the container knows the id post_voter as a Definition of the PostVoter class, it’s able to construct the PostVoter to inject it into the AccessManager.

public/index.php

ℹ️ Note that a more compact way to add definitions in the container exists using the register function. The Definition is a fluent class, it means you can chain the methods in order to ease configuration.

public/index.php

With this configuration, 2 more definitions will be added to the container:

post_voter => for the App\Authorization\Voter\PostVoter class

=> for the class access_manager => for the App\Authorization\AccessManager class

Services scope

Our configuration is going in the right direction. However, we can access the PostVoter instance from the get() method of the container.

dump($containerBuilder->get('post_voter') instanceof PostVoter); // true

Accessing a service like a voter directly from the container is not a good practice. Voters are meant to live inside the AccessManager, therefore it’s useless to get them from the container. The ContainerBuilder proposes us a solution to fix that: private and public services. We can set a definition to be public or not. A private definition is not be accessible from the ContainerBuilder::get() method, while a public definition can be.

public/index.php

Wait, aren’t we supposed to not be able to access the post_voter from the get method? Even with the setPublic(false) call, the definition is still accessible from the container. That's because of a primordial step of Symfony's container: compilation.

After registering the definitions, we’ll call the ContainerBuilder::compile() method:

public/index.php

Now if we try to retrieve our post_voter from the container, we'll get this error as intended:

Fatal error: Uncaught Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: The "post_voter" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.

Even if we can’t access it from the container, it will still be injected in the AccessManager as an inline service.

ℹ️ Inline services are “single usage” services. They are usually injected as an argument of another service. Private services may be inlined under some complex conditions (e.g. if it is an anonymous service or if it is a private service that is only used once).

That’s going pretty well, but why do we have to compile the container in order to make it work? What happened during the compilation? Let’s find out.

The compilation of the container

https://symfony.com/doc/current/components/dependency_injection/compilation.html

The service container can be compiled for various reasons. These reasons include checking for any potential issues such as circular references and making the container more efficient by resolving parameters and removing unused services. Also, certain features — like using parent services — require the container to be compiled. […] The compile method uses Compiler Passes for the compilation. The DependencyInjection component comes with several passes which are automatically registered for compilation.

Symfony’s container is shipped with a bunch of classes called compiler passes. Those classes allow to modify the container during its compilation. For example, the RemovePrivateAliasesPass compiler pass will delete private aliases, while the CheckCircularReferencesPass will throw a ServiceCircularReferenceException if a circular reference is detected between services. You can also create your own compiler passes, we will talk about that later.

What you must remember is that a CompilerPass detected that the post_voter definition was private, therefore it removed the definition — after having injected it into the AccessManager — from the container. We can prove that by dumping the container before and after the compilation:

// before compilation

ContainerBuilder {

-definitions: array:3 [

"service_container" => Definition {}

"post_voter" => Definition {}

"access_manager" => Definition {}

]



// after compilation

ContainerBuilder {

-definitions: array:2 [

"service_container" => Definition {}

"access_manager" => Definition {}

]