PHPTools‎ > ‎

DependencyInjectionContainer

Dependency injection is a way to decouple classes and improve testability.  For example, let's say you have a User object and you need to store it in the session.  Not too far-fetched.  Being a good programmer, you have a User object and it has to start the session.  No big deal.

class User {
    function __construct() {
        session_start();
        if ($_SESSION['userId']) {
            // Code here to load the user
        } else {
            // Load the guest user
        }
    }
}

No problems.  Now you need to change the session ID.  Or perhaps you aren't going to use $_SESSION but instead use the cool, spiffy alternate that you found which uses memcache or something entirely different.  To accomplish that, you'll need to modify the User class.  Now, why should we be modifying a User class in order to get it to persist the data differently?  The user shouldn't really care about the storage mechanism; it should really care that it wants to persist and let the "other thing" actually do the saving.  Separating User from the $_SESSION superglobal and from whatever storage mechanism you use.  The code could become more testable since it relies upon fewer external dependencies.

A presentation from Fabien Potencier goes into a very simple example of how dependency injection could be used to swap out the storage mechanism from the above example.  Fabien also wrote a multi-part dependency injection discussion on the topic and he also covers how a container can be the glue between your different classes and will be the central point for your configurable class-to-class interaction.

The DependencyInjectionContainer class is nearly identical to the one Fabien has in his presentation, except that closures are not only passed the container, but they are also passed the key that was referenced.  You may not need it, but the asShared() method does use it so that we can assign the shared single instance of the class directly on the container again. This way we can save one extra function call with subsequent accesses to the same property.

Usage

To create the initial container is pretty easy.

$container = new DependencyInjectionContainer();

Now we can assign properties.

// Easy way to access our SOAP client
$container->sc = function ($container) {
    // Gets the WSDL from the container
    return new SoapClient($container->wsdl);
}

// Define our WSDL - Note that this is able to be defined after
// we set up "sc" to use the WSDL set in the container.  This
// setting won't actually be used until we call $container->sc;
$container->wsdl = 'http://example.com/soap.wsdl';

// A shared database connection, only opened if needed
$container->dbHost = 'localhost';
$container->dbUser = 'root';
$container->dbPass = 'SecretKey';
$container->db = $container->asShared(function ($container) {
    $res = mysql_connect($container->dbHost, $container->dbUser, $container->dbPass);
    return $res;
});

Note that none of the closures will get called yet.  We're just setting them up so they could get called if some of our code needs anything stored in the container.  Let's go see how that could work - I expect your implementation to be different.

class Item {
    static function loadById($container, $id) {
        $db = $container->db;
        // At this point the container finally looked up the DB
        // configuration and connected to the database
        $result = mysql_query('SELECT * FROM Item WHERE id = ' .
            ((integer) $id), $db);
        // Really a database abstraction layer would be better.
        // ... Process $result and then
        return Item::__set_state($loadedData);
    }
}

For more explanation, please see the examples in Fabien's presentation and his articles.

Caveats

You are unable to add to an array directly:

// Fail
$container->collection = array();
$container->collection[] = 'something new';

Instead, you can either assign to a temporary variable or use ArrayObjects, since objects do work well.

// Temporary variable
$container->collection = array();
$temp = $container->collection;
$temp[] = 'something new';
$container->collection = $temp;

// ArrayObject
$container->collection = new ArrayObject();
$container->collection[] = 'something new';

Additionally, you can not assign closures to the container and expect them to be returned.  If you try this, you will get the returned value from the closure instead.

// Fail
$container->closure = function () { return 'oops'; };
$closure = $container->closure;  // $closure == 'oops'

However, I haven't had a huge need to return closures yet.  Perhaps you won't either.
Comments