How does a Dependency Injection Container work in PHP
7 December, 2020One of PHP's most important features is its ability to autoload classes, and with that comes the Dependency Injection Containers. Let's see how a DI Container works.
What is Dependency Injection
Before explaining how a DI Container works internally, we need to understand Dependency Injection itself, so we all are on the same page.
In short, Dependency Injection consists of passing dependencies as arguments that a class needs. To be clear, dependencies are also classes.
It may seem obvious or literal, but people have been falling into the Anti Pattern of injecting or passing the container itself for a long time. Then, the class will use it to retrieve the dependencies it wants.
What is wrong with that? -You may ask. To begin with, it obfuscates the class's real needs. When reading a class's constructor signature, you will see that the class needs the container instead of the real dependencies. This isn't good. A class should be explicit about what it really needs to work correctly. Let's see why.
What problems does Dependency Injection solve
Ability to statically analyze for missing dependencies
When a class asks for its dependencies explicitly, a static analyzer can tell if all of them exist without actually running the code. If one or many of the dependencies don't exist yet, we can know immediately. In case the class asks for the container, we won't notice the wrong dependencies until the class actually tries to get them using the container; thus, in runtime.
Use of abstractions or subtypes with Liskov Substitution
If you are familiar with the SOLID principles, and you should be, the Liskov Substitution principle states that any class should be replaceable by any of its children or subtypes. To clarify, let's see an example:
class Car
{
private $engine;
public function __construct(Engine $engine)
{
$this->engine = $engine;
}
}
class Engine
{
public function accelerate(){
// increase speed
};
}
Here we have a Car that needs an Engine, and it declares this need explicitly. Now, what Liskov says is that Engine may be replaced by any subtype of it.
class CombustionEngine extends Engine
{
public function accelerate(){
// consume fuel to increase speed
}
}
class ElectricalEngine extends Engine
{
public function accelerate(){
// use batteries to accelerate
};
}
It doesn't matter which Engine you give to the Car, it won't know and doesn't really care, because any Engine has de ability to accelerate, which is what matters to the Car.
We could go deeper and have the Car asking for abstract classes or interfaces, so our code is more flexible or easier to maintain, but that is a story for another day.
Add Testing thanks to mocks and doubles
Specifically, unit testing. When we are testing a single piece of code, a class at a unit level, we only care about what it does and not about what its dependencies do. It also implies we want to be 100% sure that the failure's cause is in the class being tested and not in any of the dependencies.
How do we avoid a failure in a dependency that's making our test fail? We need to replace the real dependency with a dummy object like a mock or a double. These are classes with the same type as the real one, therefore, accepted by our tested class, but without implementing any behavior. This way, we ensure they won't break the test.
How do we do it? Because the tested class is asking for specific classes or interfaces as dependencies, we can create a subtype of them just for the test. We can also inject spies to ensure the dependencies are being "used" correctly without worrying about their actual behavior. If you want to learn more about that, take a look at prophecy. If we were to inject the container directly, it would be harder to replace the actual class being used as a dependency. There are workarounds about this, but it is harder to achieve and doesn't always work the way you expect.
It's important to declare a class's needs explicitly so we can apply the SOLID principles, properly unit test it, and keep up with the code quality.
What is a Container in Dependency Injection
To simplify, it's a God object that gives concrete instances of classes when asking for them. In other words, it contains dependencies. It is nothing more than a class anyone can create. Its only responsibility is creating other classes whenever asked for. In my opinion, the only reason it's called container is that in older implementations, it actually contained the dependencies. For me, the first time I implemented one, I called it AutoFactory because it behaves like a magic Factory, able to build instances from any class.
Let's see some particularities about DI Containers:
- The classes can be asked by FQDN (Fully Qualified Domain Name), by key, by tag... It depends on the container's implementation.
- A container can behave like a Factory or give classes as Singleton.
- As a Factory, it creates a new instance each time a class is required.
- As a Singleton, it always gives the same instance of a class when it is required. This is also known as services, and it's useful for common classes like database connectors, mail senders, or anything that requires an external connection.
If you are more curious about what a container can do or should do, PHP has a PSR about containers: PSR-11.
How a DI Container works
Now we know a Container is a global object. It can behave as a factory or provide other global objects, known as Singleton objects or services. Its main responsibility is to resolve and build dependencies and do it recursively. What else?
It uses PHP's dark magic: Reflection!. Don't worry about this, it's fine.
You may be thinking, "Great, another complex new thing about all this stuff", but don't worry. If you follow the previous link, you can see Reflection comes with a lot of methods, but we only need like four or five of them to make the container work.
Reflection in PHP is the way we have to introspect classes and objects. It allows us to bypass class visibilities and to modify them in runtime. It's also useful to read a class's methods or its constructor arguments.
Nice. We've learned a lot of new stuff (I hope you did!). Now, finally, to the container!
In case you didn't guess it yet, we will use reflection to read our class's constructor, and from that, which dependencies it has. We will do it recursively, meaning we are going to apply the same logic to our class's dependencies and our dependencies' dependencies.
Code! Give me code! Ok then. Let's start with a Car as an example. An empty car that does nothing, but it can be created.
class Car
{
public function __construct()
{
}
}
Now, let's create an instance of our beloved Container and try to use it to handle the shiny new cars' creation.
class Container
{
public function get(string $class): object
{
$reflectionClass = new ReflectionClass($class);
return $reflectionClass->newInstance();
}
}
And test the container can create a Car when asked.
/** @test */
class ContainerTest extends TestCase
{
/** @test */
public function it_can_be_created()
{
$container = new Container;
$this->assertInstanceOf(Container::class, $container);
}
/** @test */
public function it_can_create_an_instance_of_another_class()
{
$container = new Container;
$object = $container->get(Car::class);
$this->assertInstanceOf(Car::class, $object);
}
}
So far, so good. But we haven't really done anything new yet. We could just use return new $class;
and it would work the same way as long as you are passing the class FQDN as a string (instead of an object). And we aren't using dependencies yet, so let's add a little bit of sauce then.
First, let's create an Engine for our Car
class Engine
{
public function accelerate()
{
// run run
}
}
class Car
{
private $engine;
public function __construct(Engine $engine)
{
$this->engine = $engine;
}
}
Now that the Car does need an Engine, you can't use new $class
anymore. We need to figure out its dependencies first and then build them before making the Car's instance. To do that, we will use the reflection instance we've created to get the params from the constructor. Since we are modern people who use TypeHinting, we can know the type of these params, so we can build them.
$params = $this->getConstructorParams($class);
class Container
{
public function get(string $class): object
{
$reflectionClass = new ReflectionClass($class);
$params = $this->constructorParams($class);
}
/**
* @return ReflectionParameter[]
*/
private function constructorParams(string $class): array
{
$reflectionMethod = new ReflectionMethod($class, '__construct');
$params = $reflectionMethod->getParameters();
return $params;
}
}
Here we are using the ReflectionMethod to read the class's constructor and get the parameters. But how do we get these parameters? What are them? Ideally, we would get an array of FQDN, meaning an array with each dependencies' class name, but we are getting an array of ReflectionParameter objects.
That is how you work with Reflection. You have ReflectionClass. If you want to get a method from there, you get a ReflectionMethod. And if you want a parameter from the method, then it would be a ReflectionParameter. That's fine, we can get a parameter's type from the ReflectinParameter using $parameter->getClass()->name
. We could even add an array_map
to the constructorParams()
method to get directly an array of FQDN instead of an array of ReflectionParameters.
private function constructorParams(string $class): array
{
$reflectionMethod = new ReflectionMethod($class, '__construct');
$params = array_map(function (ReflectionParameter $parameter) {
return $parameter->getClass()->name;
}, $reflectionMethod->getParameters());
return $params;
}
But that would be too much responsibility for our method, don't you think? I mean, it's not only that this method is doing two things instead of one. We could live with that, considering that the second thing is only an array_map. But we are also losing access to the ReflectionParameter objects. Looking at the documentation, we can see it has a lot more to offer than the class name, so we don't want to lose access to that so soon. Who knows, maybe we end not using anything else from that parameters, but keeping the option to do it seems like a good idea, so let's move the dealing with the param reflections to another step.
private function buildDependencies(array $reflectionParams)
{
$dependencies = [];
/** @var ReflectionParameter $reflectionParam */
foreach ($reflectionParams as $reflectionParam) {
$className = $reflectionParam->getClass()->name;
$dependencies[] = $this->get($className);
}
return $dependencies;
}
In this method, we are going through each ReflectionParam and using its name to build it (for now, it seems we are online going to need the param's name after all).
Have you noticed that recursivity?
$dependencies[] = $this->get($className);
In my opinion, this is the most complex thing about the container. It's also what makes it harder to debug in case you need it.
We are using the get
method to build a class. For that, we are extracting its dependencies and using get
again to build them, but also, to build these dependencies` children, and so on.
The next step should be to add some defensive programming, so we don't go through all this trouble when a class doesn't have dependencies anymore.
if (! $reflectionClass->hasMethod('__construct')) {
return new $class;
}
public function get(string $class): object
{
// Get the reflection class
$reflectionClass = new ReflectionClass($class);
// If the class doesn't has a constructor, we don't even bother in trying to get its parameters and just build the instance and return it
if (! $reflectionClass->hasMethod('__construct')) {
return new $class;
}
// Get the constructor params. We won't get this far if this class doesn't have a constructor
$params = $this->constructorParams($class);
// Build the constructor params so we can pass it as arguments to the class we are going to build
$dependencies = $this->buildDependencies($params);
// Build and return the object from the class we were asked for
return $reflectionClass->newInstanceArgs($dependencies);
}
Let's put all the pieces together, wrap the container, and test it all.
<?php
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
class Container
{
public function get(string $class): object
{
$reflectionClass = new ReflectionClass($class);
if (! $reflectionClass->hasMethod('__construct')) {
return new $class;
}
$params = $this->constructorParams($class);
$dependencies = $this->buildDependencies($params);
return $reflectionClass->newInstanceArgs($dependencies);
}
/**
* @return ReflectionParameter[]
*/
private function constructorParams(string $class): array
{
$reflectionMethod = new ReflectionMethod($class, '__construct');
$params = $reflectionMethod->getParameters();
return $params;
}
private function buildDependencies(array $reflectionParams)
{
$dependencies = [];
/** @var ReflectionParameter $reflectionParam */
foreach ($reflectionParams as $reflectionParam) {
$className = $reflectionParam->getClass()->name;
$dependencies[] = $this->get($className);
}
return $dependencies;
}
}
/** @test */
public function it_can_create_an_instance_of_a_class_with_dependencies()
{
$container = new Container;
$object = $container->get(Car::class);
$this->assertInstanceOf(Car::class, $object);
}
Yea! It works! But how can we be sure our Car has its engine? Should we even care?
Usually, we don't test such things. I mean, the Car has declared explicitly it needs an Engine. If the container weren't providing it, the code wouldn't even run. But, in this case, we want to be extra sure, so we want to test it anyway. We want to be explicitly sure the car has an Engine inside. How do we do it?
We could change the engine's param visibility from private to public (bad idea, don't do it). We could also create a class named CarDouble
, which inherits from Car and exposes the engine through a method. That would work, sure, but since we've come so far and we just learned about Reflection, we could use it to our advantage, don't we? Let's see how.
/** @test */
public function it_can_create_an_instance_of_a_class_with_dependencies()
{
$container = new Container;
$car = $container->get(Car::class);
$this->assertInstanceOf(Car::class, $car);
$reflectionClass = new ReflectionClass($car);
$properties = $reflectionClass->getProperties();
/** @var ReflectionProperty $engineProperty */
$engineProperty = $properties[0];
$engineProperty->setAccessible(true);
$this->assertInstanceOf(Engine::class, $engineProperty->getValue($car));
}
After ensuring the object returned by the container is actually a Car, we proceed to extract the Car's properties using reflection since we also want the private properties because the engine is private. Then, we make the engine property public by calling the setAccessible(true)
. Don't worry; we are not actually changing the visibility of the Car's engine. We are only dealing with the engineProperty object. If we try to access the engine using our Car, it will fail. $car->engine // this will throw an exception
Using the engineProperty itself, is how we access the engine $engineProperty->getValue($car)
, and to do so, we need to pass the car instance. You could pass any car instance, and it will give you its engine.
That's it! We've made it! Now we have a functional Dependency Injection Container capable of creating objects from classes and dealing with its dependencies, recursively.
There are some things to consider about this container, though.
- It can only deal with typed arguments.
- It can't deal with interfaces or abstract classes.
- It can't deal with default values.
Seems a little bit limited, doesn't it? How can we improve the container to be smarter and able to deal with such scenarios?
I'll tell you next on the week. Here's a hint: configuration.
If you have any questions or concerns, have found any mistakes, or have enjoyed it, let me know on Twitter @joanmorell