Introduction

Zend_Di is a dependency injector component. It minimizes coupling between groups of classes, makes unit testing much simpler, and provides an easy way to re-configure a package to use custom implementations of components. The architecture of the Zend_Di component is based on the following concepts:

  • Dependency injection is a technique that consists of passing objects via the constructor or setter methods.
  • The Container provides an easy way of re-configuring a package to use custom implementations of components.
  • Responsibility for object management is taken over by whatever container is being used to manage those objects.

Benefits of using a DI Container:

  • Easy best practice unit testing
  • Component reuse
  • Centralized configuration
  • Clean and declarative architecture
  • Maintainability and adaptability

References

Requirements

  • This component will use Reflection to wire dependencies.
  • This component will use Configuration to wire dependencies.
    • This component must support PHP, XML and INI configuration files.
  • This component must allow all components to be constructed using Zend_Config instances.

Dependencies

  • Zend_Config (optional)
  • Zend_Exception
  • Zend_Loader

Operation

Zend_Di provides generic factory classes that instantiate instances of classes. These instances are then configured by the container, allowing construction logic to be reused on a broader level. For example:

$components = array(
    'Foo' => array(
        'class'        => 'Zend_Foo',
        'arguments'    => array(
            '__construct' => 'ComponentA',
        ),
    ),
    'ComponentA' => array(
        'class'        => 'Zend_Foo_Component_A',
        'instanceof'   => 'Zend_Foo_Component_Interface',
    ),
    'ComponentB' => array(
        'class'        => 'Zend_Foo_Component_B',
        'instanceof'   => 'Zend_Foo_Component_Interface',
    ),
);

$config = new Zend_Config($components);
// $config = new Zend_Config_Xml('components.xml', 'staging');

$di = new Zend_Di_Container($config);

// Create an instance of Zend_Foo and injects Zend_Foo_Component_A via the constructor method
$foo = $di->loadClass('Foo')->newInstance();

Once we separate configuration from use, we can easily test the Car with different Engines. It's just a matter of re-configuring the package and injecting Zend_Car_Parts_Engine_Gas instead of Zend_Car_Parts_Engine_Fuel.

Package

  • Zend_Di
  • Zend_Di_Container
  • Zend_Di_Factory
  • Zend_Di_Reflection
  • Zend_Di_Parameter
  • Zend_Di_Data
  • Zend_Di_Registry
  • Zend_Di_Storage_Interface
  • Zend_Di_Storage_Object
  • Zend_Di_Storage_Exception
  • Zend_Di_Exception

Use cases

Zend_Di handles injections via the constructor or setters methods. In addition, the component allows the user to map out specifications for components and their dependencies in a configuration file and generate the objects based on that specification.

Assembling Objects Using Reflection

Assembling objects using Zend_Di_Reflection

class Zend_Foo {
    public function __construct(Zend_Foo_Component_A $componentA) {}  
    public function setComponentA(Zend_Foo_Component_Interface $component) {}   
    public function setComponentB(Zend_Foo_Component_Interface $component) {} 
    public function setComponentC(Zend_Foo_Component_C $componentC) {}  
} 

$di = new Zend_Di_Reflection();  
$di->addComponent('Zend_Foo_Component_A', array('__construct', 'setComponentA'));  
$di->addComponent(new Zend_Foo_Component_B(), array('setComponentB')); 
$di->addComponent(new Zend_Foo_Component_C()); 

$di->loadClass('Zend_Foo')->newInstance();
$foo = $di->getComponent('Zend_Foo');

Assembling objects using Zend_Di_Container

class Zend_Foo {
    public function __construct(Zend_Foo_Component_A $componentA) {}  
    public function setComponentA(Zend_Foo_Component_Interface $component) {}   
    public function setComponentB(Zend_Foo_Component_Interface $component) {} 
    public function setComponentC(Zend_Foo_Component_C $componentC) {}  
} 

$di = new Zend_Di_Container();

$di->loadClass('Zend_Foo')
    ->addComponent('Zend_Foo_Component_A')
    ->selectMethod('setComponentA')
        ->addComponent('Zend_Foo_Component_A')
    ->selectMethod('setComponentB')
        ->addComponent('Zend_Foo_Component_B')
    ->selectMethod('setComponentC')
        ->addComponent('Zend_Foo_Component_C')
    ->newInstance();

Assembling Objects Using Configuration

The configuration is typically set up in a different file. Each package can have its own configuration file: PHP, INI or XML file. The configuration file holds the components specifications and package dependencies.

You can pass an instance of Zend_Config via the constructor, or set a configuration array using the setConfigArray() method.

The cases below assume that the following classes have been defined:

class Zend_Foo {
    public function __construct(
        Zend_Foo_Component_Interface $componentA = null,
        Zend_Foo_Component_Interface $componentB = null,
        $arg3 = null,
        $arg4 = null) {
    }

    public function setComponentA(Zend_Foo_Component_Interface $component, $arg2 = null) {
    }
}

