6.0.0-git
2024-04-19
Last Modified 2010-01-15 by Chuck Hagenbuch

Horde_Injector

Horde_Injector is a lightweight dependency-injection container.

Dependency Injection Container FAQ

Where can Horde_Injector be used?

In the application layer only. If you use this in your business models I will find you and beat you to death with a shoe.

How do I provide Horde_Config values to my business models?

Factories.

$injector->bindFactory('InterfaceX', 'FactoryX', 'create');

class FactoryX {
    public function create(Horde_Injector $injector) {
        $setting = $injector->getInstance('Horde_Config')->get('main', 'setting');
        return new X($setting);
    }
}

I have an array-typed parameter in my constructor, do I have to use a factory to provide the array of values?

Maybe. It depends on which you believe is easier. Consider this example where a more specific ArrayObject is used. Does this array get reused? If so it may be worth creating a special extension of ArrayObject.

$injector->bindFactory('Dictionary_Sources', 'Dictionary_Sources_Factory');
$dictionary = $injector->createInstance('Dictionary');

class Dictionary {
    public function __construct(Dictionary_Sources $sources) {
        ...
    }
}

class Dictionary_Sources extends ArrayObject{}

class Dictionary_Sources_Factory {
    public function create(Horde_Injector $injector) {
        return new Dictionary_Sources(array(
            $injector->getInstance('Dictionary_Source_Cache'),
            $injector->getInstance('Dictionary_Source_Db'),
            $injector->getInstance('Dictionary_Source_Json')
        ));
    }
}

You're probably thinking that you could just create a factory to build your Dictionary object since you need to write a factory anyways. The real benefit is when you decide that Dictionary needs a new collaborator, say a Logger. If you have defined a factory for your Dictionary object, then you must edit that factory and create a Logger and pass it to your Dictionary. With the method above, you would simply need to edit your constructor, and the Logger will be provided for you. This gives you much greater flexibility, especially if you have objects that can operate on the same array of objects, but need slightly different configuration.

Dependency Injection Container Spec


Terms

  • Dependency Injection - DI
  • Dependency Injection Container - DIC

Requirements

Switching business layer implementations across an application is expensive.

DI decouples our business layer classes from other business layer classes but doesn't solve the problem of decoupling our application layer classes from our business layer classes. Without functionality to tackle this issue, business layer class configuration is duplicated throughout the application.

As expense is the primary concern, any solution to the problem must not simply move the expense to initial application layer implementation.


Functional Spec

Goals

  • Decouple application layer (controller) from business layer (model)
  • Allow easy use of models which are decoupled from their dependencies using DI
  • Favor code over configuration
  • Simple, testable design

Features

A DIC allows you to make typically difficult configuration changes, like changing an entire application from using one service implementation to another, with simple modifications in one localized place.

A DIC manages the way your objects are wired together. It makes the wiring entirely configurable per-application, per-module, and all the way down to per single object instantiation. Needing configuration to make lots of individual objects is inefficient and hard to maintain which is why its reserved for only special cases. In reality most classes rely on only a few service objects (configuration, database connection, cache, etc.) and these object most often will have identical configurations in the entire application with a few per-module exceptions.

DICs don't require configuration for every object you want them to create. In most cases class dependencies will be detectable and the DIC will be able to provide those dependencies automatically.

Using a DIC for all application-level object creation makes your application unit-testable. Dependencies can easily be swapped out with mock objects because all dependencies are provided to the application using a DIC.


Technical Spec

The name of our DIC implementation is Horde_Injector. Horde_Injector can be told how to create objects using factories or it can try to create them itself using reflection. It can also be told to return an already-instantiated object when an interface is requested. Finally, if the requested interface has not been bound to a factory, implementation, or instance the injector will attempt to create the object using an implementation binder. For this to be successful the interface must actually be a concrete class.

Binders

