Diving into Symfony’s DependencyInjection — Part 1: First steps with the container


In this article, we’re going to dive into Symfony’s dependency injection component.
We will use it from scratch, out of the Symfony framework, with the minimum of external dependencies.
You can code along with me while reading this article, each steps will be detailed so you can easily follow.

This article is divided into three posts:
First steps with the container
In this part you will learn how to setup a container from scratch and add definitions to it. We will create a simple security feature so we can interact with the container.
Symbiosis with the Config component
The DependencyInjection works well with the Config component. You’ll learn how to separate configuration in dedicated files to ease configuration and injection with autoconfiguration and autowiring.
Advanced uses
In this final part we’ll work with monolog to apply some advanced uses of the DependencyInjection component like service decoration. We’ll also learn how to cache the compiled container to improve our application performances.
Photo by Boba Jovanovic on Unsplash

Starting from scratch

We’ll start by creating an empty project and running the composer init command. Just leave the default values to generate our composer.json file.

We need two dependencies to start our project:

  • composer require symfony/dependency-injection
  • composer require --dev symfony/var-dumper to ease debugging.

We also add a PSR-4 autoloading rule to handle our namespaces: the App\ namespace will refer to the src/ directory.
Here is our composer.json file.

composer.json

The ContainerBuilder

The ContainerBuilderis an object that helps you build Symfony’s container by configuring it, adding some definitions etc.

Ok let’s start coding!
Create a public/index.php file as the entry point of our app.

public/index.php
💡To simply run your code, you can start a local web server, by running the 
php -S localhost:8000 -t public/command. Your index.php file will be available at localhost:8000.
Dump of the created container

First thing you may notice: our newly created container is not empty. It already has a definition, and two aliases. Furthermore, this definition refers to the container itself.

  • $containerBuilder->get('service_container');
  • $containerBuilder->get(\Psr\Container\ContainerInterface::class);
  • $containerBuilder->get(\Symfony\Component\DependencyInjection\ContainerInterface::class);
ℹ️ You may also notice that the interface \Symfony\Component\DependencyInjection\ContainerInterface extends the \Psr\Container\ContainerInterface.
It means that it respects the PSR-11 about containers and service locators.
In other words, if your application already has a PSR-11 container, it will be easy to replace it with the symfony/dependency-injection container.

That’s it, you now have a fully functional container.


The business Code

We now need an application to interact with the container. I will implement a rudimentary security system that works like Symfony’s Voters.

ℹ️ For those who don’t know about voters, they are used to check permission on a resource. It is based on the Chain Of Responsibilities design pattern. There are multiple voters that live in an AccessDecisionManager class. The manager will iterate over each voter. When the current voter supports the resource to check, it will be executed to decide if the resource can be accessed by the user.

Model

We need a simple model to make it work. A User and a Post classes are enough. Our voter will handle access to a Post resource.

User

src/Entity/User.php

Post

src/Entity/Post.php

VoterInterface

We need to define a common interface for our voters. VoterInterface will have two functions:

  • supports : tells if the voter should be executed
  • vote : if the supports function returns true, this function is executed. It returns true if the user can access the resource.

Each function takes 3 parameters:

  • string $attribute : a string mostly used to know if the current voter can support the resource
  • $subject : a resource to check
  • App\Entity\User $user : the logged in user
src/Authorization/Voter/VoterInterface.php

PostVoter

Now, let’s create a concrete PostVoter that implements our VoterInterface

  • If the user has the User::ROLE_USER role, he can only read a Post resource.
  • If the user has the User::ROLE_ADMIN role, he can read but also write Post resource.
src/Authorization/Voter/PostVoter.php

AccessManager

The last thing we need is the manager that will handle each voter: the AccessManager. This class will iterate over each voter to know which one to call, thanks to the VoterInterface::supports() function. For now, voters will be passed as a constructor argument. The manager will only have one method:

  • decide : takes the 3 same parameters as the VoterInterface::vote() function
src/Authorization/AccessManager.php

Running our business code

Back in the public/index.php file:

public/index.php

If you run it, you’ll see that our business code is working as expected. The $user can only read the $post resource, and the $admin can do both read and write on $post.

The problem is that we need to manually instantiate the PostVoter and the AccessManager. Good news, the ContainerBuilder can create those objects for us. Let’s play with the container by adding some definitions for our business classes.


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
  • 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 {}
]
You can see the list of all shipped compiler passes here: https://github.com/symfony/dependency-injection/tree/master/Compiler

What’s next?

This first part was mostly about preparing the business code in order to play with the DI component. However, we’ve learned a basic way to interact with the container and how to define services through definitions
We’ve also seen that the container could be compiled, a process that allows us to interact with the container and its definitions.

In the next part, we’ll require help from the Config component of Symfony, which works great with the DependencyInjection component. Our container will step up into a next level, and its configuration will be greatly simplified. We’ll also improve the business code to get the most out of the container.

📖 You can read the next part here: Part 2: Symbiosis with the Config component.