Dependency Injection Container – simple implementation

If you’ve ever used any modern framework you’ve most likely used a Dependency Injection Container, also known as a DI container.

Sometimes it can feel like magic, you ask it for a class and it will give said class regardless of how many other classes it depends on.

In this article I’ll implement a simple DI container to showcase the idea behind it. The final goal being to use the container instead of doing new class.

I’ll use PHP but the idea behind it is the same for any language that supports reflective programming.

As a side note I’ll be using Composer because I don’t want to be bothered with class loading.

My directory structure will be a top level dir called src in which I’ll place every class.

The namespace will be App for autoloading purposes.

Lets start with a simple class, no dependencies, no nothing. Just the class itself.

namespace App;

class Dog {
    public function speak() : string
    {
        return "Woof";
    }
}

Normally you’d instantiate this class by doing new Dog(). Lets try to do that with a DI Container.

Lets start with a class called Container

namespace App;

class Container {

}

The idea is to use Reflection in order to instantiate any class without knowing what it is or how many other dependencies it has(in this case we know, it’s zero).

Lets modify the Container class so that it accepts a string which will be a fully qualified name of the class. PHP uses namespaces. If you come from a language such as Java you may know this concept as a package name. A fully qualified name can look like \Foo\Bar\Container.

In our case a fully qualified name will be \App\Dog.

namespace App;

class Container {
    public function make(string $class) {
        $reflectionClass = new \ReflectionClass($class);
        $constructor = $reflectionClass->getConstructor();
        if(null === $constructor) {
            return $reflectionClass->newInstance();
        }
    }
}

Taking this line by line:

  • Create a new ReflectionClass for whatever fully qualified name we receive as the parameter.
  • Get the class constructor.
  • If the constructor is null, the class has no constructor so we can simply instantiate and return it.

So far easy peazy.

Calling the Container with our Dog object is pretty straight forward

$container = new \App\Container();
$dog = $container->make(\App\Dog::class);

print $dog->speak(); // Woof

Lets make it a bit more realistic by adding a few arguments to the Dog class.

namespace App;

class Dog {
    private string $speakWord;
    
    public function __construct(string $speakWord) {
        $this->speakWord = $speakWord;
    }
    
    public function speak() : string
    {
        return $this->speakWord;
    }
}

Now that the class receives a parameter we can’t simply instantiate it because we’d get an error. And since we can’t just randomly place a string in there we need a way for the caller to give a value to the parameter(or parameters if there’s more than one).

namespace App;

class Container {

    private array $parameterBinding = [];

    public function bindParameters(string $concrete, array $parameters): self {
        foreach($parameters as $paramKey => $paramValue) {
            $this->parameterBinding[$concrete][$paramKey] = $paramValue;
        }

        return $this;
    }

    public function make(string $class) {
        $reflectionClass = new \ReflectionClass($class);
        $constructor = $reflectionClass->getConstructor();
        if(null === $constructor) {
            return $reflectionClass->newInstance();
        }
    }
}

Now we have a way to register parameters of a class.

$container = new \App\Container();
$container->bindParameters(\App\Dog::class, ['$speakWord' => 'Woof']);

$dog = $container->make(\App\Dog::class);

print $dog->speak();

Lets see how we’d use reflection to take care of instantiating the class.

In the previous version we got the constructor, for a class with no constructor the method $reflectionClass->getConstructor() returned null, but in this case it will return something. I used a debugger to access the variable in question after it was called and the results look like this

Fortunately we can see that it returns a ReflectionMethod(which makes sense since a constructor is in fact just a method). It also has documentation, but for reflection stuff specifically it’s very poorly written. We’re looking for the getParameters method of the ReflectionMethod class.

Long story short, we want something like this

    public function make(string $class) {
        $reflectionClass = new \ReflectionClass($class);
        $constructor = $reflectionClass->getConstructor();
        if(null === $constructor) {
            return $reflectionClass->newInstance();
        }

        $parameters = $constructor->getParameters();
        ...
    }

Now that we have a list of constructor parameters we can loop over them and match them with whatever the caller gave us then return a new instance of the class.

    public function make(string $class) {
        $reflectionClass = new \ReflectionClass($class);
        $constructor = $reflectionClass->getConstructor();
        if(null === $constructor) {
            return $reflectionClass->newInstance();
        }

        $parameters = $constructor->getParameters();

        foreach($parameters as $parameter) {
            $name = "$".$parameter->getName();
            if(!array_key_exists($name, $this->parameterBinding[$class])) {
                throw new \Exception("No parameter bound for class $class, expecting parameter with name $name but no matches were found");
            }

            $this->instances[$class][] = $this->parameterBinding[$class][$name];
        }

        return $reflectionClass->newInstance(...$this->instances[$class]);
    }

