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.
Top comments (0)