PHP

PHP Dependency Injection Tutorial

Dependency Injection is a design pattern that should be followed in almost every software project, regardless of its size, and if the programming language with are working with allows us. Fortunately, PHP provides the tools to implement it.

This is a tutorial of how to deal with Dependency Injection in PHP, looking at the advantages that Dependency Injection supposes, and checking out different ways and tools to implement it.

For this tutorial, we will use:

  • Ubuntu (14.04) as Operating System.
  • Apache HTTP server (2.4.7).
  • PHP (5.5.9).
  • SQLite3, as an example of a dependency to inject.
  • Composer PHP dependency manager.

 

Tip
You may skip environment preparation and jump directly to the beginning of the tutorial below.

1. Preparing the environment

1.1. Installation

Below, commands to install Apache, PHP and SQLite are shown:

sudo apt-get update
sudo apt-get install apache2 php5 libapache2-mod-php5 php5-sqlite
sudo service apache2 restart

To install Composer, we need to download the PHP archive from its website, and move to it to binary files directory to execute it globally:

curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

1.2. PHP Configuration

In /etc/php5/apache2/php.ini file, we need to include the SQLite extension, in order to use it:

/etc/php5/apache2/php.ini

extension=sqlite.so

Don’t forget to restart Apache after doing any change.

2 Introduction to DI

2.1. What is Dependency Injection (DI)?

Dependency Injection is a design pattern where we supply a class (a client) the object instances it needs (the dependency), instead of instantiating itself. This pattern is based in a principle known as Dependency Inversion Principle (DIP). This principle says, basically, that high level modules should not depend directly on low level modules, where low level modules are those that implement basic operations; and high level modules, which use those modules for a specific purpose (what we call business logic).

The aim of DIP, conceptually, is to change from:

A needs to create, or use, B, in order to do C

To:

A needs something that allows doing C

What A wants is to do C, no matter how. The following diagram illustrates how Dependency Injection does this:

Let’s describe each component:

  • The class Client uses an interface, where some methods are described. This aggregation relation describes what we defined above as needs something that allows doing something. That interface allows to do an operation. The client doesn’t know how will be it done, but it can do it.
  • As said before, our interface DependencyInterface defines a set of methods. Definition means only method name, receiving parameters and returning type (this last actually not important in languages like PHP). This interface will be implemented by the dependencies that are going to be injected.
  • We have, in this case, DependencyA and DependencyB specific implementations of DependencyInterface definitions. Each dependency will implement the operation() method according to its nature.
  • The Injector is the component which will instantiate the required dependency, to inject it into the Client. The Client won’t know what is receiving; it will use the implementation decided by the Injector.

Remember that DI and DIP are not equivalent, but that DI follows the DIP.

2.2. How does DI help us?

  • Responsibility division: each dependency can be developed individually; the only restriction is the interface it must implement, which has to be agreed before. This allows to divide the work between developers, avoiding problems at integration time.
  • Maintainable code: does one of the features need changes or fixes? With DI, we target immediately the piece of code susceptible for changes, and we will be sure that the changes will have to be done only in that piece of code.
  • Reusable code: Dependency Injection makes us to design independent pieces of code (the dependencies), which can be reused as many times as we want, without repeating the code.
  • Testeable code: without DI, testing is much more complicated. Why? Because, without DI, if we testing a component that is using another compontent that is not being injected, it will actually use that component in the test. In most of the cases, this is not interesting, because we would be testing several times the same thing. With Dependency Injection, we can easily mock the dependencies to test better and faster.

To put all this in context, let’s see an example.

3. A non-DI example

Let’s consider the following scenario: we are going to develop a system that inserts and reads data.

The very first version, will work with a simple text file. So, we would have the following class to deal with the text file:

Textfile.php

<?php

/**
 * Methods for text file handling.
 */
class Textfile {

    const TEXTFILE = 'data.txt';

    /**
     * Writes the data into the textfile.
     *
     * @param $data The data to write into the textfile.
     */
    public function writeData($data) {
        file_put_contents(self::TEXTFILE, $data . '<br>', FILE_APPEND);
    }

