DEV Community

loading...

How to create a psr11 dependency injection container in php

Given Ncube
A tech geek I guess
・5 min read

Dependency Injection, such a fancy name haa, I'm not going to explain what dependency injection is, the fact that you're here means you already know that. If not check out this article about dependency injection. In this article, I'm going to show you how to implement a psr11 compliant dependency injection container.

First of what is a container

According to Dependency Injection, Principles, Practices, And Patterns, a dependency injection container is

a software library that provides DI functionality and allows automating many of the tasks involved in Object Composition, Interception, and Lifetime Management. DI Containers are also known as Inversion of Control (IoC) Containers

In this article, we're going to implement psr11 interfaces for dependency injection. The first thing you need to do is go to your project folder and install the psr11 packages

composer require psr/psr-container

After it's done downloading the packages open your favorite text editor, I'm using atom but you can use whatever you like.
Open the

composer.json

file and enter the following lines of code

autoload:{
psr-4: {
"Flixtechs\\": "src/"
}
}

Create a new file and call it

index.php

and put following

<?php

//add composer's autoloader
require 'vendor/autoload.php';

In the project, folder create a new file

src/Container.php

and add the following lines of code

<?php

namespace Flixtechs;

use Psr\Container\ContainerInterface;

class Container implements ContainerInterface 
{
}

We need a way to keep track of registered entries in the container as key-value pairs. Add the entries property to the class

/**
 * Store all entries in the container
 */
 protected $entries = [];

/**
 * Store a list of instantiated singleton classes
 */ 
 protected $instances = [];

/**
 * Rules used to resolve the classes
 */
protected $rules = [];

Now we need the implement the

get()

method of psr11 interfaces add this to the body of the


 class


```php
/**
 * Finds an entry of the container by its identifier and returns it.
 *
 * @param string $id Identifier of the entry to look for.
 *
 * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
 * @throws ContainerExceptionInterface Error while retrieving the entry.
 *
 * @return mixed Entry.
 */
public function get($id)
{
 if (!$this->has($id)) {
  $this->set($id);
 }
 if ($this->entries[$id] instanceof \Closure || is_callable($this->entries[$id])) {
  return $this->entries[$id]($this);
 }
 if (isset($this->rules['shared']) && in_array($id,   $this->rules['shared'])) {
  return $this->singleton($id);
 }
 return $this->resolve($id);
}

The method takes an

$id

and first checks if it's in the container if not it adds it. If the value in the entries is a closure it calls the closure that will resolve the object we want. Next, it checks if the

$id

is in the

$shared

array, that is it should be a singleton class and calls the singleton method which we'll see in a moment. Finally, if the above conditions are not met it calls its resolve method to get the class.
Let's take a look into the

has()

method

/**
 * Returns true if the container can return an entry for the given identifier.
 * Returns false otherwise.
 *
 * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
 * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
 *
 * @param string $id Identifier of the entry to look for.    
 * @return bool
 */
public function has($id)
{
    return isset($this->entries[$id]);
}

This method simply checks if the given id is set in the

$entries

array.
Next is the

set()

method

public function set($abstract, $concrete = null)
{
    if(is_null($concrete)) {
    $concrete = $abstract;
    }
    $this->entries[$abstract] = $concrete;
}

The set method takes 2 arguments abstract which is the id and the concrete. The concrete can be a closure or full class name.

Now let us move to the

resolve()

method where the "magic" happens

/**
 * Resolves a class name and creates its instance with dependencies
 * @param string $class The class to resolve
 * @return object The resolved instance
 * @throws Psr\Container\ContainerExceptionInterface when the class cannot be instantiated
 */
public function resolve($alias)
{
    $reflector = $this->getReflector($alias);
    $constructor = $reflector->getConstructor();
    if ($reflector->isInterface()) {
        return $this->resolveInterface($reflector);
    }
    if (!$reflector->isInstantiable()) {
        throw new ContainerException(
                "Cannot inject {$reflector->getName()} to {$class} because it cannot be instantiated"
          );
    }
    if (null === $constructor) {
        return $reflector->newInstance();
    }
    $args = $this->getArguments($alias, $constructor);
    return $reflector->newInstanceArgs($args);
}

This method takes an id as an argument and tries to instantiate the class.
Here we are using the Reflection API to help us resolve the classes. The first call to

getReflector()

returns to the reflection of the given id. Next, we get the constructor of the class by calling the

getConstructor()

method of the reflector. Then we check if the reflected class is an Interface then call the method to resolve a class from a type hinted interface which we'll see in a moment.
Next, it throws an exception if the reflected class is not instantiable. If the class does not have a constructor we simply return its instance.
Next, we get all the arguments by calling the

getArguments()

method of our container. Then finally return a new instance with the arguments by calling the

newInstanceArgs($args)

method of the reflector.

The

getReflector()

method

public function getReflector($alias)
{
    $class = $this->entries[$alias];
    try {
        return (new \ReflectionClass($class));
    } catch (\ReflectionException $e) {
        throw new NotFoundException(
                $e->getMessage(), $e->getCode()
          );
    }
}

The method gets the class from the entries array. Try to return a reflection class of that and throw an exception if it fails. You can implement those exceptions on your own

The

getArguments()

method

/**
 * Get the constructor arguments of a class
 * @param ReflectionMethod $constructor The constructor
 * @return array The arguments
 */
public function getArguments($alias, \ReflectionMethod $constructor)
{
    $args = [];
    $params = $constructor->getParameters();
    foreach ($params as $param) {
        if (null !== $param->getClass()) {
            $args[] = $this->get(
                $param->getClass()->getName()
             );
        } elseif ($param->isDefaultValueAvailable()) {
            $args[] = $param->getDefaultValue();
        } elseif (isset($this->rules[$alias][$param->getName()])) {
            $args[] = $this->rules[$alias][
                $param->getName()
             ];
        }
    }
    return $args;
}

The method takes the alias and the reflectionMethod of the constructor. It calls the

getParameters()

to get an array of

ReflectionParameters

. Next, it loops through all the parameters. First check if the parameter is a type hinted class, if so it calls the

get()

method of the container to resolve the class.
If it's not a class it checks if it has a default value. If it has a default it pushes that into the

$args

array. If the argument is not a type hinted class and it doesn't have a default value it checks if its value has been set in the

$rules

array via the

configure()

method and pushes that value to the

$args

array. Last it returns the

$args

array.

The configure method is straightforwad

public function configure(array $config)
{
 $this->rules = array_merge($this->rules,$config);
 return $this;
}

I left out the

resolveInterface()

and

singleton()

methods to resolve a class from a type hinted interface and signleton classes. You can find all the code in this article here

Lets see if our container works

Let's create three classes

class Task
{
public function __conctruct(Logger $loger)
{
    echo "task created\n";
}
}

class Logger
{
public function __construct(DB $database)
{
    echo "Logger created\n";
}
}

class DB
{
public function __construct()
{
    echo "DB created";
}
}

Now say we want to instantiate the Task class. The traditional way would be like this

$db = new DB();
$logger = new Logger($db);
$task = new Task($logger);

Using our DI container, however, we'll be doing it like this. Put this in your index.php file

use Flixtechs\Container;

$container = new Container;

$container->get(Task::class);

The container will take of everything.
Now run

composer dumpautoload

and then

php index.php

It should output the following

Db created
Logger created
Task created

That was one way of doing it, you can't use this code in production though I wanted to clarify the magic with DI containers and show you how to build your own from scratch. But you can improve it from here.

Discussion (0)