Backend Development

Implementing Simple IOC Container In PHP

Implementing Simple IOC Container In PHP

In this post we will learn about the ioc (inversion of control) container, how it it works and we will implement a simple script in php that simulates the container functionality.

 

 

Many today frameworks uses a special tool to manage classes and dependencies, this tool is the IOC (inversion of control) container. If you worked with frameworks such as symfony or laravel perhaps you see how this tool works. Although each framework has specific implementation of this tool but the concept is the same.

What IOC can do:

  • The first thing and the most important IOC can do is handling dependency injected services in constructors or other methods.
  • Registering external packages and libraries into the container to be used through the application.
  • Creating singleton instances.
  • Binding a Type to the container.
  • Resolving a Type from the container.

Also beside to that each framework can have additional features in it’s IOC container.

You can thing of IOC as a stack that hold the application dependencies in an array and we can fetch each dependency at runtime through the application instance, for example in laravel you can get the translator from the container like so:

app('translator')->setLocale()

Now let’s see how IOC handle dependency injection dynamically. Let’s illustrate this with an example:

Imagine we have this front controller class:

FrontController.php

<?php

require_once __DIR__.'/Service.php';

class FrontController
{
    private $service;

    public function __construct(Service $service)
    {
        $this->service = $service;
    }

    public function index()
    {
        return json_encode($this->service->getData());
    }
}

This class uses a Service class to retrieve some data:

Service.php

<?php

require_once __DIR__ .'/MysqlRepository.php';

class Service
{
    private $mysqlRepository;

    public function __construct(MysqlRepository $mysqlRepository)
    {
        $this->mysqlRepository = $mysqlRepository;
    }

    public function getData()
    {
        return $this->mysqlRepository->getAll();
    }
}

MysqlRepository.php

<?php


class MysqlRepository
{
    public function getAll()
    {
        // for demonstration purposes only, i return the data as a php array
        return [
            [
                "id" => 1,
                "title" => "product 1",
                "price" => "$120"
            ],
            [
                "id" => 2,
                "title" => "product 2",
                "price" => "$450"
            ]
        ];
    }
}

In this example, i created three classes that talk to each other in terms of dependency injection.

At the first the request reaches at the front controller class which i inject the Service class in the constructor function and invoked the Service::getData() method.

In the same way in the Service class, also injected the MysqlRepository class which should return data from mysql using a mysql query by calling the getAll() method. Note that here for the purpose of the example i returned a php array.

At this point a typical usage of the FrontController can be like so:

client.php

<?php

require_once __DIR__ . '/FrontController.php';

$controller = new FrontController(new Service(new MysqlRepository()));

echo($controller->index());

As you see the important thing i am mentioning here is this line:

$controller = new FrontController(new Service(new MysqlRepository()));

As shown how we passed the instances in the constructor. The front controller accepts an instance of Service which i passed using new Service(). The service in turn needs an instance of MysqlRepository which i passed into the service constructor using new MysqlRepository. This is a simple example with three classes only, but in real world projects there be many many classes.

This process is done dynamically by the IOC which we will implement in the next section.

 

Implementing The IOC

As listed on php-fig website the PSR-11 responsible for implementing containers, you can read more about it if you have to.

Create a new directory in your server root folder called “custom-ioc”. I will use composer and we be using psr/container package .

Inside the project folder open the terminal and run this command to create composer.json:

composer init

Complete all the steps to generate the file, then install psr/container with this command:

composer require psr/container

Then create a new directory inside the project called “app“, this will hold the container source code, and update the composer.json to map this directory to a namespace like so:

composer.json

"autoload": {
      "psr-4": {
      	"App\\": "app"
      }
    }

This maps the app/ directory to the App namespace. So every file or folder in the app/ directory will begin with App.

Inside the app/ directory create this folder structure as shown:

 

 

 

Creating The Exception Class

As mentioned in the php-fig we must implement the NotFoundExceptionInterface like so:

app/Exceptions/NotFoundException.php

<?php


namespace App\Exceptions;


use Psr\Container\NotFoundExceptionInterface;

class NotFoundException extends \Exception implements NotFoundExceptionInterface
{

}

The NotFoundExceptionInterface comes with the Psr/Container package and it’s supposed that we through this exception in case specific dependency not found in the container.

 

Creating The Container Class

Inside the app/Container create a new class Container.php

app/Container/Container.php

namespace App\Container;


use App\Exceptions\NotFoundException;
use Psr\Container\ContainerInterface;

class Container implements ContainerInterface
{
    private $services = [];

    public function get(string $id)
    {
         
    }

    public function has(string $id): bool
    {
        
    }
}