    /**
     * Reads the data from the textfile.
     *
     * @return Textfile data.
     */
    public function readData() {
        $data = file_get_contents(self::TEXTFILE);

        return $data;
    }
}

And, then, a script to use this class:

Controller.php

<?php

require_once('Textfile.php');

define('ACTION_READ', 'read');
define('ACTION_WRITE', 'write');
define('WRITE_DATA', 'data');

/**
 * Checks if the given parameters are set. If one of the specified parameters is not set,
 * die() is called.
 *
 * @param $parameters The parameters to check.
 */
function checkGETParametersOrDie($parameters) {
    foreach ($parameters as $parameter) {
        isset($_GET[$parameter]) || die("Please, provide '$parameter' parameter.");
    }
}

// Flow starts here.

checkGETParametersOrDie(['action']);

$action = $_GET['action'];

$textfile = new Textfile();

switch ($action) {
    case ACTION_READ:
        $response = $textfile->readData();
        break;

    case ACTION_WRITE:
        checkGETParametersOrDie([WRITE_DATA]);
        $data = $_GET[WRITE_DATA];

        $textfile->writeData($data);
        $response = 'Data written.';
        break;

    default:
        $response = 'Please, provide a valid "' . WRITE_DATA . '" parameter.';
        break;
}

echo $response;

Nothing really special: we include the required Textfile class in line 3, we instantiate it in line 27, and we call its methods in lines 31 and 38, depending on what we are going to do.

As time goes by, we realize that a text file is no longer a suitable option because we need something more sophisticated; a relational database. So, we would develop another class to deal with the database:

DB.php

<?php

/**
 * Methods for database handling.
 */
class DB extends SQLite3 {

    const DATABASE = 'data.db';

    /**
     * DB class constructor. Initialize method is called, which will create data table if
     * it does not exist already.
     */
    public function __construct() {
        $this->open(self::DATABASE);
        $this->initialize();
    }

    /**
     * Craetes the table if it does not exist already.
     */
    protected function initialize() {
        $sql = 'CREATE TABLE IF NOT EXISTS data (
                    data STRING
                )';

        $this->exec($sql);
    }

    /**
     * Inserts the data into the databse.
     *
     * @param $data Data to insert.
     */
    public function insertData($data) {
        $sql = 'INSERT INTO data
                VALUES (:data)';

        $statement = $this->prepare($sql);
        $statement->bindValue(':data', $data);

        $statement->execute();

        $statement->close();
    }

    /**
     * Selects data from database.
     *
     * @return Database data.
     */
    public function selectData() {
        $sql = 'SELECT data
                FROM   data';

        $results = $this->query($sql);

        $data = '';

        while ($row = $results->fetchArray()) {
            $data .= $row['data'] . '<br>';
        }

        return $data;
    }
}

And now, we have to update our Controller.php file, in order to use our brand-new DB class:

Controller.php

<?php

require_once('DB.php');

define('ACTION_READ', 'read');
define('ACTION_WRITE', 'write');
define('WRITE_DATA', 'data');

/**
 * Checks if the given parameters are set. If one of the specified parameters is not set,
 * die() is called.
 *
 * @param $parameters The parameters to check.
 */
function checkGETParametersOrDie($parameters) {
    foreach ($parameters as $parameter) {
        isset($_GET[$parameter]) || die("Please, provide '$parameter' parameter.");
    }
}

// Flow starts here.
checkGETParametersOrDie(['action']);

$action = $_GET['action'];

$db = new DB();

switch ($action) {
    case ACTION_READ:
        $response = $db->selectData();
        break;

    case ACTION_WRITE:
        checkGETParametersOrDie([WRITE_DATA]);
        $data = $_GET[WRITE_DATA];

        $db->insertData($data);
        $response = 'Data written.';
        break;

    default:
        $response = 'Please, provide a valid "action" parameter.';
        break;
}

echo $response;

