IoC Container

Table of Contents

  1. Introduction
    1. Explanation of Dependency Injection
    2. Dependency Injection Container
  2. Basic Usage
  3. Binding Instances
  4. Binding Singletons
  5. Binding Prototypes
  6. Binding Factories
  7. Targeting
  8. Bootstrappers
  9. Calling Methods
  10. Calling Closures
  11. Checking a Binding
  12. Removing a Binding
  13. Applications in Opulence

Introduction

Explanation of Dependency Injection

Dependency Injection refers to the practice of passing a class its dependencies instead of the class creating them on its own. This is very useful for creating loosely-coupled, testable code. Let's take a look at an example that doesn't use dependency injection:

class Foo
{
    private $database;

    public function __construct()
    {
        $this->database = new Database();
    }

    public function insertIntoDatabase(string $query) : bool
    {
        return $this->database->insert($query);
    }
}

Databases are complex, and unit testing them is very tricky. To make unit testing simpler, we could mock the database class so that we don't ever actually query a real database:

class DatabaseMock extends Database
{
    public function insert(string $query) : bool
    {
        return true;
    }
}

The issue with Foo is that it creates its own instance of Database, so there's no way to pass it DatabaseMock without having to rewrite the class just for the test. The solution is to "inject" the Database dependency into Foo:

class Foo
{
    private $database;

    public function __construct(Database $database)
    {
        $this->database = $database;
    }

    public function insertIntoDatabase($query) : bool
    {
        return $this->database->insert($query);
    }
}

The difference is subtle, but now we can easily inject DatabaseMock when writing unit tests:

$database = new DatabaseMock();
$foo = new Foo($database);
echo $foo->insertIntoDatabase('bar'); // 1

By inverting the control of dependencies (meaning classes no longer maintain their own dependencies), we've made our code easier to test.

Dependency Injection Container