The $constructor->getParameters() will give us the parameters as they are named without the $ is front. So now it’s a simple matter of adding a $ and searching for them in the parameterBindings.

You’ll notice that the parameters are stored in a instances data structure. This is because we may have more than one parameter so we can’t simply create the new instances right away.

Coincidentally this also solves the problem of having more than one parameter in the constructor.

Lets modify the Dog class to have more than one parameter and see if this still works.

namespace App;

class Dog {
    private string $speakWord;
    private string $name;

    public function __construct(string $speakWord, string $name) {
        $this->speakWord = $speakWord;
        $this->name = $name;
    }

    public function speak() : string
    {
        return sprintf("%s says %s", $this->name, $this->speakWord);
    }
}

And lets test it out.

$container = new \App\Container();
$container->bindParameters(\App\Dog::class, ['$name' => 'Rex', '$speakWord' => 'Woof']);

$dog = $container->make(\App\Dog::class);

print $dog->speak();

I get the following result with the above code

Rex says Woof
Process finished with exit code 0

So far so good. We can instantiate any class with any number of parameters. We just gotta pass in the class name and all the parameters…

That doesn’t seem very useful does it. It’s easier to do new Dog(...). But wait, lets introduce a class dependency for Dog.

namespace App;

class Food {
    public function getFood() : string
    {
        return "Pork chops";
    }
}
namespace App;

class Dog {
    private string $speakWord;
    private string $name;
    private Food $food;

    public function __construct(Food $food, string $speakWord, string $name) {
        $this->food = $food;
        $this->speakWord = $speakWord;
        $this->name = $name;
    }

    public function speak() : string
    {
        return sprintf("%s says %s and then eats %s", $this->name, $this->speakWord, $this->food->getFood());
    }
}

Traditionally the Dog class would be instantiated like this

$dog = new \App\Dog(new \App\Food(), "woof", "Rex");
$dog->speak();

Imagine if Food had it’s own dependency on Ingredient and Ingredient had another dependency on another class. You can see how this can quickly get out of control. But with a DI container all dependencies can be resolved recursively without you knowing how to resolved them.

Speaking of recursion lets see how we can recursively resolve our recursive dependencies.

The goal is to traverse each constructor for each dependency that Dog has and resolve them somehow.

Currently the Food class has no parameters so it’s fairly easy to solve. Lets look at an implementation.

namespace App;

class Container {

    private array $parameterBinding = [];
    private array $instances = [];

    public function bindParameters(string $concrete, array $parameters): self {
        foreach($parameters as $paramKey => $paramValue) {
            $this->parameterBinding[$concrete][$paramKey] = $paramValue;
        }

        return $this;
    }

    public function make(string $class) {
        $reflectionClass = new \ReflectionClass($class);
        $constructor = $reflectionClass->getConstructor();
        if(null === $constructor) {
            return $reflectionClass->newInstance();
        }

        $parameters = $constructor->getParameters();

        foreach($parameters as $parameter) {
            $type = $this->getParameterClassName($parameter);
            if(null !== $type) {
                $this->instances[$class][] = $this->make($parameter->getType()->getName());
                continue;
            }

            $name = "$".$parameter->getName();

            if(!array_key_exists($name, $this->parameterBinding[$class])) {
                throw new \Exception("No parameter bound for class $class, expecting parameter with name $name but no matches were found");
            }

            $this->instances[$class][] = $this->parameterBinding[$class][$name];
        }

        return $reflectionClass->newInstance(...$this->instances[$class]);
    }

    private function getParameterClassName($parameter): ?string
    {
        $type = $parameter->getType();

        if (! $type instanceof \ReflectionNamedType || $type->isBuiltin()) {
            return null;
        }

        return $type->getName();
    }
}

The important bit is this one here

$this->instances[$class][] = $this->make($parameter->getType()->getName());

This will recursively call itself with a new class name taken from the constructor parameters.

You’ll also notice a new private method called getParameterClassName. This will check if the constructor parameter is either some instantiable object or something else(like primitive/scalar – int, string, etc). The isBuiltin method checks that. Documentation here – A built-in type is any type that is not a class, interface, or trait.

