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.
Table Of Contents
- 1. Preparing the environment
- 2. Introduction to Dependency Injection (DI)
- 3. A non-DI example
- 4. First approach to DI: manual injection
- 5. Second approach to DI: Injector class
- 5.1. Creating the injector
- 5.2. Calling the injector
- 6. PHP-DI
- 6.1. Installation
- 6.2. The Injection Container
- 6.3. Injecting with annotations
- 6.4. Injecting with PHP definitions
- 7. Chaining dependency injections: a more thorough and complete example
- 7.1. Directory structure
- 7.2. Storage dependency’s Language dependency
- 7.3. Injecting the Language dependency to Storage
- 7.4. Defining the injection
- 8. Summary
- 9. Download the source code
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
andDependencyB
specific implementations ofDependencyInterface
definitions. Each dependency will implement theoperation()
method according to its nature. - The
Injector
is the component which will instantiate the required dependency, to inject it into theClient
. TheClient
won’t know what is receiving; it will use the implementation decided by theInjector
.
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
andDB
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 theDB
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 receivedLanguageInterface
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.
You can download the full source code of this example here: PHPDependencyInjectionTutorial
This:
die(“Please, provide ‘$parameter’ parameter.”);
will throw you “PHP Notice: Undefined variable $parameter”.
Dear reader,
That statement is inside a “foreach” loop, and the “$parameter” is each value of the “$parameters” array, so, that notice won’t be thrown ;)
In any case, the error that could be thrown can be about the “$parameters” array, but only if the function is improperly called, not passing any parameter.
Regards,
Julen