Pragmatism in the real world

An introduction to Zend\Di

Zend Framework 2 provides its own dependency injection container, Zend\Di which is a key underpinning of the entire framework and especially the MVC system. I have covered before, my thoughts on the reasons for using dependency injection, so this article looks at the fundamentals of using Zend\Di.

Constructor injection

Consider this code:

namespace My;

class DatabaseAdapter
{
}

class UserTable
{
    protected $db;
    
    public function __construct (DatabaseAdapter $db)
    {
        $this->db = $db;
    }
}

This is simple code – a UserTable must have a DatabaseAdapter. As we are using constructor injection for the dependency on DatabaseAdapter, it follows that the DatabaseAdapter object must be created first.

Standard PHP code would look like this:

$db = new MyDatabaseAdapter();
$userTable  = new MyUserTable($db);

As we have type-hinted the $db parameter in the constructor for UserTable, the ZendDi component can quite easily do this work for us:

$di = new Zend\Di\Di();
$userTable = $di->get('My\UserTable');

This code produces exactly the same result (an instantiated $userTable) that the snippet above does, but this time, we used Zend\Di\Di to actually do the object creation.

Note that get() will always return the same object. Use newInstance() for a different one.

Parameters in dependent class constructors

If the dependent class takes constructor parameters we can use the instance manager’s setParameters method. Let’s assume that the DatabaseAdapter has a dsn property and looks like this:

class DatabaseAdapter
{
    protected $dsn;

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

We can set the dsn for our adapter using:

$di->instanceManager()->setParameters('My\DatabaseAdapter', array(
    'dsn' => 'mysql:host=localhost;dbname=testdb',
));

and then we get the $userTable object using this code:

$userTable = $di->get('My\UserTable');

We could also set the dsn property of the dependent object at the same time as we retrieve the $userTable. This is accomplished using the $parameters argument to get():

$userTable = $di->get('My\UserTable', array('dsn'=>'mysql:host=server1;dbname=testdb'));

which also works as you would expect.

Setter injection

Setter injection is another popular way of setting dependencies for a class. Generally, I personally prefer to use setters for dependencies that are optional for the class to work and constructor parameters for required dependencies.

Setter injection means that we use a setXyx() method rather than the constructor, so this is how it’s done with ZendDi. Lets start with the class definitions:

namespace My;

class DatabaseAdapter
{
    protected $dsn;
    public function __construct($dsn)
    {
        $this->dsn = $dsn;
    }
}

class UserTable
{
    protected $db = null;
    
    public function setDatabaseAdapter(DatabaseAdapter $db)
    {
        $this->db = $db;
    }
}

ZendDi understands the method format of set followed by capital letter as setter methods and we turn enable it using:

$di = new Zend\Di\Di();
$di->configure(new Zend\Di\Configuration(array(
    'definition' => array(
        'class' => array(
            'My\UserTable' => array(
                'setDatabaseAdapter' => array('required' => true)
            )
        )
    )
)));
$userTable = $di->get('My\UserTable', array('dsn'=>'mysql:host=server2;dbname=somedb'));

Firstly we tell the Di object that the setDatabaseAdapter() method is required in order to instantiate My\UserTable objects and then our usual call to get will work. It therefore follows that if your object has more than one set method that you want to be called by Di, then you set the required flag on all of them within the Configuration object.

Again, we get a configured UserTable object from the DI container when we call get().

Aliases

One last thing that I want to mention in this article is that Zend\Di also has the concept of aliases. These are intended to make it simpler to specify dependencies and/or substitutions. As an example, I might alias Zend\Db\Adapter\Driver\Mysqli\Mysqli to “db“, allowing me to call $di->get('db') and retrieve that class. This means that I could very easily configure the container differently to alias db to a different adapter and not need to change the code that retrieves the class from the container.

Conclusion

In this article, I’ve tried to cover the basics of how Zend\Di works. If you want to explore more, Ralph Schindler has created a number of examples on GitHub. I recommend running them locally.

Note
I have updated the example in this article based on feedback from Ralph Schindler and Marco Pivetta.

8 thoughts on “An introduction to Zend\Di

  1. Hi Rob,

    it looks like ZendDi works a little differently to what I am used to. Not sure I am a fan of concrete type hints for autowiring but each to their own :)

    Quick question, if $di->get() returns the same instance but you can pass it constructor or setter parameters at runtime, what happens if you call it twice with different parameters?

  2. What if I want use a subclass of Artist? What if I do type hint with a Interface?

  3. what is the advantage of doing this? I see much more simple to write:

    $artist = new MyArtist();
    $album  = new MyAlbum($artist);
    

    instead of

    $di = new ZendDiDi();
    $di->configure(new ZendDiConfiguration(array(
        'definition' => array(
            'class' => array(
                'MyAlbum' => array(
                    'setArtist' => array('required' => true)
                )
            )
        )
    )));
    $album = $di->get('MyAlbum', array('name'=>'Train'));
    

    could someone explain to me? sorry is a new concept to me

  4. @sergio: Dependency injection (DI) helps manage your dependencies and to reduce the complexities of your applications (making them more maintainable). In simple applications it's easy to manage your own dependencies but as the application grows and your service layer is expanded keeping track of those dependencies can become an daunting task. DI aims to simplify the process by allowing you to specify a configuration and then letting the DI container handle keeping track of instances and the dependencies of each object.

  5. Sergio, if you are only injecting the dependencies into a few classes then the first way you suggest would be indeed easier. Doing so though does not give you the full advantages of DI, this only comes by using it throughout the application and effectively pushing the creation of dependencies out of the running of the application altogether.

    This is best achieved having all of an objects dependencies injected in and not using the container at all apart from at the entry point to the application. At which point it will set up the object you request with all its dependencies and their dependencies and so on, through the whole object graph.

    When you are then creating all the dependencies for an application your simple example would be repeated numerous times and having a configurable container would start to look more appealing than manually configuring the dependencies.

    The container will also give you the advantage of only creating the dependencies of the object you initially retrieve from it whereas manually creating the dependencies may lead to a lot of unnecessary object creation.

Comments are closed.