interface Zend_Foo_Component_Interface {
}
class Zend_Foo_Component_A implements Zend_Foo_Component_Interface {
}
class Zend_Foo_Component_B implements Zend_Foo_Component_Interface {
}

$components = array(
    'Foo' => array(
        'class'        => 'Zend_Foo',
        'arguments'    => array(
            '__construct' => 'ComponentA',
        ),
    ),
    'ComponentA' => array(
        'class'        => 'Zend_Foo_Component_A',
        'instanceof'   => 'Zend_Foo_Component_Interface',
    ),
    'ComponentB' => array(
        'class'        => 'Zend_Foo_Component_B',
        'instanceof'   => 'Zend_Foo_Component_Interface',
    ),
);

$config = new Zend_Config($components);
// $config = new Zend_Config_Xml('components.xml', 'staging');

$di = new Zend_Di_Container($config);

// Create an instance of Zend_Foo and injects Zend_Foo_Component_A via the constructor method
$foo = $di->loadClass('Foo')->newInstance();

The two major flavors of Dependency Injection are Setter Injection (injection via setter methods) and Constructor Injection (injection via constructor arguments). Zend_Di provides support for both, and even allows you to mix the two when configuring the one object.

Constructor dependency injection

When a class is loaded, the constructor method is selected by default.

Inject a single dependency:
$di->loadClass('Foo')
    ->addComponent('ComponentA')
    ->newInstance();
Inject multiple dependencies:
$di->loadClass('Foo')
    ->addComponent('ComponentA')
    ->newInstance();
Inject dependencies and pass arguments:
$di->loadClass('Foo')
    ->addComponent('ComponentA')
    ->addComponent('ComponentB')
    ->addValue('arg3')
    ->addValue('arg4')
    ->newInstance();

// Or...
$di->loadClass('Foo')
    ->addComponent('ComponentA', 'ComponentB')
    ->addValue('arg3', 'arg4')
    ->newInstance();

Users can map out specifications for components and their dependencies. So whenever a class is loaded, Zend_Di will inject the dependencies automatically. For example:

$config = array(
    'Foo' => array(
        'class'        => 'Zend_Foo',
        'instanceof'   => 'Zend_Foo',
        'arguments'    => array(
            '__construct'   => 'ComponentA, ComponentB, :param',
            'setComponentA' => 'ComponentA'
        ),
    ...

$di = new Zend_Di_Container();
$di->setConfigArray($config);

// Bind parameter and create an instance of the Zend_Foo class
$di->loadClass('Foo')
    ->bindParam(':param', 'Parameter 1')
    ->newInstance();

Setter dependency injection

// Pass dependencies through the setComponentA() method
$di->loadClass('Foo')
    ->selectMethod('setComponentA')
        ->addComponent('ComponentA')
    ->newInstance();

Zend_Di injects dependencies using the top-down fashion, starting with the constructor and ending with the setter methods.

// Constructor and setter dependency injection
$di->loadClass('Foo')
    ->addComponent('ComponentA', 'ComponentB')
    ->addValue('arg3', 'arg4')
    ->selectMethod('setComponentA')
        ->addComponent('ComponentA')
        ->addValue('arg2')
    ->newInstance();

Users can map out specifications for a component:

$config = array(
    'Foo' => array(
        'class'        => 'Zend_Foo',
        'instanceof'   => 'Zend_Foo',
        'arguments'    => array(
            'setComponentA' => 'ComponentA',
        ),
    ...

$di = new Zend_Di_Container();
$di->setConfigArray($config);

$foo = $di->loadClass('Foo')->newInstance();

Storage Containers

You can tell Zend_Di what components to manage by adding them to a container (the order of registration has no significance). Containers are stored are retrieved using the Zend_Di_Registry class. The Zend_Di_Registry::getContainer() method returns an instance of Zend_Di_Storage_Interface.

$di = new Zend_Di_Container();
$di->setConfigArray($config);
$foo = $di->loadClass('Foo')->newInstance();

$registry = $di->getRegistry();
$registry->open('FooPackage');
$registry->add('Foo');
$registry->close();

// Get an instance of the container FooPackage
$fooPackage = $registry->getContainer('FooPackage');

while ($obj = $fooPackage->current()) {
    echo $fooPackage->getClassName();
    $fooPackage->next();
}

// Get a single instance of the Foo class
$foo = $registry->getSingleton('Foo');

You can register your own container as long as you pass an instance of Zend_Di_Storage_Interface. New containers can be register using the Zend_Di_Registry::setStorage() method.

class Zend_Di_Storage_Cache implements Zend_Di_Storage_Interface {
}

$di = new Zend_Di_Container();
$di->getRegistry()->setStorage(new Zend_Di_Storage_Cache());

Real-life example

Please visit the following page:
http://framework.zend.com/wiki/display/ZFPROP/Zend_Di+Example

SVN Repository

$ cd libraries/Zend
$ svn co http://svn.fedecarg.com/repo/Zend/Di

Also available in: HTML TXT