Posted by Thiago Marini on May 06, 2015

Dependency Injection tutorial

A lot has been written about dependency injection or DI (for short) in PHP, but it’s a bit difficult to find a tutorial showing how DI actually works. The objective of this post is to create a DI container and show the internals and concepts of dependency injection.

So, what is DI?

A lot of people — usually developers who come across a DI when using PHP frameworks — might be led to think that DI is all about instantiating an object without worrying about other objects that this object depends upon.
Sure it’s a cool feature if seen this way, but some interesting concepts exist behind the scenes.

DI is a Design Pattern commonly used to implement the Inversion of Control Principle in software development.

DI should not be confused with Service Locator or autoloading as they care about locating classes in an application. In this tutorial you’ll see that we will use PHP autoload feature to make our DI container work. So, they’re not the same thing.

The fundamental difference between them is that in DI you pass a service to a dependent object as an abstraction, which gives you a few good things:

  • Modularity: instead of statically coupling your classes, you’ll be pointed in the direction of creating self contained modules/services that can evolve independently.
  • Extensibility: if you check the given service as an interface in the dependent object, you can replace it for other services in the future without breaking anything. This can be a life saver as the system ages.

Wwhhhhaaaaattt???? I don’t know about these things :(

No problem, let’s see it in code. We are developers, aren’t we?

Imagine that a driver needs a car to drive.

class Car {
    public function run()
    {
        echo 'Vroooaaammmm!';
    }
}

class Driver
{
    private $car;
    public function __construct()
    {
        $this->car = new Car();
    }
    public function drive()
    {
        $this->car->run();
    }
}

It shows that Driver has a Car dependency. It’s kind of fine in this example but you could end up including tons of files if the application grows, which could be a bit hard to maintain. It’s also a bit clumsy to unit test Car as it just echos some string.

It could be improved if we did the following:

class Car {
    public function run()
    {
        echo 'Vroooaaammmm!';
    }
}

class Driver
{
    private $car;
    public function __construct(Car $car)
    {
        $this->car = $car;
    }
    public function drive()
    {
        $this->car->run();
    }
}

Now instead of instantiating Car we are receiving it ready to go on the constructor, it’s a small improvement as we don’t need to worry about its instantiation (it might have dependencies too). It’s also a bit easier to unit test, we can create a spy to check that its method run was called for example.

But it doesn’t bring us the promised benefits of modularity and extensibility as we’re still relying on Car to make Driver work. Let’s change it a bit more.

interface Car
{
    public function run();
}

class Ferrari implements Car {
    public function run()
    {
        echo 'Vroooaaammmm!';
    }
}

class Driver
{
    private $car;
    public function __construct(Car $car)
    {
        $this->car = $car;
    }
    public function drive()
    {
        $this->car->run();
    }
}

Something really cool happened here, we are not relying on Car as an object anymore, we now rely on its behavior. Ferrari can be replaced by any other class as soon they implement the Car interface. This is abstraction in practice.

And now our application looks a bit like real life, in real life a driver can drive any car (as soon as they have a driving license). Also they can change their car as they see fit, being able to change your car is a good thing, right?

In our application we should be able to give whichever Car we want to the Driver, this is exactly the kind of problem DI can be used to solve.

Let’s write our DI container

Step 1, the name:

First things first, let’s choose a name. We need to inject objects into objects, what do we use in real life for injecting things?

A syringe, of course! The first step is done, our project has a name: Syringe.

Step 2, the ingredients:

Strategy, to write our DI container we’ll need:

  • A file format where we can write down our dependencies, Json will do.
  • A repository to keep our instantiated services.
  • A factory to instantiate our services.
  • A class representing the container.

Next I’ll cover only the essential features of each ingredient of our DI container recipe, this way you’ll get a sense of why I’m using them. The full implementation of Syringe is on Github, where you can clone the repository and play with each element as you please.

File format

{
    "services": [
        {
            "id": "class-a",
            "class": "DummyServices\\ClassA"
        },
        {
            "id": "class-b",
            "class": "DummyServices\\ClassB",
            "arguments": [
                {
                    "id": "class-a"
                }
            ]
        }
    ]
}

In the structure above, we’ll call the classes we need to instantiate services. Each service should have an id and and the complete class name with namespace so the PHP autoload feature can find them.

Services can also have other services as arguments, in this example class B receives class A as an argument, like we had in our Car/Driver case.

Repository Class

In its most basic form a repository is a place where you can add and get your objects, like a database:

class Repository
{

	private $objects = [];

	public function add($id, $obj)
	{
		$this->objects[$id] = $obj;
	}

	public function get($id)
	{
		return $this->objects[$id] ;
	}
   
}

Factory Class

The factory class will be in charge of instantiating our objects, we will receive the json file, read it and use the id and class parameters in the json data to find and instantiate the services. We will use the PHP reflection class to do the job.

class Factory
{

    public function __construct()
    {
		$this->serviceList; // TODO: load the data from json file and put it in a variable
    }
    
    public function create($id)
    {
		// use the id to get the service from the list
       $class     = $this->serviceList[$id]['class'];
       $reflector = new \ReflectionClass($class);

       return $reflector->newInstance();
    }
}

Container Class

In the container class we’ll use the repository to store our instantiated services and the factory to create the services that have not been instantiated.

class Container
{
    private $repository;

    private $factory;

    public function __construct(FactoryInterface $f, RepositoryInterface $r)
    {
        $this->factory    = $f;
        $this->repository = $r;
    }

    public function get($id)
    {
        $service = $this->repository->get($id);
		// if we don't have the service we need to create it using the factory
        if (is_null($service)) {
            $service = $this->factory->create($id);
            // add the newly created service to the repository class
            $this->repository->add($id, $service);
        }
        return $service;
    }
}

Did you notice that we’ve just used the abstraction technique we saw earlier in the constructor? This way we can change our factory and repository whenever we want without breaking the container!

Step 3, some gotchas and the final result:

The final result of this tutorial is in a repository on Github: https://github.com/thiagomarini/syringe.

Clone it and mess around as you please.

Some gotchas found on the way:

  • To guarantee only one instance of each service we pass the container to the factory so when loading arguments the factory can check if the service exist in the container beforehand. Kind of forcing a singletonwish behavior in the container.
  • There is also the circular dependency problem that happens for example when a service A depends on service B and B depends on service A. If this happens an Exception will be thrown.

Jobs at MyBuilder

We need an experienced software engineer who loves their craft and wants to share their hard-earned knowledge.

View vacancies
comments powered by Disqus