Honestly that’s just poor naming if you ask me, but it is what it is.

Regardless, if isBuiltin returns true then we got a primitive which obviously can’t be instantiated so it needs to be bound via user definition.

So now that we know if a constructor parameter can or cannot be instantiated it’s as simple as recursively calling a function until we reach a constructor that has either all primitives or a combination of primitives and classes that have no constructor arguments so they can be instantiated easily.

$container = new \App\Container();
$container->bindParameters(\App\Dog::class, ['$name' => 'Rex', '$speakWord' => 'Woof']);

$dog = $container->make(\App\Dog::class);

print $dog->speak();

This prints out

Rex says Woof and then eats Pork chops
Process finished with exit code 0

Awesome, now we can add classes to our constructor. And since we’re using recursion those classes can have other classes in the constructor. It’s classes all the way down.

Lets modify the Food class and see if it still works.

namespace App;

class Food {
    private Pork $pork;
    private Seasoning $seasoning;

    public function __construct(Pork $pork, Seasoning $seasoning)
    {
        $this->pork = $pork;
        $this->seasoning = $seasoning;
    }

    public function getFood() : string
    {
        return sprintf("%s with %s", $this->pork->getIngredient(),  $this->seasoning->getSeasoning());
    }
}
namespace App;

class Pork {
    public function getIngredient() : string
    {
        return "pork chops";
    }
}
namespace App;

class Seasoning {
    public function getSeasoning() : string {
        return "Salt and pepper";
    }
}

Running exactly the same code as before gives me this

Rex says Woof and then eats pork chops with Salt and pepper
Process finished with exit code 0

Looks like it works just fine.

Unfortunately this isn’t the end of it. What happens if we want to be good little programmers and work with interfaces or abstract classes rather than concrete classes. That’s going to be an issue since we can’t instantiate either of those. We need some way to specify which interface/abstract class should bind to what concrete implementation.

Lets modify the Seasoning class and add an interface to it.

namespace App;

interface Seasoning
{
    public function getSeasoning() : string;
}
namespace App;

class Salt implements Seasoning {
    public function getSeasoning() : string {
        return "Salt";
    }
}
namespace App;

class Pepper implements Seasoning
{
    public function getSeasoning() : string {
        return "Pepper";
    }
}
namespace App;

class Food {
    private Pork $pork;
    private Seasoning $seasoning;

    public function __construct(Pork $pork, Seasoning $seasoning)
    {
        $this->pork = $pork;
        $this->seasoning = $seasoning;
    }

    public function getFood() : string
    {
        return sprintf("%s with %s", $this->pork->getIngredient(),  $this->seasoning->getSeasoning());
    }
}

Great, now Food can receive any implementation of Seasoning interface, allowing us to easily swap implementations. Only issue is that if we run our DI Container we’ll get an error.

Fatal error: Uncaught Error: Cannot instantiate interface App\Seasoning

Well yeah, no surprise there.

So we need a way for the caller to let us know what concrete implementation to use. In other words we need to bind the interface/abstract class to a concrete class.

class Container {
    private array $parameterBinding = [];
    protected array $abstractBinding = [];
    private array $instances = [];

    public function bindAbstract(string $abstract, string $concrete): self {
        $this->abstractBinding[$abstract] = $concrete;

        return $this;
    }

    public function bindParameters(string $concrete, array $parameters): self {
        foreach($parameters as $paramKey => $paramValue) {
            $this->parameterBinding[$concrete][$paramKey] = $paramValue;
        }

        return $this;
    }

    private function makeReflectionClass(string $class) : \ReflectionClass {
        $reflectionClass = new \ReflectionClass($class);
        if($reflectionClass->isInterface() || $reflectionClass->isAbstract()) {
            if(!array_key_exists($reflectionClass->getName(), $this->abstractBinding )) {
                throw new \Exception("Trying to bind class {$reflectionClass->getName()} but no binding for it was defined");
            }
            $reflectionClass = new \ReflectionClass($this->abstractBinding[$reflectionClass->getName()]);
        }

        if(!$reflectionClass->isInstantiable()) {
            throw new \Exception("Class {$class} is not instantiable");
        }

        return $reflectionClass;
    }

    public function make(string $class) {
        $reflectionClass = $this->makeReflectionClass($class);
        $constructor = $reflectionClass->getConstructor();
        if(null === $constructor) {
            return $reflectionClass->newInstance();
        }

        $parameters = $constructor->getParameters();

        foreach($parameters as $parameter) {
            $type = $this->getParameterClassName($parameter);
            if(null !== $type) {
                $this->instances[$class][] = $this->make($parameter->getType()->getName());
                continue;
            }

            $name = "$".$parameter->getName();

            if(!array_key_exists($name, $this->parameterBinding[$class])) {
                throw new \Exception("No parameter bound for class $class, expecting parameter with name $name but no matches were found");
            }

            $this->instances[$class][] = $this->parameterBinding[$class][$name];
        }

        return $reflectionClass->newInstance(...$this->instances[$class]);
    }

    private function getParameterClassName($parameter): ?string
    {
        $type = $parameter->getType();

        if (! $type instanceof \ReflectionNamedType || $type->isBuiltin()) {
            return null;
        }

        return $type->getName();
    }
}

We’ve added a new property to the container

class protected array $abstractBinding = [];

This will be used to store a map of abstract to concrete implementation.

You’ll also notice

    public function bindParameters(string $concrete, array $parameters): self {
        foreach($parameters as $paramKey => $paramValue) {
            $this->parameterBinding[$concrete][$paramKey] = $paramValue;
        }

        return $this;
    }

This is a simple setter method, nothing special about it.

However, the key is the new makeReflectionClass method

    private function makeReflectionClass(string $class) : \ReflectionClass {
        $reflectionClass = new \ReflectionClass($class);
        if($reflectionClass->isInterface() || $reflectionClass->isAbstract()) {
            if(!array_key_exists($reflectionClass->getName(), $this->abstractBinding )) {
                throw new \Exception("Trying to bind class {$reflectionClass->getName()} but no binding for it was defined");
            }
            $reflectionClass = new \ReflectionClass($this->abstractBinding[$reflectionClass->getName()]);
        }

        if(!$reflectionClass->isInstantiable()) {
            throw new \Exception("Class {$class} is not instantiable");
        }

        return $reflectionClass;
    }

This method will check if the class is an interface or abstract via these 2 methods

$reflectionClass->isInterface() || $reflectionClass->isAbstract()

If that’s the case, then we simply look for a match of the

$reflectionClass->getName()

in the abstractBinding map. If we find it, then we overwrite

$reflectionClass = new \ReflectionClass($this->abstractBinding[$reflectionClass->getName()]);

This of course will work recursively regardless of how many nested dependencies there are.

Lets modify the call to Container with the new bindings.

$container = new \App\Container();
$container->bindParameters(\App\Dog::class, ['$name' => 'Rex', '$speakWord' => 'Woof']);
$container->bindAbstract(\App\Seasoning::class, \App\Salt::class);

$dog = $container->make(\App\Dog::class);

print $dog->speak();

This outputs

Rex says Woof and then eats pork chops with Salt
Process finished with exit code 0

But wait, lets change the Seasoning implementation so that Rex gets some variety.

$container->bindAbstract(\App\Seasoning::class, \App\Pepper::class);

Running the code again gives me

Rex says Woof and then eats pork chops with Pepper
Process finished with exit code 0

That’s fantastic, now we got a generic(please add generics to PHP, thank you) way to add as many classes or parameters as want. As well as bind interfaces/abstract classes to concrete implementations.

Lets modify the code to add an abstract class as well.

I’ll add an AnimalShelter that can take in an AbstractAnimal(the class hierarchy won’t make much sense, just go along with it…)

namespace App;

class AnimalShelter
{
    private AbstractAnimal $abstractAnimal;

    public function __construct(AbstractAnimal $abstractAnimal)
    {
        $this->abstractAnimal = $abstractAnimal;
    }

    public function speakFromAnimalShelter(): string
    {
        return $this->abstractAnimal->speak();
    }
}
namespace App;

abstract class AbstractAnimal
{
    abstract public function speak() : string;
}
namespace App;

class Dog extends AbstractAnimal {
    private string $speakWord;
    private string $name;
    private Food $food;

    public function __construct(Food $food, string $speakWord, string $name) {
        $this->food = $food;
        $this->speakWord = $speakWord;
        $this->name = $name;
    }

    public function speak() : string
    {
        return sprintf("%s says %s and then eats %s", $this->name, $this->speakWord, $this->food->getFood());
    }
}
namespace App;

class Cat extends AbstractAnimal
{
    private string $speakWord;
    private string $name;
    private Food $food;

