How To Create A Psr-11 Service Container In Php

How To Create A Psr-11 Service Container In Php

Given Ncube

I will not 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 PSR-11 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 PSR 11 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 add the 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

<?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 interface. To do that add this to the body of the class.

/**
 * 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 a string $id as an argument 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 property, 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 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 has been 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 strings as arguments $abstract which is the id and $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(
            "Error: {$reflector->getName()} 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 it cannot instantiate the reflected class. 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. Tries to return a reflection class of that class 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 it has set its value 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 straightforward

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 singleton classes. You can find all the code in this article here.

Let's 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.