Let’s see carefully what has happened:

  • The file requirement and the instance have changed. Well, this is something that must be done somewhere.
  • Here is the important part: every object method call has changed. Apart from the object name itself, the methods have changed. Why? Because we have not forced to the Textfile and DB classes to share the same interface. And, in this case, we are lucky that the only thing that has changed is the name, and not the parameters, or the return type.
  • There is another thing to take into account: how shall we test Controller.php? Currently, is using the DB class. If we test it, we are using the real database. For example, the database could be that big that could make the tests quite slow.

This have been affordable changes, we just changed a few lines. But imagine this problem in a real project, with several dependencies and developers. This can be a real problem not only for maintaining, but at development also.

4. First approach to DI: manual injection

Let’s resolve this adding an abstraction layer between our components.

The first step must be to ask ourselves:

What do we want to offer?

In this case is pretty clear. We want to offer methods for reading and writing. It doesn’t matter how, from where, or any other implementation details. We only focus in what.

4.1. Defining the interface

For that, as in other Object Oriented Programming languages, we have the Interfaces. Object interfaces allow to specify which methods have to be implemented by a class, without defining how are going to be implemented.

So, lets define an interface for our read and write purposes:

StorageInterface.php

<?php

/**
 * Interface for method definitions for dealing with storage.
 */
interface StorageInterface {
    
    /**
     * Writes the given data into the storage.
     *
     * @param $data The data to write.
     */
    public function writeData($data);

    /**
     * Reads the data from the storage.
     *
     * @return The read data from storage.
     */
    public function readData();
}

So simple. We just define the methods inside an interface piece of code. Now, we will implement it in our classes.

4.2. Implementing the interface

Textfile.php

<?php

require_once('StorageInterface.php');

/**
 * Methods for text file handling.
 */
class Textfile implements StorageInterface {

    const TEXTFILE = 'data.txt';

    /**
     * Writes the data into the textfile.
     *
     * @param $data The data to write into the textfile.
     */
    public function writeData($data) {
        file_put_contents(self::TEXTFILE, $data . '<br>', FILE_APPEND);
    }

    /**
     * Reads the data from the textfile.
     *
     * @return Textfile data.
     */
    public function readData() {
        $data = file_get_contents(self::TEXTFILE);

        return $data;
    }
}

The interfaces are implemented with implements keyword. There are some things that we must know about interfaces:

  • A class that implements an interface, needs to implement every method defined in that interface. Otherwise, a PHP fatal error will be thrown.
  • The implemented methods must have the same name and parameters as the function defined in the interface.

Let’s do the same with DB class:

DB.php

<?php

require_once('StorageInterface.php');

/**
 * Methods for database handling.
 */
class DB extends SQLite3 implements StorageInterface{

    const DATABASE = 'data.db';

    /**
     * DB class constructor. Initialize method is called, which will create data table if
     * it does not exist already.
     */
    public function __construct() {
        $this->open(self::DATABASE);
        $this->initialize();
    }

    /**
     * Craetes the table if it does not exist already.
     */
    protected function initialize() {
        $sql = 'CREATE TABLE IF NOT EXISTS data (
                    data STRING
                )';

        $this->exec($sql);
    }

    /**
     * Inserts the data into the databse.
     *
     * @param $data Data to insert.
     */
    public function writeData($data) {
        $sql = 'INSERT INTO data
                VALUES (:data)';

        $statement = $this->prepare($sql);
        $statement->bindValue(':data', $data);

        $statement->execute();

        $statement->close();
    }

    /**
     * Selects data from database.
     *
     * @return Database data.
     */
    public function readData() {
        $sql = 'SELECT data
                FROM   data';

        $results = $this->query($sql);

        $data = '';

        while ($row = $results->fetchArray()) {
            $data .= $row['data'] . '<br>';
        }

        return $data;
    }
}

Note that we have changed the method names (lines 37 and 54) to coincide with those defined in the interface.

Nothing really exciting for the moment, right? Actually, some can think that we have done anything new.

4.3. Accessing the interface