    public function __construct(Food $food, string $speakWord, string $name) {
        $this->food = $food;
        $this->speakWord = $speakWord;
        $this->name = $name;
    }

    public function speak() : string
    {
        return sprintf("%s says %s and then eats %s", $this->name, $this->speakWord, $this->food->getFood());
    }
}

And the DI Container make code

$container = new \App\Container();
$container->bindParameters(\App\AbstractAnimal::class, ['$name' => 'Rex', '$speakWord' => 'Woof']);
$container->bindAbstract(\App\Seasoning::class, \App\Pepper::class);
$container->bindAbstract(\App\AbstractAnimal::class, \App\Dog::class);

$shelter = $container->make(\App\AnimalShelter::class);

print $shelter->speakFromAnimalShelter();

You’ll notice that now we no longer bindParameters to a concrete class, but to an abstract class.

This will output

Rex says Woof and then eats pork chops with Pepper
Process finished with exit code 0

Lets swap this out to a cat instead of a dog.

$container = new \App\Container();
$container->bindParameters(\App\AbstractAnimal::class, ['$name' => 'Fifi', '$speakWord' => 'Meow']);
$container->bindAbstract(\App\Seasoning::class, \App\Pepper::class);
$container->bindAbstract(\App\AbstractAnimal::class, \App\Cat::class);

$shelter = $container->make(\App\AnimalShelter::class);

print $shelter->speakFromAnimalShelter();
Fifi says Meow and then eats pork chops with Pepper
Process finished with exit code 0

It’s almost complete. We have the ability to bind parameters, bind interfaces/abstract classes and we can have(technically) an infinite nesting of dependencies that will get auto-resolved.

But what happens if a parameter is optional?

Of course it will fail with a fatal error since we didn’t account for that. Fortunately the fix is quite easy.

All we need to do is check if a constructor parameter isOptional

if(!array_key_exists($name, $this->parameterBinding[$class])) {
    if($parameter->isOptional()) {
        continue;
    }
    throw new \Exception("No parameter bound for class $class, expecting parameter with name $name but no matches were found");
}

If we don’t find a match for it in the parameterBinding map then we check if it’s optional, if it is, we just continue.

Lets modify the Dog class to include an optional parameter in the constructor.

namespace App;

class Dog extends AbstractAnimal
{
    private string $speakWord;
    private string $name;
    private Food $food;
    private bool $hasChip;

    public function __construct(Food $food, string $speakWord, string $name, bool $hasChip = true)
    {
        $this->hasChip = $hasChip;
        $this->food = $food;
        $this->speakWord = $speakWord;
        $this->name = $name;
    }

    public function hasChip(): bool
    {
        return $this->hasChip;
    }

    public function speak(): string
    {
        return sprintf("%s says %s and then eats %s", $this->name, $this->speakWord, $this->food->getFood());
    }
}

Now we have a bool $hasChip which defaults to true.

$container = new \App\Container();
$container->bindParameters(\App\Dog::class, ['$name' => 'Rex', '$speakWord' => 'Woof']);
$container->bindAbstract(\App\Seasoning::class, \App\Salt::class);

$dog = $container->make(\App\Dog::class);

print $dog->speak();

print $dog->speak();
print PHP_EOL;
print $dog->hasChip();
Rex says Woof and then eats pork chops with SaltRex says Woof and then eats pork chops with Salt // $dog->speak();
true // $dog->hasChip();
Process finished with exit code 0

If you want to play around with the code, you can find it on my github


Conclusion

Regardless of the language you choose to code in the principles of a DI Container are the same. Use reflection and recursion to create new classes.

Some DI Containers allow you to specify bindings directly. A good example of that is Laravel others offer the option to specify various bindings in configs files, Symfony uses that path(although I do believe you can specify them directly too).

Others have it down to magic if you don’t know what’s happening behind the veil. Spring uses annotations such as @ComponentScan, @Bean, and a few others to generate bindings. Although other methods besides annotations are available they seem to be the most popular, at least in recent years.

So it can seem that annotations like @Autowire just magically work, when in fact it’s just bindings(most of the time contextual) that you’re simply not aware of because they’ve been abstracted away.

Most frameworks have at their heart a DI Container of some sort. Thus it’s a very important concept to understand.

While this article is not by any means complete and it’s a very crude example of a complex topic I believe it’s sufficient for a basic understanding of what lies behind Dependency Injection Containers.


Posted

in

by