12 minutes to read, 6.02K views since 2016.10.12

Implementation of dependency injection in php

Introduction

By the time we start speak about dependency injection we must to deal with services and service container.

Perhaps, you've already wrote a thouthands lines of obejct orientated code and even got your first money from coding on php. You've also noticed that changing parts of your code is a terrible task and even substitution of some little part of the code breaks everything.

That's because you wrote the bad code, where the all components (if they exists, rarely the whole application is hardcoded there is nothing to do except complete rewriting) are highly coupled together and depends on their implementation.

There is much ways to avoid such situations. They are the interface design and component orientated architecture. What are the component architecture and what's the hell is design based on intefaces ? - that's it, dude, its where the magic begins.

If you write your code by using interfaces it would be much more easier to change the behaviour of it. Lets watch at example, suggest we have following interfaces:

interface BackendInterface {
  public function store($key,$data); // this method puts data to storage
  public function get($key); // this method gets data from storage
}

interface FrontendInterface {
  public function decode($data); // this method is used when you're getting  your data from storage
  public function encode($data); // this method used when you're going to put some data to storage
}

And a class which uses them:

class Cache {

  /**
  * 
  * @var \Framework\Cache\BackendInterface
  */
  private $cacheBackend;

  /**
  *
  * @var \Framework\Cache\FrontendInterface
  */
  private $cacheFrontend;

  public function save($key,$data) {
    $actualDataToStore = $this->cacheFrontend->encode($data);
    $this->cacheBackend->store($key,$actualDataToStore);
  }

  public function get($key) {
    return $this->cacheFrontend->decode($this->cacheBackend->get($key));
  }

  public function __construct(\Framework\Cache\FrontendInterface $f,\Framework\Cache\BackendInterface $b) {
    $this->backendCache = $b;
    $this->frontendCache = $f;
  }
}

This is the example of fully flexible caching system. You can easily provide few implementations of backend cache (this is where data would be stored): MysqlBackend or RedisBackend or even FileBackend and use with corresponding frontend cache: SerializeFrontend which would use serialize on saving to cache and unserialize on getting from it. Or you can implement RawFrontend which would do nothing in decode and encode which would be great for texts or primitive types.

So, somewhere in the initialization of your application you'd declare what is the actual Cache class is using of:

$cache = new \Application\Cache(new SerializeFrontend(),new MemcachedBackend()); 

or

$cache = new \Application\Cache(new SerializeFrontend(),new RedisBackend()); 

And then use it where apropriate in rest of your application:

public function postAction($id){

  $user = $cache->get('post_view_chart_'.$id);
  // ... do somethig with a stats
}

Now, when you want to change you cache being in Memcache to Mysql you're just need to change one line of code when you initialized the cache class and everything would be ok, because postAction knows nothing about how the Cache is implemented, but he knows Cache could do the required actions.

Service container

Why you haven't asked me what is the magical place where you initialize your app? In the example above i've created a global variable somewhere in php and then use it inside controller's action. I've heard that i would be sent into the hell for this and it sounds like truth.

Perfectly, we should have some object in the system that would be the mediator between us and other services. This object would be called service container, because it would contain all services in our system. If wee would need some service, like Cache above, we would ask ServiceContainer to give it to us. So, instead of using a global variable we would use a service container:

class SomeController {

  private $services = ServiceContainer::i();

  public function indexAction() {

    $cache = $this->services->get('cache');
    // do something with cache
  }
}

Simply saying, service container is an array of all services (db,input sanitizier,caching manager,image cropper,...) that can be used in our system. It would be initialized with all services on application start and can be acessible from everywhere by singletone. Lests implement it by ourself. At first we need to have an allocator for all our services and two methods: to get and set services to container

class ServiceContainer {

  // singletone stuff to access container from everywhere
  private static $instance = null; 

  /**
   * @return ServiceContainer
   */
  public static function i(){
      if (self::$instance === null) {
          self::$instance = new self();
      }
      return self::$instance;
  }

  /**
  * 
  * @var ServiceInstance[]
  */
  private $allocator = []; 

  public function put($name,$service) {
    $this->allocator[$name] = $service;
  }
  public function get($name) {
    if (!isset($this->allocator[$name])) {
      throw new \Exception("No such object in `".$name."` service container");
    }
    return isset($this->allocator,$name)?$this->allocator[$name]:null;
  }
}

We can just use this implementation but its quite not optimized: we store all instances of all services in $allocator even if we don't use them. With help of anonymous functions we will make the lazy loading of services, so only the services we need will eat memory and cpu time.

Lazy loading of services

Before implementing lazy loading lets have a look at the ServiceInstance which would be used as handler for service lazy loader and actually instance:

class ServiceInstance {

  private $instance = null;
  private $name;
  private $loader;

  public function __construct($name,$instance){

    $this->name = $name;

    if ($instance instanceof \Closure) {
      $this->loader = $instance;
    } else {
      $this->instance = $instance;
    }
  }

  public function getService() {
      if ($this->instance === null) {
          $loaderClosure = $this->loader;
          $this->instance = $loaderClosure();
      }
      return $this->instance;
  }

}

i've used objects instead of arrays because they're a bit quicker and i was able to incapsulate methods within class itself. All lazy stuff now in ServiceInstance, so we only have to instantiate ServiceInstance objects in put, and use getService method in get:

public function put($name,$service) {
  $this->allocator[$name] = new ServiceInstance($name,$service);
}

public function get($name) {
  if (!isset($this->allocator[$name])) {
    throw new \Exception('No such item '.$name.' in service container');
  }
  return $this->allocator[$name]->getService();
}

Voila! now we have a lazy service container and can use it everywhere wee need! And we will use it in dependency injection!

Injection

What does it mean injection of dependencies? When we have some part of code and it depends (use) some external class, library or something like this we should get it (probably from service container) by actually declaring this, or, by creating instance of concrete object which is bad, because it breaks all the component-based philosophy.

So whats wrong with using service container to get all we need ? The answer is nothing. But we can do it automatically, without writing a line of code. For example we have some controller's action in MVC application which are located within /post/read/ID:

public function readAction($id){

  $accessManager = $this->services->get('accesses');
  $user = $this->services->get('authorizedUser');

  $post = Post::findFirst($id);

  if ($accessManager->haveRights($post,$user)) {
    if (!$post) {
      $this->services->get('logger')->error('can\'t find post with id '.$id);
    }
  } else {
    // you don't have a permission to view this article
  }

}

We used at this action three services: accesses,authorizedUser and logger, moreover two of them are used on every request because they're created on action start. To clean up code, we can use dependency injection:

class PostController {

    public function readAction(AccessManager $accessManager, User $user, $id) {

        $post = Post::findFirst($id);

        if ($accessManager->haveRights($post, $user)) {
            if (!$post) {
                $this->services->get('logger')->error('can\'t find post with id ' . $id);
            } else {
                echo "user, you can read the post! ";
            }
        } else {
            exit('you don\'t have permission to view this article');
        }
    }

}

Yes, we added our services into required parameters to controller's action. As executing of actions in MVC is handled by the framework's system, we can inject apropriate objects to parameters by using Reflection api :

How injection is implemented with Reflection

$object = new Catchmetech\DiLesson\Controllers\PostController();
$class = new \ReflectionClass($object);

$methodName = 'readAction'; // we got it from parsing an uri or something like that
$method = $class->getMethod($methodName);

$parametersFromUri = [5]; # /post/read/5

try {
    $paramsToInject = [];

    foreach ($method->getParameters() as $param) { // get all parameters to our Action and check out if we have suitable service for it
        // we should throw exception if its not an object param and we don't have suitable param in params got from uri and services too
        if ($param->getClass()) { // we are going to inject only complex services, not scalar types
            $classToGet = $param->getClass()->getName();
            $service = $services->findClass($classToGet);

            if ($service) {
                $paramsToInject[] = $service;
            } else {
                throw new \Exception('Can\'t find a proper class to inject when trying to get `'.$classToGet.'` instance');
            }
        }
    }

    // we need to pass extra arguments if we have them
    if (!empty($parametersFromUri)) {
        foreach ($parametersFromUri as $param) {
            $paramsToInject[] = $param;
        }
    }

    $method->invokeArgs($object, $paramsToInject);
} catch (\Exception $e) {
    echo $e->getMessage();
}

And this is the initialization of our service container used by this part of code:

<?php

use Catchmetech\Core\ServiceContainer;
use Catchmetech\DiLesson\Managers\AccessManager;
use Catchmetech\DiLesson\Models\User;

/* @var $services ServiceContainer */
$services = new ServiceContainer();

$services->put('accesManager', function() {
    return new AccessManager();
}, AccessManager::class);

$services->put('authorizedUser', function() {
    return new User();
}, User::class);

As you noticed, we are adding services by alias but now we searching by class name. So we need to add extra mapping from classname to alias to get possibility to find services by classname. Lets modify the ServiceContainer::put method:

public function put($name, $service, $class = '') { // we need to get the classname in case if we initialize it with closure
    if ($service instanceof \Closure) {
        $this->addMapping($class, $name);
    } else {
        if (is_object($service)) {
            $class = get_class($service);
            $this->addMapping($class, $name);
        }
    }
    $this->allocator[$name] = new ServiceInstance($name, $service);
}

// add next changes to ServiceContainer:
private $mapping = [];

/**
*
* Adds a mapping class -> service alias to give ability to search over services by a class name 
*/
private function addMapping($class,$alias) {
  if (!isset($this->mapping[$class])) {
    $this->mapping[$class] = [];
  }
  $this->mapping[$class][$alias] = true;
}

/**
*
* Returns a service by a classname
*/
public function findClass($classname){
  if (isset($this->mapping[$classname])) {
    if (count($this->mapping[$classname]) > 1) {
      throw new \Exception('The service for class '.$classname.' is ambigious, please review your code');
    } else {
      return $this->get(array_keys($this->mapping[$classname])[0]);
    }
  }
}

You can download the last version of dependency injection code in the left bar of this page and try it by yourself.

Going ahead

I intentionally not implemented injection of interfaces (parameter of the method is of type of some interface) because it would be the good hometask. Also, you can try to implement injection to constructors.

Read next article How to create an ORM framework in pure php. ORM Creation Tutorial in course Reflection in PHP