Instead of accessing the implementations directly in Controller.php, as in the non-DI example, we are going to create a wrapper for it, where the dependencies will be injected:

StorageHandler.php

<?php

require_once('StorageInterface.php');

/**
 * StorageInterface wrap.
 */
class StorageHandler {

    protected $storage;

    /**
     * StorageHandler constructor, with an implementation of StorageInterface.
     *
     * @param $storage StorageInterface implementation.
     */
    public function __construct(StorageInterface $storage) {
        $this->storage = $storage;
    }

    /**
     * Writes the data using the storage implementation.
     *
     * @param $data Data to write.
     */
    public function writeData($data) {
        $this->storage->writeData($data);
    }

    /**
     * Reads the data using the storage implementation.
     *
     * @return Read data.
     */
    public function readData() {
        $data = $this->storage->readData();

        return $data;
    }
}

This is the class that deals with data reading and writing. But it only knows about an interface, which has methods that are not implemented. The key is that it will receive a concrete implementation in the constructor, and we save it as an attribute of the class (lines 17, 18). We even specify that it must receive an implementation of StorageInterface, even if in PHP is not necessary to declare types. But, in this cases, is a good practice to specify the interface we are expecting, to express that we will receive an unknown implementation of that interface.

So, when we call those methods (lines 27, 36), we will be calling received instance’s implemented methods. What they do is not important, the only thing the class has to know is that it will have methods named as defined in the interface.

We don’t have to even require those specific implementations classes, is enough to require only the interface (line 3).

We could even add the possibility to change that implementation on runtime!

StorageHandler.php

public function setStorage(StorageInterface $storage) {
    $this->storage = $storage;
}

Adding a setter, we could change the implementation StorageHandler class is using at any time.

4.4. Instantiating interface implementations

Last step is to control the StorageHandler:

Controller.php

<?php

require_once('StorageHandler.php');
require_once('Textfile.php');

define('ACTION_READ', 'read');
define('ACTION_WRITE', 'write');
define('WRITE_DATA', 'data');

/**
 * Checks if the given parameters are set. If one of the specified parameters is not set,
 * die() is called.
 *
 * @param $parameters The parameters to check.
 */
function checkGETParametersOrDie($parameters) {
    foreach ($parameters as $parameter) {
        isset($_GET[$parameter]) || die("Please, provide '$parameter' parameter.");
    }
}

// Flow starts here.
checkGETParametersOrDie(['action']);

$action = $_GET['action'];

$storage = new Textfile();
$handler = new StorageHandler($storage);

switch ($action) {
    case ACTION_READ:
        $response = $handler->readData();
        break;

    case ACTION_WRITE:
        checkGETParametersOrDie([WRITE_DATA]);
        $data = $_GET[WRITE_DATA];

        $handler->writeData($data);
        $response = 'Data written.';
        break;

    default:
        $response = 'Please, provide a valid "action" parameter.';
        break;
}

echo $response;

As in the first case in the previous approach without DI, we suppose that we are going to use the text file as storage. So, we require it (line 4), we instantiate it (line 27), and instantiate the StorageHandler class passing the Textfile implementation in the constructor (line 28). Or, in other words: we are injecting the Textfile dependency to StorageHandler.

Now, let’s say that we want to use the database. The changes would be:

Controller.php

//...

require_once('DB.php');

// ...

$storage = new DB();

And that’s it. The $handler object will be working without the need of suffering any changes. We just include the new class, and we change the instance of the variable passed to StorageHandler instantiation; the injected dependency, now, is DB.

Isn’t it amazing?

5. Second approach to DI: Injector class

In the first approach, we have seen how to perform a Dependency Injection, in such a disorganized way: we only have to change the instantiating class, yes, but we have to know exactly where is being instantiated. And this may not be easy in medium-large sized projects – in this case, we have only a dependency, but imagine finding each injection for tens of dependencies.

So, let’s create a completely isolated wrapper for the injection to StorageHandler – just as in the diagram we saw in section 1.1.