As we implemented the NotFoundExceptionInterface above also the container implementation must implement the Psr/Container/ContainerInterface. This interface has two methods that need to be implemented the get() and has() methods respectively.

The get() method finds and returns an entry which in this case a service from the container by an identifier.

The has() method returns a boolean indicating that the container has an entry using an identifier.

The Container class should register classes and callable functions which i done below.

I updated the file and added specific implementation to those methods and added additional extra methods shown in the code below:

Container.php

<?php


namespace App\Container;


use App\Exceptions\NotFoundException;
use Psr\Container\ContainerInterface;

class Container implements ContainerInterface
{
    private $services = [];

    public function register(string $key, $value)
    {
        $this->services[$key] = $this->resolveDependency($value);
        return $this;
    }

    public function get(string $id)
    {
        try {
            if(isset($this->services[$id])) {
                return $this->services[$id];
            } else {
                $this->services[$id] = $this->resolveDependency($id);
                return $this->services[$id];
            }

        } catch (\ReflectionException $ex) {
            throw new NotFoundException($ex->getMessage());
        } catch (\Exception $ex) {
            throw new NotFoundException($ex->getMessage());
        }
    }

    public function has(string $id): bool
    {
        return isset($this->services[$id]);
    }

    public function getServices()
    {
        return $this->services;
    }

    private function resolveDependency($item)
    {
        // if item a callback
        if(is_callable($item)) {
            return $item();
        }

        // if item a class
        $reflectionItem = new \ReflectionClass($item);
        return $this->getInstance($reflectionItem);
    }

    private function getInstance(\ReflectionClass $item)
    {
        $constructor = $item->getConstructor();
        if (is_null($constructor) || $constructor->getNumberOfRequiredParameters() == 0) {
            return $item->newInstance();
        }

        $params = [];

        foreach ($constructor->getParameters() as $param) {

            if ($type = $param->getType()) {
                $params[] = $this->get($type->getName());
            }
        }

        return $item->newInstanceArgs($params);

    }
}

In this code i declared the $services property to hold the loaded services in the app. Then i added another method register(). The register() accepts a service identifier and a value, and push the service into the $services array.

The register() invoke the resolveDependency() helper method which resolve the service and return either an instance in case of a class or a function return in case of a callable function:

private function resolveDependency($item)
    {
        // if item a callback
        if(is_callable($item)) {
            return $item();
        }

        // if item a class
        $reflectionItem = new \ReflectionClass($item);
        return $this->getInstance($reflectionItem);
    }

Example registering specific class:

$container = new Container();

$container->register(App\Services\CustomService::class, App\Services\CustomService::class);

Example registering specific a callable:

$container = new Container();

$container->register("Closure_" . uniqid(), function () {
    //
});

The resolveDependency() method uses the PHP Reflection Api to make a class intance. We create an instance from the ReflectionClass() using the full class namespace, then i invoke another method getInstance() responsible for returning instance of specific service.

In the getInstance() method we retrieve the item constructor, and check to see if the constructor have any arguments. If the constructor contains no arguments then we return an instance directly using the Reflection api newInstance() method.

In the class constructor have arguments, then we loop over the arguments and push them into the $params array. And finally we return an instance using another method in the Reflection api which is newInstanceArgs($params)

Note that this process is recursive, meaning that when we retrieve specific argument we must call the get() method again and again to check if this service also contains arguments and retrieve them.

In this stage we stored the service into the $services array, now the services array contains each service id and the created instance of this service.

In the get() method we look over the service in $services array using the identifier and return it. In case the service not found, then we try to add it again and return it. Otherwise we throw a NotFoundException.

In this way we cache the service into the array so we don’t need to it every time in the container.

The has() method check to see if service exists using service identifier and return true or false.

The getServices() method return the loaded services.

 

Testing The Container

To test the container we have to make some classes and register them, but i have another approach, let’s make another class in the app/ directory and add this code:

<?php


namespace App;


use App\Container\Container;

class Application extends Container
{
    private $routes = [];

    public function setRoute($uri, $action)
    {
        $this->routes[$uri] = $action;

        return $this;
    }

    public function run()
    {
        $currentUri = $_REQUEST["route"];

        if(array_key_exists($currentUri, $this->routes)) {
            $action = $this->routes[$currentUri];

            if(is_array($action)) {
                $controller = $action[0];
                $method = $action[1];

                $this->register($controller, $controller);

                $instance = $this->get($controller);
                echo $instance->$method();
            } else {
                if(is_callable($action)) {
                    $callbackId = get_class($action) . "_" . uniqid();
                    $this->register($callbackId, $action);
                    echo $this->get($callbackId);
                }
            }
        }
    }
}

