Click here to Skip to main content
15,884,746 members
Articles / Programming Languages / PHP

A Simple Dependency Injection Container

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
12 Apr 2014CPOL6 min read 10.7K   82   6  
A lightweight dependency injection container for PHP
This article explains Dependency Injection pattern and presents a simple DI Container class that can be used in PHP projects.

Introduction

The Dependency Injection pattern has been around for a while and is widely used in many applications. In this article, I explain what the Dependency Injection pattern is and present a simple Dependency Injection Container class that you can use in your PHP projects.

Background

The Dependency Injection pattern is actually a very simple pattern and with the aid of some code samples, you can begin to understand what the pattern hopes to achieve. Below is a sample PHP code fragment.

PHP
class Mailer {
    private $transport;
    private $emailTemplate;
    
    public function __construct(){
        $this->transport = new Smtp('host', 'port', 'user', 'pass');
        $this->emailTemplate = file_get_contents('template.html');
    }
    
    public function send($from, $to, $subject){
        $this->transport->send($from, $to, $subject, $this->emailTemplate);
    }
}

$mailer = new Mailer();
$mailer->send('Syed Hussain', 'john@domain-xyz.com', 'Marketing Email');

The sample code above describes a simple Mailer class that loads an HTML template and sends it to an email address. Think of it as a newsletter.

This type of code is quite common as it wraps implementation into one single class, which to some extent can be reused. If you intend to distribute this class to your colleagues however, you will need to settle on the fact that the Mailer class can only send emails using Smtp because it has been hard coded into the class. If your colleagues requirement was to use a third party API such as Mandrill or Mailgun to send emails, then the Mailer class would not meet their requirement as it uses Smtp. They can replace the Smtp code to use Mandrill's web API. But what if they then decided to change from Mandrill to Mailgun or any other provider that can send emails? This tightly coupled implementation is not flexible enough to allow the usage of different types of transport mechanisms.

The solution to this problem is actually quite simple. Instead of hard coding the Smtp class, you simply supply it to the Mailer class as configuration data. This is done by creating an instance of the Smtp class and injecting it into the Mailer class either by the Mailers constructor or by a setter method. The code below shows a revised version of the Mailer class, which accepts an Smtp object instance in the class constructor.

PHP
class Mailer {
    private $transport;
    
    private $emailTemplate;
    
    public function __construct($transport){
        $this->transport = $transport;
        $this->emailTemplate = file_get_contents('template.html');
    }
    
    public function send($from, $to, $subject){
        $this->transport->send($from, $to, $subject, $this->emailTemplate);
    }
}

class Smtp {
    public function send($from, $to, $subject, $message){
        // Send the message using smtp.
    }
}

$mailer = new Mailer(new Smtp());
$mailer->send('Syed Hussain', 'john@domain.com', 'Marketing Email');

This simple change in the code has now made the Mailer class more flexible. It can accept any object and attempt to call its send() method. I say attempt because the class does not check if the supplied object has a send() method, but this can easily be corrected by using an interface.

The point here is that the Mailer class is being supplied with its dependency. The Mailer class cannot function without a transport mechanism, rendering the Mailer useless.

At the simplest level, Dependency Injection is the ability to provide configuration data to a client (the Mailer class) that is dependent on other services (the Smpt class).

You have probably already at some point used the Dependency Injection pattern either knowingly or unknowingly. Dependency Injection is also referred to as Inversion of Control (IoC). This name comes from the fact that control from the client has been inverted, meaning the client is no longer responsible for its dependencies and the control has been giving to another entity.

If you remove dependencies from a client, that means the dependencies must be created in another scope. The term scope used here is "somewhere else" and it's possible that those dependencies may have dependencies of themselves. In any case, all dependencies must exist before a dependent (the client) can use them. This has the side effect of bloating your code and introducing errors. For example, you might know which dependencies a class requires but your colleague might not and might use a different dependency which may result in errors. This is where Dependency Injection Containers (DIC) are primarily used. A DIC is an implementation and can be implemented in any programming language.

Dependency Injection Container

A DIC can be considered to be a registry, where dependencies are registered for usage. Each dependency is given a name, this can be a class name or a unique name that identifies the dependency. Additionally, each dependency can be configured with parameters that the dependency needs. For example, the parameter for the Smtp dependency might be the host, port, username and password as it needs these details to connect to an Smtp server. With all dependencies registered into a container, the container is then responsible for returning instances of these dependencies using a get() method. This approach centralizes all dependencies into a single container. You can have more than one container but the end goal is to obtain pre-configured dependencies when you need them.

Storing instantiated dependencies into a DIC has one major performance issue. Consider this scenario. You create an instance of the Smtp class and store the object into the DIC. However, not all parts of your code will use the Smtp object, which means the object has been instantiated but not used. This will cause performance issues when many dependencies have been registered but not all are used in the context of the executing code. For this reason, DICs do not store instantiated objects, rather a DIC contains a reference to the dependency class name. When a dependency is required, it is dynamically instantiated at that moment (on demand dependency), which means you only load instances when you need them. No dependencies are loaded if they are not required.

Simple Dependency Injection Container

In this section, I will explain the usage of the attached sample project with some code samples. The following code creates an instance of the DiContainer and registers a dependency.

PHP
$DiContainer->register('db', 'PDO')
    ->addArgument('mysql:host=localhost;dbname=db')
    ->addArgument('username')
    ->addArgument('password');

The register method registers a dependency and takes two arguments. The first argument is a unique name that has been given to the dependency. This is the name used, when retrieving the dependency. The second argument is the class name of the dependency. The code above has registered a dependency 'db' with the class name 'PDO'.

The addArgument() method is used to add arguments that the PDO class constructor requires. Notice that the code does not store any instantiated objects, just a reference to the class name that would be dynamically instantiated.

With the dependency registered, it can now be accessed in three different ways, which are shown below:

PHP
$db = $DiContainer->getInstance('db');

$db = $DiContainer->getSingleInstance('db');

$db = $DiContainer->db;

Calling the getInstance() method with the dependency name, will return a new instance of the dependency. Calling the getSingleInstance() method will only return once instance. The DiContainer will internally maintain this instance and will return it on subsequent calls to the same dependency using the getSingleInstance() method. The last method allows you do use the dependency name as a property. Internally, the DiContainer will call the getInstance() method.

In some situations, you might want to use a registered dependency as the argument for another dependency that is being registered. The code sample below shows how to solve this problem.

PHP
class Mailer {
    private $transport;
    public function __construct($transport){
        $this->transport = $transport;
    }
}

class Smtp{
    public function __construct($host, $port, $user, $pass){
        // Process arguments
    }
}

$DiContainer = new DiContainer();

$DiContainer->register('smtp', 'Smtp')
    ->addArgument('host')
    ->addArgument('port')
    ->addArgument('user')
    ->addArgument('pass');
    
$DiContainer->register('mailer', 'Mailer')
    ->addArgument('@@smtp');
    
$mailer = $DiContainer->getInstance('mailer');

Notice that the dependency smtp has been registered and given the name smtp. The Mailer class requires an smtp instance passed to its constructor. When registering the Mailer dependency the double @@ followed by the dependency name is used to reference a previously registered dependency.

DICs are implemented differently and each programming language or vendor has its own syntax. Some DICs go beyond passing arguments to the class constructor and can even call class methods with arguments.

PHP's simple reflection capabilities make it very easy for anyone to develop their own DIC library. The code in this sample project can be easily extended to register dependencies from a configuration file.

This brings me to the end of this article. Please feel free to leave your comments and suggestions.

History

  • 12th April, 2014: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --