What Is DI Container?
Dependency injection container (DIC) is a class that can instantiate and configure objects.
It may surprise you, but in many cases you don't need a dependency injection container to take advantage of dependency injection (DI for short). After all, even in introductory chapter we showed specific examples of DI and no container was needed.
However, if you need to manage a large number of different objects with many dependencies, a dependency injection container will be really useful. Which is perhaps the case for web applications built on a framework.
In the previous chapter, we introduced the classes Article
and UserController
. Both of them have some
dependencies, namely database and factory ArticleFactory
. And for these classes, we will now create a container. Of
course, for such a simple example, it doesn't make sense to have a container. But we'll create one to show how it looks
and works.
Here is a simple hardcoded container for the above example:
class Container
{
public function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection('mysql:', 'root', '***');
}
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->createDatabase());
}
public function createUserController(): UserController
{
return new UserController($this->createArticleFactory());
}
}
The usage would look like this:
$container = new Container;
$controller = $container->createUserController();
We just ask the container for the object and no longer need to know anything about how to create it or what its dependencies are; the container knows all that. The dependencies are injected automatically by the container. That's its power.
So far, the container has everything hardcoded. So we take the next step and add parameters to make the container really useful:
class Container
{
public function __construct(
private array $parameters,
) {
}
public function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection(
$this->parameters['db.dsn'],
$this->parameters['db.user'],
$this->parameters['db.password'],
);
}
// ...
}
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
Astute readers may have noticed a problem. Every time I get an object UserController
, a new instance
ArticleFactory
and database is also created. We definitely don't want that.
So we add a method getService()
that will return the same instances over and over again:
class Container
{
private array $services = [];
public function __construct(
private array $parameters,
) {
}
public function getService(string $name): object
{
if (!isset($this->services[$name])) {
// getService('Database') calls createDatabase()
$method = 'create' . $name;
$this->services[$name] = $this->$method();
}
return $this->services[$name];
}
// ...
}
The first call to e.g. $container->getService('Database')
will have createDatabase()
create a
database object, which it will store in the array $services
and return it directly on the next call.
We also modify the rest of the container to use getService()
:
class Container
{
// ...
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->getService('Database'));
}
public function createUserController(): UserController
{
return new UserController($this->getService('ArticleFactory'));
}
}
By the way, the term service refers to any object managed by the container. Hence the method name
getService()
.
Done. We have a fully functional DI container! And we can use it:
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
$controller = $container->getService('UserController');
$database = $container->getService('Database');
As you can see, it's not difficult to write a DIC. It's notable that the objects themselves don't know that a container is creating them. Thus, it is possible to create any object in PHP this way without affecting their source code.
Manually creating and maintaining a container class can become a nightmare rather quickly. Therefore, in the next chapter we will talk about Nette DI Container, which can generate and update itself almost automatically.