5.1. Creating the injector

As said, let’s define a class only for instantiating the dependency for StorageHandler:

StorageInjector.php

<?php

require_once('Textfile.php');
/**
 * Storage dependency injector.
 */
class StorageInjector {

    /**
     * Retrieves the specified instance of StorageInterface.
     *
     * @return StorageInterface implementation.
     */
    public static function getStorageInstance() {
        return new Textfile();
    }
}

So simple, just a function that retrieves the defined instance. We have defined as a static function, because there’s no real need to instantiate this class.

5.2. Calling the injector

Now, the only thing we have to do is to pass the returned object by StorageInjector::getStorageInstance() to StorageHandler constructor:

Controller.php

$handler = new StorageHandler(StorageInjector::getStorageInstance());

And the StorageHandler will use the whatever dependency injected by the StorageInjector, which will have the responsibility of deciding the dependency to inject.

So, now, if we have to inject the DB dependency, we exactly know where to change that injection:

StorageInjector.php

public static function getStorageInstance() {
    return new DB();
}

Including, of course, the file where DB is defined.

We say that we will exactly know the location of the injection because we are supposing that we are looking for a Injector suffixed file, or any other agreed standard for injector files.

6. PHP-DI

The PHP community is so great. It is constantly developing useful components. And Dependency Injection is not an exception: PHP-DI. In this chapter we will see how we can use it.

6.1. Installation

The installation is so simple, thanks to our beloved Composer. Just enter the following command in your working directory:

composer require php-di/php-di

And it will download the required stuff. Note that a composer.json and composer.lock, and a vendor directory have been created. Don’t touch them. The first two define the component we are using, and the vendor folder contains the PHP-CI code itself.

6.2. The Injection Container Container

PHP-CI uses a concept called container for the dependency injection, using a technique named autowiring, inspired by Java’s Spring framework. Let’s see how it works.

First, in the Controller.php file, we need to require a new file in order to use PHP-CI’s classes:

Controller.php

require_once('vendor/autoload.php');

// ...

Then, we will replace the following lines:

Controller.php

// ...

$storage = new Textfile();
$handler = new StorageHandler($storage);

// ...

With the following:

Controller.php

// ...
$container = DI\ContainerBuilder::buildDevContainer();
$handler = $container->get('Textfile');

// ...

What $container->get() does is, actually, create both Textfile and StorageHandler instances. The container uses the above mentioned autowiring technique to inject a Textfile object to the StorageHandler object.

This is actually not very impressive. We could say that even has complicated more the things.

But this is not the only way PHP-CI allows us to inject dependency, and is actually quite limited – it is not able to resolve dependencies that have parameters in the constructor. Let’s see more interesting ways.

6.3. Injecting with annotations

In order to use this feature, first, we need Doctrine Annotations library, so, we will install it via Composer:

composer require doctrine/annotations

Apart from that, we also have to make sure that, in php.ini, we have the following directive set to 1:

/etc/php5/apache2/php.ini

opcache.load_comments=1

Don’t forget to restart Apache after doing any changes.

To use the annotations, we have to construct the container in a different way. First, we need a ContainerBuilder instance, to configure it to use the annotations.

StorageHandler.php

// ...

$containerBuilder = new DI\ContainerBuilder();
$containerBuilder->useAnnotations(true);
$container = $containerBuilder->build();

// ...

Note that, in this case, we have constructed the container in a different way: first, creating the container builder, and then, creating the container using this container builder instance. This is necessary, because we need to configure the container builder to use the annotations.

Now, let’s inject our dependency using the annotation. We will begin injecting the Textfile dependency:

StorageHandler.php

<?php

require_once('StorageInterface.php');

/**
 * StorageInterface wrap.
 *
class StorageHandler {

    private $storage;

    /**
     * @Inject({"Textfile"})
     */
    public function __construct(StorageInterface $storage) {
        $this->storage = $storage;
    }

    // ...
}

As you can see in the line 10, we add an @Inject annotation in the PHP DocBlock, specifying the dependency we are injecting.