Hopefully, you can see that injecting dependencies is a simple, yet powerful feature. Now the question is "Where should I inject the dependencies from?" The answer is a dependency injection container (we'll call it a container from here on out). A container can take a look at a constructor/setter methods and determine what dependencies a class relies on. It creates a collection of various dependencies and automatically injects them into classes. One of the coolest features of containers is the ability to bind a concrete class to an interface or abstract class. In other words, it'll inject the concrete class implementation whenever there's a dependency on its interface or base class. This frees you to "code to an interface, not an implementation". At runtime, you can bind classes to interfaces, and execute your code.

Basic Usage

The container looks at type hints in methods to determine the type of dependency a class relies on. The container even lets you specify values for primitive types, eg strings and numbers.

Note: Classes that accept only concrete classes in their constructors do not need to be bound to the container; they can be instantiated automatically. A class should only be bound to the container if it depends on an interface, abstract class, or primitive.

Let's take a look at a class A that has a dependency on IFoo:

interface IFoo
{
    public function sayHi();
}

class ConcreteFoo implements IFoo
{
    public function sayHi()
    {
        echo 'Hi';
    }
}

class A
{
    private $foo;

    public function __construct(IFoo $foo)
    {
        $this->foo = $foo;
    }

    public function getFoo() : IFoo
    {
        return $this->foo;
    }
}

If we always want to pass in an instance of ConcreteFoo when there's a dependency on IFoo, we can bind the two:

use Opulence\Ioc\Container;

$container = new Container();
$container->bindSingleton('IFoo', 'ConcreteFoo');

Now, whenever a dependency on IFoo is detected, the container will inject an instance of ConcreteFoo. To create an instance of A with its dependencies set, simply:

$a = $container->resolve('A');
$a->getFoo()->sayHi(); // "Hi"

As you can see, the container automatically injected an instance of ConcreteFoo. You can also bind a value to multiple interfaces with a single call:

$concreteFoo = new ConcreteFoo();
// $concreteFoo will be bound to both "IFoo" and "ConcreteFoo"
$container->bindInstance(['IFoo', 'ConcreteFoo'], $concreteFoo);

Binding Instances

Binding a specific instance to an interface is also possible through the bindInstance() method. Every time you resolve the interface, this instance will be returned.

$concreteInstance = new ConcreteFoo();
$container->bindInstance('IFoo', $concreteInstance);
echo $concreteInstance === $container->resolve('IFoo'); // 1

Binding Singletons

You can bind an interface to a class name and have it always resolve to the same instance of the class (also known as a singleton).

$container->bindSingleton('IFoo', 'ConcreteFoo');
echo get_class($container->resolve('IFoo')); // "ConcreteFoo"
echo $container->resolve('IFoo') === $container->resolve('IFoo'); // 1

If your concrete class requires any primitive values, pass them in an array in the same order they appear in the constructor.

Binding Prototypes

You can bind an interface to a class name and have it always resolve to a new instance of the class (also known as a prototype).

$container->bindPrototype('IFoo', 'ConcreteFoo');
echo get_class($container->resolve('IFoo')); // "ConcreteFoo"
echo $container->resolve('IFoo') === $container->resolve('IFoo'); // 0

If your concrete class requires any primitive values, pass them in an array in the same order they appear in the constructor.

Binding Factories

You can bind any callable to act as a factory to resolve an interface. Factories are only evaluated when they're needed.

$container->bindFactory('IFoo', function () {
    return new ConcreteFoo();
});
echo get_class($container->resolve('IFoo')); // "ConcreteFoo"

Note: Factories must be parameterless.

By default, resolving interfaces that were bound with a factory will return a new instance each time you call resolve(). If you'd like the instance created by the factory to be bound as a singleton, specify true as the last parameter:

$container->bindFactory('IFoo', function () {
    return new ConcreteFoo();
}, true);
echo $container->resolve('IFoo') === $container->resolve('IFoo'); // 1

Targeting

By default, bindings are registered so that they can be used by all classes. If you'd like to bind a concrete class to an interface or abstract class for only a specific class, you can create a targeted binding using for(TARGET_CLASS_NAME) before your binding method:

$container->for('A', function ($container) {
    $container->bindSingleton('IFoo', 'ConcreteFoo');
});

Now, ConcreteFoo is only bound to IFoo for the target class A.

Note: Targeted bindings take precedence over universal bindings.

Targeting works for the following methods:

Bootstrappers

Sometimes, you'll find yourself needing to bind several components of your module to your IoC container. To keep yourself from writing repetitive code to do these bindings, you can use bootstrappers. They're perfect for plugging-and-playing whole modules into your application.

To learn more about them, read their docs.

Calling Methods

It's possible to call methods on a class using the container to resolve dependencies using callMethod():

class D
{
    private $foo;
    private $bar;

    public function getBar()
    {
        return $this->bar;
    }

    public function getFoo()
    {
        return $this->foo;
    }

    public function setFoo(IFoo $foo, $bar)
    {
        $this->foo = $foo;
        $this->bar = $bar;
    }
}

$container->bindSingleton('IFoo', 'ConcreteFoo');
$instance = new D();
$container->callMethod($instance, 'setFoo', ['Primitive was set']);
echo get_class($c->getFoo()); // "ConcreteFoo"
echo $instance->getBar(); // "Primitive was set"

Calling Closures

You can use callClosure() to automatically inject parameters into any closure:

echo $container->callClosure(
    function (Foo $foo, $somePrimitive) {
        return get_class($foo) . ':' . $somePrimitive;
    },
    ['123'] // Pass in any primitive values
);

This will output:

Foo:123

Checking a Binding

To check whether or not a binding exists, call hasBinding().

$container->bindSingleton('IFoo', 'ConcreteFoo');
echo $container->hasBinding('IFoo'); // 1
echo $container->hasBinding('NonExistentInterface'); // 0

Removing a Binding

To remove a binding, call unbind():

$container->bindSingleton('IFoo', 'ConcreteFoo');
$container->unbind('IFoo');
echo $container->hasBinding('IFoo'); // 0

Applications in Opulence

If you use Opulence's command library, the container automatically resolves the Command class. Also, the routing library uses Opulence\Routing\Dispatchers\IDependencyResolver to automatically resolve a matched controller. Typically, Opulence's container is used by IDependencyResolver to do the resolution.