The way we tell Horde_Injector how to create objects is using "binders" (which implement Horde_Injector_Binder). Horde_Injector maintains references to binders. References can be added in two ways:

$binder = new Horde_Injector_Binder_X($param1);
$injector->addBinder($interface, $binder)

$injector->bindX($interface, $param1);

Factory binders

[Factories|http://en.wikipedia.org/wiki/Factory_method_pattern] are classes with methods which instantiate and return objects. In the following example an interface is bound to a factory. If DataSourceX had dependencies it could instantiate them itself or ask $injector for those dependencies.

$injector->bindFactory('DataSourceInteface', 'DataSourceFactory', 'create');

class DataSourceFactory {
    public function create(Horde_Injector $injector) {
        return new DataSourceX();
    }
}

{warning}Factory method names can be whatever you want but they must only require one parameter and it must be a Horde_Injector object.{warning}

Implementation binders

Reflection allows us to programmatically inspect the structure of the class that is to be instantiated. By reading the interface types of the class constructor's parameters and then asking the injector to create those objects as well we try to provide the requested class's constructor with all its dependencies.

$injector->bindImplementation('DataSourceInteface', 'DataSourceX');

class DataSourceX {
    public function __construct(DependencyY $dependencyY) {
        ...
    }
}

Implementation binders also allow the calling of optional setter injection methods. Providing the method parameters here is done the same way as its done in the constructor, using reflection.

$injector->bindImplementation('DataSourceInteface', 'DataSourceX')
         ->bindSetter('setLogger');

class DataSourceX {
    public function __construct(DependencyY $dependencyY) {
        ...
    }

    public function setLogger(Logger $logger) {
        ...
    }
}

Choosing a binder

Use a factory binder if:

  • The class you are instantiating has any untyped parameters
  • You wish to create an instance of a class, that needs to have 2 objects of the same interface, but configured differently. [See FAQ|Dependency Injection Container FAQ]

Use an implementation binder if:

  • The class you are instantiating has only typed parameters

Instances

Horde_Injector maintains an array of object instances which are bound to interfaces. Instance binding happens two different ways: setting the instance binding manually, or by asking the injector to give you an instance (if it doesn't have one, then one is created and a reference to the internal instances array is added.)

$instance = new X();
$injector->setInstance('X', $instance);

Getting objects

For requested objects to be returned the injector must already have all the information present it needs to create the object.

Creating a new instance

To get a guaranteed new object use createInstance. References to instances retrieved in this manner are not stored. They will not be available to other objects unless you use setInstance to store the instance on the Injector.

$injector->createInstance('X');

{note}Although the return object will be new, its dependencies may not be. The injector will search its internal instances array for an instance matching the dependency's interface and if a match if found it will be used. If for some reason you need to guarantee that all dependencies are new, then you should consider using a factory binder.{note}

Getting an instance

As previously mentioned instances are pooled by the injector, so getInstance() gives developers the opportunity to reuse objects. If an instance exists for the requested interface it will be returned, otherwise it will be created, added to the injectors internal instances array, and returned.

$injector->getInstance('X');

Scoping with child injectors

Horde_Injector implements the [Chain of Responsibility|http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern] design pattern with bindings and instances. What this means is that injectors will try to give you a binder or instance but if it doesn't have it it will ask its parent injector for them and try returning that to you.

$injector->bindFactory('InterfaceX', 'FactoryX', 'create');
$childInjector = $injector->createChildInjector();
$x = $childInjector->createInstance('InterfaceX'); // success!!!

$childInjector = $injector->createChildInjector();
$childInjector->bindFactory('InterfaceY', 'FactoryY', 'create');
$y = $injector->createInstance('InterfaceY'); // failure!!!

$x = $injector->getInstance('X');
$childInjector = $injector->createChildInjector();
$x === $childInjector->getInstance('X'); // true

Child Injectors allow you to configure sub-modules of code differently, without leaking any state into the global scope.