The Application class extend the Container class and has the setRoute() method which stores routes into the $routes array.

The run() method parse a controller route into an action and return the result, just like in laravel routes. This is done by checking if there a query string called “route” and checking if this query string exists inside the $routes array.

If the route exists then we fetch the value from the $routes array. If the fetched value is an array, then i retrieve the controller and method, register the controller using $this->register(), retrieve the instance using $this->get() and finally calling the controller action.

 $controller = $action[0];
 $method = $action[1];

 $this->register($controller, $controller);

 $instance = $this->get($controller);
 echo $instance->$method();

If the fetched value is a callable then i compose a callable id, register the callable, and then calling get() directly like so:

if(is_callable($action)) {
   $callbackId = get_class($action) . "_" . uniqid();
   $this->register($callbackId, $action);
   echo $this->get($callbackId);
}

 

Now let’s simulate the process of retrieving product list using specific controller class

create this controller in the Controllers/ directory

app/Controllers/ProductController.php

<?php


namespace App\Controllers;


use App\Services\ProductService;

class ProductController
{
    private $productService;

    public function __construct(ProductService $productService)
    {
        $this->productService = $productService;
    }

    public function index()
    {
        return json_encode($this->productService->getProductList());
    }
}

And create the Services/ directory and add this service class:

app/Services/ProductService.php

<?php


namespace App\Services;


use App\Repository\MysqlProductRepository;

class ProductService
{
    private $productRepo;

    public function __construct(MysqlProductRepository $productRepo)
    {
        $this->productRepo = $productRepo;
    }

    public function getProductList()
    {
        return $this->productRepo->getProducts();
    }
}

Also create the Repository/ directory and add this repository class:

app/Repository/MysqlProductRepository.php

<?php


namespace App\Repository;


class MysqlProductRepository
{
    public function getProducts()
    {
        // for demonstration purposes i return the products as a php array
        return [
            [
                "id" => 1,
                "title" => "product 1",
                "price" => "$120"
            ],
            [
                "id" => 2,
                "title" => "product 2",
                "price" => "$300"
            ],
            [
                "id" => 3,
                "title" => "product 3",
                "price" => "$1000"
            ]
        ];
    }
}

Create bootstrap.php in the root directory of the project, this file acts as the bootstrap point of the application.

bootstrap.php

<?php
require_once __DIR__ . '/vendor/autoload.php';

$app = new \App\Application();

$app->setRoute("/products", [\App\Controllers\ProductController::class, "index"]);
$app->setRoute("/", function () {
   return "This is the landing page";
});

return $app;

As shown i initialized the Application class, then added two routes to test with. The first route “/products” displays the products list. Here i passed an array with two items, which is the controller and the method. The second route is the landing page “/” which is just a closure function.

Also create the index.php file in the root of the project

index.php

<?php

$app = require __DIR__ . '/bootstrap.php';

$app->run();

Don’t forget to run:

composer dump-autoload

In browser get http://localhost/<project folder>/?route=/products. You will see the product list as json. also if you changed the route to ?route=/ you should see the landing page.

Now the container is working.

 

Circular Dependency Problems

I will conclude this post by talking about important topic related to containers and dependency injection, though not many people aware about which is the circular dependency problem.

When working with frameworks like laravel or symfony with many classes, services and repositories, people can stuck in this error by mistake:

Fatal error: Allowed memory size of 536870912 bytes exhausted

Although this error has many reasons. But i talk about this error in context of dependency inject. Developers think that increasing memory_limit in php.ini can solve the problem but it’s not because in this case you are inside an infinite loop.

One reason for this error is injecting classes in each other constructor and vice versa, so the container tries to resolve the first class dependency. When resolving this dependency and finds that the second also needs a dependency of the first class and so on and stuck in an infinite loop which raises the above error.

To see this in action modify the MysqlProductRepository as shown:

<?php


namespace App\Repository;


use App\Services\ProductService;

class MysqlProductRepository
{
    private $productService;

    public function __construct(ProductService $productService)
    {
        $this->productService = $productService;
    }

    public function getProducts()
    {
        ///
        ....
        ....
    }
}

Here i added a constructor and injected Product Service class, now go to http://localhost/<project folder>/?route=/products and refresh, the error appears.

So to solve this problem make sure you follow those rules:

  • Don’t inject classes inside each other. If you have class A and class B, you can inject class A into B or B into A but not both.
  • The dependency tree must end with a class constructor that have no arguments, this marks the stop point in dependency injection recursive system, like in the above example the MysqlProductRepository constructor takes no argument.

 

5 2 votes
Article Rating

What's your reaction?

Excited
4
Happy
3
Not Sure
0
Confused
1

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments