In this guide, you'll learn about dependency injection and a way to build a simple DIC (Dependency Injection Container) in PHP using PSR-11 from scratch.
First,
What is an Ordinary Dependency?
This is when an object depends on another object to function, it is as simple as that. Look at the following =>
<?php
class Data {
private $post;
public function __construct() {
$this->post = new Post();
}
public function getPostData(){
return $this->post->getPost();
}
}
Here, we have a class Data that obviously depends on Post, we initialize the post dependency in the constructor, we then get the post with the getPostData() method.
To use this, we can do:
(new Data())->getPostData();
What you need to understand is that, the Data object is depending on an object to function, without the post object it obviously won't work. This is an example of Ordinary Dependency.
The problem with this approach is that the Data object is constructing the dependency itself, and we are likely to face issues if we want to edit the Data, for example, if we wanna use a Page object, we would do something like =>
<?php
class Data {
private $post;
private $page;
public function __construct() {
$this->post = new Post();
$this->page= new Page();
}
public function getPostData(){
return $this->post->getPost();
}
public function getPageData(){
return $this->page->getPage();
}
}
If we wanna add Menu, we would repeat the same steps, you can see where still can get messy. The Data object would end up knowing too much, and here is where...
Dependency Injection Comes to Our Rescue
So, what is dependency injection, and how can it help us solve the aforementioned issue:
First, dependency injection is when object (e.g. our Data object) receive instances of objects from other codes, instead of the Data Object having to construct its dependencies itself, the dependency is simply passed to it from an outside code, here is an example =>
<?php
class Data {
private $post;
public function __construct($post) {
$this->post = $post;
}
public function getPostData(){
return $this->post->getPost();
}
}
To use this, we can do =>
$post = new Post();
(new Data($post))->getPostData();
You see, this is just a way of passing the dependency to the Data object without the Data object doing it itself, the good thing about this is that, you can easily change the dependency. We can make our code better by type hinting the dependency we want to receive =>
<?php
class Data {
private $post;
public function __construct(Post $post) {
$this->post = $post;
}
public function getPostData(){
return $this->post->getPost();
}
}
This is saying the $post
instance should be of type Post. You don't have to do this, but this helps to catch error in the occasion you erroneously add a wrong type plus it also gives you a bit of documentation. Adding more injection is as simple as doing:
.......
public function __construct(Post $post, Page $page) {
$this->post = $post;
$this->page= $page;
}
......
Well, hold on! If you've been looking closely, you'll notice that the above isn't really difference from Ordinary Dependency except this time we are passing it to Data object constructor, to add more dependency would still prove a bit cumbersome.
The solution to this is by using an interface, so, you'll create a DataInterface and describe the common method (e.g get()
) the classes implementing it would have, by doing this, you can freely change the Data object dependency with ease.
Now, let's move to...
Dependency Injection Container
Now imagine you have a tool that can automatically build your object dependencies for you, and that is what DIC is all about, it is really useful when you need to manage a lot of different objects with a lot of dependencies.
In the above example, you would have noticed you have to know all the dependencies you need before you can create the actual object, it would even be worse if you have nested dependencies (object that depends on another object that also depend on another and so on).
This is the usefulness of Dependency Injection Container, it is an object that knows how to instantiate and configure object dependencies, the way it does this is, it needs to know the relationship between the objects.
For cases like this, we would write a DIC to manage the dependencies for us, let's get started.
The best way to understand this is to open your IDE and type along, you'll thank me later...
Installing and Using The PSR-11 Interface
We would be using the PSR-11 container interface, this interface provides us the method with no implementation, so, we would implement the actual functionality ourselves, the advantage of using a PSR is it provides interoperability of components, this way any developer creating a container library can follow the same standards, and that would yield better code readability and maintainability since incorporating the standard means standardizing the way it is done across projects.
Install psr-11 using:
composer require psr/container
Creating Container Project Directory
Now, you can create a Container directory with the file Container that has the following:
<?php
namespace App\Container;
use Psr\Container\ContainerInterface;
class Container implements ContainerInterface
{
/**
* This holds all of the id of the instance passed to the container
* @var array
*/
private array $instances = [];
/**
* @inheritDoc
*/
public function get($id, $parameters = [])
{
}
/**
* @inheritDoc
*/
public function has(string $id): bool
{
}
}
Abstract Overview of The get and has method
The get()
and has()
method was from the PSR container interface. The get($id, $parameters = [])
is how to get the container by its identifier which is then returned, and the has(string $id)
method is for returning Boolean (TRUE/FALSE), so, if the container can return an entry for the given identifier, we return true, and false otherwise.
The private property ($instances
) is an array that store the classes that needs its dependency taken care of.
The set and has method
Now, everything is meaningless without setting the id, so let's add a set()
method:
/**
* @param $id
* @param Closure|null $concrete
*/
public function set($id, Closure $concrete = NULL)
{
if (is_null($concrete)){
$concrete = $id;
}
$this->instances[$id] = $concrete;
}
/**
* @inheritDoc
*/
public function has(string $id): bool
{
// simple return a bool if we have an $id in the instance array or not
return array_key_exists($id, $this->instances);
}
The set method has two parameters, one is the id, this is used in identifying the class that wants its dependency to be taken care of, and the second parameter takes a closure. The closure parameter in most cases is the parameter the container needs to load the dependency of the class, this would be done by the container itself, so, we only need to worry about the id.
The has is self-descriptive, it simply checks if we have an $id of the instance, and it returns a Boolean. I won't be using it in this guide, but you can if you wanna take things a bit further.
So, before going further, add a dump after the $this->instances[$id] = $concrete;
like so:
................
$this->instances[$id] = $concrete;
dump($this->instances);
Let's try to set something to the container =>
$container = new Container();
$container->set('Post');
// Output is:
array:1 [▼
"Post" => "Post"
]
I am trying to break it down little by little, so, what is happening is, when we set an identifier, the set method checks if the parameter that is taking a closure is null, if it is, it overrides the closure parameter with the identifier, having done that, it then set the value of the identifier to concrete, here: $this->instances[$id] = $concrete;
which is why we have the above array result.
But if the concrete isn't null, it set the value of the identifier to whatever is passed, so, most times, the container would be the one passing the parameters. Now, remove the dump function.
Now, before we go further, let's revisit the Data and Post class we were working with, here is the Data class:
<?php
namespace App\Container;
class Data {
private $post;
public function __construct(Post $post) {
$this->post = $post;
}
public function getPostData(){
return $this->post->getPost();
}
}
and here is the Post Class:
<?php
class Post {
/**
* @return object
*/
public function getPost(): object
{
return [
'Title' => 'Post One',
'Body' => 'I am the post',
];
}
}
So, without Dependency Injection Container, we can handle the dependency ourselves like so:
$post = new Post();
$data = new Data(post: $post);
$data = $data->getPostData();
dump($data); // Output is: array:2 [▼ "Title" => "Post One" "Body" => "I am the post" ]
The above is without Dependency Injection Container, if we were to use DIC, all we might have needed to pass is just the Data::class
and it would resolve the dependency for us. The good thing about this is that if Post also depends on something (maybe Database), DIC would recursively resolve the dependency. So, let's get back to our DIC creation, here is what we have so far in our Container.php =>
<?php
class Container implements \Psr\Container\ContainerInterface
{
/**
* This holds all of the id of the instance passed to the container
* @var array
*/
private array $instances = [];
/**
* @param $id
* @param Closure|null $concrete
*/
public function set($id, Closure $concrete = NULL)
{
if (is_null($concrete)){
$concrete = $id;
}
$this->instances[$id] = $concrete;
}
/**
* @inheritDoc
*/
public function get(string $id)
{
// TODO: Implement get() method.
}
/**
* @inheritDoc
*/
public function has(string $id): bool
{
// TODO: Implement has() method.
}
}
Implementing the get method
The next line of action is a way to get the dependency resolved, now in your get method add the following =>
/**
* @inheritDoc
*/
public function get($id, $parameters = [])
{
// if we don't have it, just set it anyway
if (!isset($this->instances[$id])) {
$this->set($id);
}
return $this->resolve($this->instances[$id], $parameters);
}
Whenever we call the get method to resolve a dependency of an object, the first thing it does is, it checks if the object isn't set in the instances
array, if it isn't, we set it anyway, we can choose to return null, but setting it would mean, perhaps we forgot to set it, so, we set it implicitly.
The resolve method
The next step is where the magic happens, we resolve the dependency, and here is the rest of the container implementation:
/**
* The resolve method would iterate over the instances that has been...
* passed to the container, and resolve it
* @param $id
* @param $parameters
* @return object
* @throws ReflectionException
* @throws ExceptionDependencyNotInstantiable
*/
public function resolve($id, $parameters)
{
$reflector = new ReflectionClass($id);
// get class constructor
$__construct = $reflector->getConstructor();
// Checks if the function is an anonymous function
// if so, we return it return $id($this, $parameters);
// $this is the current class instance, so, it contains info about this container or whatever
// properties are in this container
// $parameters are the object parameter if any is set
if ($reflector->isAnonymous()) return $id($this, $parameters);
// if class is not instantiatable we throw exception
if (!$reflector->isInstantiable()) throw new ExceptionDependencyNotInstantiable("Class $id not instantiable.");
// If there is no constructor in class, just return the object
// by returning the object, you can think of it has (new Class())
if(!$__construct) return $reflector->newInstance();
$dependencies = $this->resolveDependencies($__construct->getParameters());
// This create a new class instance from $dependencies retrieved
return $reflector->newInstanceArgs($dependencies);
}
public function resolveDependencies($parameters){
$dependencies = [];
foreach ($parameters as $param) {
// this get the type hinted class in the constructor
// so, if the class constructor has __construct(Post $post)...
// then the type hinted class is Post
if($paramType = $param->getType()->getName()) {
$dependencies[] = $this->get($paramType);
// dd($dependencies);
}
}
return $dependencies;
}
To better understand, let's assume we wanna resolve the following dependencies:
$data = new Data(
new Post(new Test()),
new Page()
);
An Instance of Class Data -> Depends on an Instance of Post and Page Class:
// CLASS DATA
class Data {
private Post $post;
private Page $page;
public function __construct(Post $post, Page $page) {
$this->post = $post;
$this->page = $page;
}
public function getPostData(){
return $this->post->getPost();
}
public function getPageData(){
return $this->page->getPage();
}
}
// CLASS POST
class Post {
public Test $test;
public function __construct(Test $test){
$this->test = $test;
}
/**
* @return array
*/
public function getPost(): array
{
return [
'Title' => 'Post One',
'Body' => 'I am the post',
'Test' => $this->test->getTest()
];
}
}
// CLASS PAGE
class Page
{
public function getPage(): array
{
return [
'Title' => 'Page One',
'Body' => 'I am the page',
];
}
}
The Post also has a Test instance dependency, so, here is the Class Test:
// CLASS TEST
class Test
{
public function getTest()
{
return 'This is a test';
}
}
So, whenever you call:
$data = (new Container())->get(Data::class)
It turns it into this behind the scene:
$data = new Data(
new Post(new Test()),
new Page()
);
So, how does it do it? It is simple =>
STEP 1: PASSING THE CLASS =>
The moment you passed the Data::class to the get method, the get method checks if what you passed hasn't already been passed before, and if that is true, we keep it in an array, which is $this->instances.
STEP 2: SEND THE CLASS FOR DEPENDENCY RESOLVING =>
If step 1 is done, we then call the resolve method to resolve it dependencies
return $this->resolve($this->instances[$id], $parameters);
STEP 3: THE RESOLVE METHOD =>
Inside the resolve method, we examine the class you send in step 2 using ReflectionClass, here:
$reflector = new ReflectionClass($id);
See ReflectionClass has a way to know the details of the class, e.g, its constructor, methods, properties, and about everything you have in the class.
STEP 4: GETTING THE CLASS CONSTRUCTOR =>
Now, we can go ahead and store its constructor in a variable, recall that in step 3, the reflector has already given us everything about the Class, so, getting the constructor is as simple as doing:
$__construct = $reflector->getConstructor()
If for example, the class has: __construct(Post $post, Page $page), then by getting the constructor, you are simply getting the following:
ReflectionMethod {#7 ▼
+name: "__construct"
+class: "App\Container\Data"
parameters: {▼
$post: ReflectionParameter {#21 ▼
+name: "post"
position: 0
typeHint: "App\Container\Post"
}
$page: ReflectionParameter {#20 ▼
+name: "page"
position: 1
typeHint: "App\Container\Page"
}
}
}
You can see how it packed the constructor for you,
STEP 5: CHECK IF CLASS IS ANONYMOUS =>
so, moving on, we check if the class is of anonymous, if so, we return it, here:
if ($reflector->isAnonymous()) return $id($this, $parameters);
STEP 6: CHECK IF CLASS IS INSTANTIABLE =>
We also check if the class is instantiable (instantiable meaning can you instantiate it i.e new Data()), if it is not instantiable, we throw an exception, here:
if (!$reflector->isInstantiable()) throw new ExceptionDependencyNotInstantiable("Class $id not instantiable.");
For the exception, create a new folder called Exception and you should also create a class called ExceptionDependencyNotInstantiable (give it any name appropriate), having done that, add the following in the class you just created:
<?php
use Exception;
use Psr\Container\ContainerExceptionInterface;
class ExceptionDependencyNotInstantiable extends Exception implements ContainerExceptionInterface{}
STEP 7: RETURN CLASS INSTANCE IF THERE IS NO CONSTRUCTOR =>
In the event that the class doesn't have a constructor, we instantiate, here:
if(!$__construct) return $reflector->newInstance();
STEP 8: RESOLVE DEPENDENCIES IF CLASS HAS CONSTRUCTOR =>
A class that doesn't have a construtor, won't get to step 8, it would just return an instance back to wherever it is called, but if a class does have a constructor we get its dependencies, here:
$dependencies = $this->resolveDependencies($__construct->getParameters());
Here is the implementation of the resolveDependencies() method:
public function resolveDependencies($parameters){
$dependencies = [];
foreach ($parameters as $param) {
// this get the type hinted class in the constructor
// so, if the class constructor has __construct(Post $post)...
// then the type hinted class is Post
if($paramType = $param->getType()->getName()) {
$dependencies[] = $this->get($paramType);
}
}
return $dependencies;
}
We initialized a dependencies array, we then beging loop through the parameters passed to the resolveDependencies() method, the paarameters of the Data class would look something like so:
array:2 [▼
0 => ReflectionParameter {#8 ▼
+name: "post"
position: 0
typeHint: "App\Container\Post"
}
1 => ReflectionParameter {#9 ▼
+name: "page"
position: 1
typeHint: "App\Container\Page"
}
]
So, here:
if($paramType = $param->getType()->getName()) {
$dependencies[] = $this->get($paramType);
}
we are saying if there is a ParameterType, if there is one, we call $this->get($paramType); in a recursive mode, this means, it goes back to step 1 with the new class name until there is no dependency to resolve. If this souds confusing, dump the dependencies, so, you can see how everything wires. add it after storing it, here:
if($paramType = $param->getType()->getName()) {
$dependencies[] = $this->get($paramType);
dump($dependencies); //<- here
}
You should get the following:
^ array:1 [▼
0 => App\Container\Test {#14}
]
^ array:1 [▼
0 => App\Container\Post {#12 ▼
+test: App\Container\Test {#14}
}
]
^ array:2 [▼
0 => App\Container\Post {#12 ▼
+test: App\Container\Test {#14}
}
1 => App\Container\Page {#22}
]
The above is how tjhe execution played out, and lastly we create the new instance from the dependencies retrieved, here:
return $reflector->newInstanceArgs($dependencies);
and that is it.
So, resolving our dependency is as easy as doing:
$data = (new Container())->get(Data::class);
I know you'll have some question, send them away!