It would be possible to inject several dependencies:

StorageHandler.php

// ...

/**
 * @Inject({"Textfile", "WhateverDependency"})
 */
public function __construct(StorageInterface $storage, Dependency $anotherDependency) {
    // ...
}

There is the possibility also to specify the injection of each parameter, specifying the name:

StorageHandler.php

// ...

/**
 * @Inject({"storage" = "Textfile", "anotherDependency" = "WhateverDependency"})
 */
public function __construct(StorageInterface $storage, Dependency $anotherDependency) {
    // ...
}

This injection method is definitively better than previously seen autowiring: we can specify several dependencies; and the injection is made in an isolated way, in the class using the dependency. This allows us to have the injections in a tidier way, without the need of having Injector classes.

But we have to do another little change before ending. When getting the instance through the container, we must specify the class were the dependency is defined:

Controller.php

// ...

$handler = $container->get('Handler');

// ...

Instead of the dependency itself, as with autowiring.

Now, to change the injected dependency, we have make the change in the class the injection is made:

StorageHandler.php

// ...

/**
 * @Inject({"DB"})
 */
public function __construct(StorageInterface $storage) {
    $this->storage = $storage;
}

// ...

The injection is organized in a more organized way than before, and much more than manually, don’t you think so?

Unfortunately, the annotations have some limitations:

  • Only classes can be injected, not values.
  • As with autowiring, we can’t inject dependencies that are expecting parameters in the constructor.

6.4. Injecting with PHP definitions

PHP-DI still offers another way to do the injections: with PHP definitions.

Let’s define a file for the dependencies for our scenario:

dependencies.php

<?php

require_once('vendor/autoload.php');
require_once('Textfile.php');

return [
    'StorageInterface' => new Textfile()
];

We are defining our Textfile class as the dependency for 'StorageInterface' key in the array.

Now, we are going to inject the dependency:

Controller.php

// ...

$builder = new DI\ContainerBuilder();
$builder->addDefinitions('dependencies.php');
$container = $builder->build();

$handler = $container->get('StorageInterface');

// ...

And that’s it! The only thing we have to do is to specify the key of the array we defined in dependencies.php, to get its value.

If we want to inject the DB dependency, we just change the dependency file:

dependencies.php

<?php

require_once('vendor/autoload.php');
require_once('DB.php');

return [
    'StorageInterface' => new DB()
];

And the container, behind the scenes, will inject for us the DB dependency.

In comparison with other ways of injecting, this allows us to inject the dependency in a key-value binding, which may result a more tidier way to define the injections. And we could pass parameters in the constructor with any problem.

We could consider this injection way the most closer to a completely isolated injection solution, since we are actually defining a configuration file (yes, is PHP code, but with a configuration file format, quite close to properties files, for example), specifying the value of each dependency.

7. Chaining dependency injections: a more thorough and complete example

We have dealed with a quite naive example: just a dependency injection. This can be a good example for starting to understand the DI pattern, but we will probably have to deal with more than one dependency in the same project.

And it could happen that one of the dependency, requires another dependency. We will suppose that our StorageInterface needs an own dependency, and we will use the all the powerfulness of PHP-DI’s PHP definitions to deal with it.

Let’s imagine that the Storage classes will need a dependency to write, along with the received data, an informative message in a language. So, it will depend on a LanguageInterface dependency to write that message.

7.1. Directory structure

The project is getting bigger, and having all the sources in the same directory is not a good idea. We should organize the files in directories, taking into account the “dependency levels”.

├── storage_dependencies/
│   ├── language_dependencies/
│   │   ├── English.php
│   │   └── LanguageInterface.php
│   ├── DB.php
│   ├── StorageInterface.php
│   └── Textfile.php
├── StorageHandler.php
├── Controller.php
├── dependencies.php
├── composer.json
├── composer.lock
└── vendor/

Note: don’t remember to install PHP-DI with Composer, as explained in section 6.1.

7.2. Storage dependency’s Language dependency

Let’s create our interface for the language dependency of the Storage.

LanguageInterface.php

<?php

interface LanguageInterface {

    public function getInfoString();

}

We will create an implementation for this interface, for English:

English.php

<?php

require_once('LanguageInterface.php');

class English implements LanguageInterface {

    public function getInfoString() {
        return 'Writing data: ';
    }

}

For later testing, you could implement your own LanguageInterface with the language you prefer (or another version of the English interface, if you prefer).

7.3. Injecting the Language dependency to Storage

We must update our StorageInterface to inject our brand-new LanguageInterface dependency. This is how we are going to do it:

StorageInterface.php

<?php

require_once('language_dependencies/LanguageInterface.php');

interface StorageInterface {

    public function __construct(LanguageInterface $language);

    public function writeData($data);

    public function readData();
}

What we are doing is forcing every class implementing StorageInterface to have a constructor method reciving an instance of LanguageInterface. Of course, those classes will have to retrieve that parameter and save it as a class property:

// ...

protected $language;

public function __construct(LanguageInterface $language) {
    $this->language = $language;
}

// ...

In both Textfile and DB classes.

7.4. Defining the injection

Let’s create a file were the dependencies will be injected, using PHP-DI’s PHP definitions:

dependencies.php

<?php

require_once('vendor/autoload.php');
require_once('storage_dependencies/Textfile.php');
require_once('storage_dependencies/language_dependencies/English.php');

return [
    'LanguageInterface' => DI\Object('English'),

    'StorageInterface' => function (LanguageInterface $language) {
        return new Textfile($language);
    }
];

Let’s see it step by step:

  • We require the injecting dependencies, as we have been doing every time.
  • We start injecting from the inner dependency level. In other words, if a first dependency needs a second dependency, we start injecting that second one, and so on. In this case, we map the English implementation to the LanguageInterface dependency required by other class.
  • Now, we need to inject the outer dependency, wich needs the previous defined dependency. How? With a closure, that receives the injected dependency for LanguageInterface, and then returns the instance for the injecting dependency, constructing it with the received LanguageInterface implementation.

And that’s it! To instantiate the class receiving the StorageInterface dependency, we just have to do as in 6.4 section:

Controller.php

// ...

$builder = new DI\ContainerBuilder();
$builder->addDefinitions('dependencies.php');
$container = $builder->build();

$handler = $container->get('StorageInterface');

// ...

And we will have a StorageHandler instance, which will have a Textfile instance, which will have, in the same way, an English instance, as defined in dependencies.php.

To change any of the injections, in any link of the chain, we have just to make the change in dependencies.php. A file that will be perfectly accessible (it’s the only file were the injections are defined, and the injection definition is the only purpose of the file), and that does not have any impact in the actual application logic, minimazing the possibilities of introducing bugs (well, if we declare the array incorrectly, we will get an error, inevitably).

You may thought that the injections can be made in the following way:

dependencies.php

<?php

require_once('vendor/autoload.php');
require_once('storage_dependencies/DB.php');
require_once('storage_dependencies/language_dependencies/English.php');

return [
    'StorageInterface' => new Textfile(new English());
];

Which is okay and would work, but that it can be not as clear as the other way, specially if a considerable number of dependencies have to be injected.

8. Summary

In this tutorial, we have seen why Dependency Injection is so important, and how it can help us to build maintainable and testeable software. For that, we have first created an example without using it, before using the DI, to recognize the real usefulness of this pattern.

We have used several methods to achieve the Dependency Injections. Deciding which can be the most suitable for a project, is a decision that the developers have to take. But the most important thing is to, regardless which method is used, to follow that agreement in every part of the project, without mixing different methods of injection.

9. Download the source code

This was a tutorial of Dependency Injection in PHP.

Download
You can download the full source code of this example here: PHPDependencyInjectionTutorial
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Reader
Reader
7 years ago

This:

die(“Please, provide ‘$parameter’ parameter.”);

will throw you “PHP Notice: Undefined variable $parameter”.

Back to top button