1
\$\begingroup\$

I have recently been looking into the concepts/patterns behind dependency injection, inversion of control, and registries/service locators. I have searched about the internet on the subjects and have come up with some decent understanding about them, but some things still seem a little cloudy. To start, I would like to know if my understanding on the subjects is correct:

  • Dependency Injection - injecting a dependent class into another class that needs the dependency to operate correctly. e.g. ClassA depends on ClassB, so ClassB is injected into ClassA through either a method or constructor of ClassA.
  • Registries/Service Locators - Basically an array or object with keys/properties that hold other objects that can be called up when needed by other classes.
  • Inversion of Control - I actually don't really know what this is completely, yet. From what I can understand IoC is simply dependency inject exploited and enhanced to another level. IoC seems to utilize Closures to create "definitions" of what objects should look like (with dependencies) that can be called later in an application.

I'm interested in some clarifications on what these three things are in relation to each other and if I have the meanings/concepts behind each understood correctly. Each seem to have their uses, but I see mixed responses on the subjects. So perhaps some simple examples or situations on where they each have their use could help clear the air for me.

I learn best with practice, trial by error, and good 'ol hands-on approach. The internet, along with many posts over on Stack Overflow, reveal a number of tried and true IoC/DIC plugins you can use for your PHP apps, however I feel the need to make my own for understanding, experience, and personal enrichment. So to better understand all of these concepts I have tried to created my own IoC/DIC to get a grip on how all of this works and if I can use this sort of thing in future developments. I would like some feedback on my code and need to know if I am missing some considerations from an implementation standpoint. I have made this as a static class, as it seems to make sense. I know I will likely shamed and stoned for making such a thing static, but to me the end result if I were to use it would be to throw it behind a namespace and not pollute the global space. Making the class static also means I don't have to inject the IoC into other classes as well. But perhaps there are some considerations there I am not seeing right away either, so again, feedback is much appreciated.

Here is my IoC/DIC code I have in IoC.php:

<?php

/**
 * Dependancy Injector Container / Inversion of Control
 *
 * Learning project for DIC/IoC design pattern.
 *
 */

class IoC {

    private static $definitions = array();
    private static $registry = array();

    /**
     * Register a Closure for use in creating an object later.
     *
     * @param string $identifier String to identify the registered definition.
     * @param Closure $closure A Closure to be called that will create an object.
     */
    public static function register($identifier, Closure $closure) {
        if(array_key_exists($identifier, self::$definitions)) {
            throw new Exception("Identifier '$identifier' already defined in registry.");
        } else {
            self::$definitions[$identifier] = $closure;
        }
    }

    /**
     * Check if a registered definition exists.
     *
     * @param string $identifier The definition's indentifier to search for.
     * @return bool TRUE if a definition was found or FALSE if it was not.
     */
    public static function exists($identifier) {
        if(array_key_exists($identifier, self::$definitions)) {
            return true;
        }
        return false;
    }

    /**
     * Launches the Closure definition to create a specified object.
     *
     * @param string $identifier The Closure definition to locate and call.
     * @return mixed An object created by a Closure definition.
     */
    public static function create($identifier) {
        if(static::exists($identifier)) {
            $item = self::$definitions[$identifier];
            return $item();
        } else {
            throw new Exception("No item registered with the identifier of '$identifier'.");
        }
    }

    /**
     * Launches a Closure definition and stores the created object in the registry.
     *
     * @param string $identifier String to identify the created object in the registry.
     * @param string $object A Closure definition to look for and call.
     */
    public static function store($identifier, $object) {
        if(static::exists($object)) {
            $item = self::create($object);          
            self::$registry[$identifier] = $item;
        } else {
            throw new Exception("Unable to store item. No item registered with the identifier of '$identifier'.");
        }
    }

    /**
     * Gets an object stored in the registry.
     *
     * @param string $identifier The object identifier to find in the registry.
     * @return mixed Returns the requested object or FALSE if the object wasn't found.
     */
    public static function get($identifier) {
        if(array_key_exists($identifier, self::$registry)) {
            return self::$registry[$identifier];
        } else {
            throw new Exception("No item registered with the identifier of '$identifier'.");
        }
        return false;
    }
}

?>

I have created a file for this, which works as expected, but I haven't thrown anything behind a namespace to see how it works there. For my test I define a number of objects and then register a few with the IoC container and then print out some results. I start by creating a class called FruitBasket which uses some setter injection to obtain Fruit objects. I then create a Fruit parent class and some child classes Apple, Orange, Banana, and Pomegranate. One function in the IoC is called register() which I create a definition of a closure in and give it a string to identify that closure by. After a definition has been made I can have that closure called by making a call to create(), which only needs the identifier for the closure. The IoC container also contains a registry where objects can be instantiated and preserved using the store() function. store() needs an identifier to be used to identify the newly created object by in the registry and the identifier for a closure definition that has already been made using register().

Here are the classes used to create the FruitBasket and Fruits:

<?php 

class FruitBasket {

    /**
     * Array of fruits in the basket
     */
    private $fruits = array();

    /**
     * Constructor - no arguements
     */
    public function __construct() {}

    /**
     * Add a fruit to the basket.
     *
     * In order for the fruit basket to be useful it should
     * have some fruit in it.  The fruits are the dependancies.
     *
     * @param Fruit $fruit A type of Fruit to add to the basket.
     */
    public function addFruit(Fruit $fruit) {
        $this->fruits[] = $fruit;
    }

    /**
     * Show the flavors of all the fruit in the basket
     */
    public function show() {
        if(!empty($this->fruits)) {
            foreach($this->fruits as $fruit) {
                $fruit->getFlavor();
            }
        } else {
            throw new Exception("Nothing to show, no fruits!");
        }
    }
}

/**
 * Parent class: Fruit
 */
class Fruit {

    private $flavor = "";

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

    public function getFlavor() {
        print("The fruit " . get_class($this) . " tastes " . $this->flavor . ".\n");
    }
}

/**
 * Child class: Apple
 */
class Apple extends Fruit {

    public function __construct() {
        parent::__construct("sweet and juicy");
    }

}

/**
 * Child class: Orange
 */
class Orange extends Fruit {

    public function __construct() {
        parent::__construct("tangy - but in a citris way");
    }

}

/**
 * Child class: Banana
 */
class Banana extends Fruit {

    public function __construct() {
        parent::__construct("a bit on the bland side of sweetness");
    }

}

/**
 * Child class: Pomegrante
 */
class Pomegranate extends Fruit {

    public function __construct() {
        parent::__construct("juicy, seedy, and delicious followed by a little bit of sour-like sweetness");
    }

}

?>

Here is the test that I did to check some of the functionality:

<?php 

// - SET REGISTRATIONS

//Register the definition for an empty basket
//This definition doesn't do any injections.
IoC::register('emptyBasket', function () {
    $basket = new FruitBasket();
    return $basket;
});

//Register the definition with a basket that
//has some fruit.  This would hopefully be 
//an example of how to utilize setter injection.
IoC::register('fruitBasket', function () {
    $basket = new FruitBasket();
    $basket->addFruit(new Apple());
    $basket->addFruit(new Orange());
    return $basket;
});

//Register another definition but will all the
//fruits injected into the basket.
IoC::register('fullBasket', function () {
    $basket = new FruitBasket();
    $basket->addFruit(new Apple());
    $basket->addFruit(new Orange());
    $basket->addFruit(new Banana());
    $basket->addFruit(new Pomegranate());
    return $basket;
});



//Store an empty basket in the registry for shared use.
//Equivalent to storing a database connection.
IoC::store('emptyBasket', 'emptyBasket');

//Now it is time to do stuff with the things that have
//been created.

//Create a 'fruitBasket' and show its contents.
print("\nShowing contents of 'fruitBasket'. \n");
$basket = IoC::create('fruitBasket');
$basket->show();

//Create a 'fullBasket' and show its contents.
print("\nShowing contents of 'fullBasket'. \n");
$basket = IoC::create('fullBasket');
$basket->show();

//Get the 'emptyBasket' stored in the registry and add some fruits.
print("\nShowing contents of 'emptyBasket' - after some additions. \n");
$basket = IoC::get('emptyBasket');
$basket->addFruit(new Orange());
$basket->addFruit(new Pomegranate());
$basket->addFruit(new Apple());
$basket->show();

?>

This produced the following (expected) output:

Showing contents of 'fruitBasket'.
The fruit Apple tastes sweet and juicy.
The fruit Orange tastes tangy - but in a citris way.

Showing contents of 'fullBasket'.
The fruit Apple tastes sweet and juicy.
The fruit Orange tastes tangy - but in a citris way.
The fruit Banana tastes a bit on the bland side of sweetness.
The fruit Pomegranate tastes juicy, seedy, and delicious followed by a little bit of sour-like sweetness.

Showing contents of 'emptyBasket' - after some additions.
The fruit Orange tastes tangy - but in a citris way.
The fruit Pomegranate tastes juicy, seedy, and delicious followed by a little bit of sour-like sweetness.
The fruit Apple tastes sweet and juicy.

All in all I am happy with what the results are so far, but since the subjects are new to me I am reaching out the community here on Code Review for feedback. I am basically concerned with whether or not my understanding of DI/IoC/Registries/Service Locators are sound and whether or not I am overlooking various design considerations. I would also like to know if I am going in the right direction with my IoC code as it is now or if I am completely missing the point on how to create such a thing in PHP.

\$\endgroup\$
4
  • \$\begingroup\$ "I know I will likely shamed and stoned" Let me be the first to cast a stone at you. \$\endgroup\$ Dec 30, 2013 at 7:26
  • \$\begingroup\$ Humorous as it was, I'll also let you be the first to add some substance please. What is so horrible about it? I don't necessarily need an instance of the IoC object. \$\endgroup\$ Dec 30, 2013 at 8:17
  • \$\begingroup\$ Here is your code without the static. Apart from making IoC members non-static, only difference is the addition of $ctx = new IoC();. And it is a step forward, as it demonstrates I can now have many contexts in an application. \$\endgroup\$ Dec 30, 2013 at 9:23
  • \$\begingroup\$ Thanks for the suggestion. If I am understanding you correctly, are you recommending that instead of have a single IoC object out in the global space, I instead instantiate it as I need it in the various parts of my software?? \$\endgroup\$ Dec 30, 2013 at 16:52

1 Answer 1

3
\$\begingroup\$

First off: _steer clear of static's as much as you can, especially in PHP.

You seem unclear as to what IoC exactly is. It's perfectly simple, though: IoC has more to do with how your code works. How you generate the desired results. It doesn't define how you do it. At least, not as much as a definite pattern/implementation of this technique.
Dependency Injection (DI) is, actually, a technique with which IoC is achieved. Take a class that writes stuff to a log of sorts:

class Logger
{
    const LOG_FILE = 'file';
    const LOG_DB = 'db';

    private $resource = null;
    private $mode = null;

    public function __construct($resource, $mode = self::LOG_FILE)
    {
        $this->resource = $resource;
        $this->mode = $mode;
    }

    public function writeLog(array $params)
    {
        switch ($this->mode)
        {
            case self::LOG_FILE:
                return $this->logToFile($params);
            case self::LOG_DB:
                return $this->logToDb($params);
        }
    }

    private function logToFile(array $params)
    {
        fwrite($this->resource, implode(' - ', $params).PHP_EOL);
    }
}

Now, in this example, the logger class has a generic writeLog function that is public. It's this method that'll be called by the code throughout. Yet, depending on what resource was injected, the writeLog function will result in a call to a specific logTo* method.
Put simply: What you inject effectively controls how the class does what it does. The class' job is the same, but the specifics of its actions are controlled by its dependencies.

Of course, this example doesn't fully surrender control, so the inversion is only half-way there. In real life example (ie frameworks) you're quite likely to come across something that is often referred to as adapters, and your Logger class will look more like:

namespace Log;
use Log\Adapters\Adapter;

class Logger
{
    private $adapters = array();
    private $mode = null;
    private $active = null;

    public function __construct(Adapter $adapter)
    {
        $this->addAdapter($adapter, true);
    }

    public function write(array $params)
    {
        return $this->active->writeLog($params);
    }

    /**
     * Inject log adapters through this method
     * @param Log\Adapters\Adapter $adapter
     * @param bool $setActive = false
     * @return Log\Logger
     **/
    public function addAdapter(Adapter $adapter, $setActive = false)
    {
        $this->adapters[$adapter->getType()] = $adapter;
        if ($setActive)
        {
            $this->mode = $adapter->getMode();
            $this->active = $adapter;
        }
        return $this
    }

    public function __destruct()
    {
        foreach($this->adapters as $type => $adapter)
        {
            $adapter->close();
            unset($this->adapters[$type];
        }
    }
}

Now this method excepts a dependency of the type Log\Adapters\Adapter which, in itself doesn't say much. That's simply because this class should be an abstract from which all actual loggers should extend, or it should be an interface which all logger classes should then implement. This ensures/enforces the dependency will have all the methods that you're using in your Logger class, so it can do its job. However, how the data is written, and where it is written is beyond the control of the Logger itself. Hence, we have established a clear Inversion of Control: Here, the dependency actually controls how the Logger will actually function.

Let's assume Adapter is an Interface:

namespace Log\Adapters;
Interface Adapter
{
    public function getType();
    public function getMode();
    public function writeLog(array $params);
    public function close();
}

Then, a DB logger would end up looking like:

namespace Log\Adapters;
class DBLog implements Adapter
{
    private $db = null;
    private $logTbl = null;
    public function __construct(\PDO $db, $logTbl = 'logs')
    {
        $this->db = $db;
        $this->logTbl = (string) $logTbl;
    }
    public function writeLog(array $params)
    {//crude and horrid code, but just as an example
        $stmt = $this->db->prepare(
             'INSERT INTO '.$this->logTbl.
             ' ('.implode(',',array_keys($params)).') VALUES ('.
             substr(str_repeat('?,',count($params)),0,-1).')'
        );
        $stmt->execute($params);
        return $this;
    }
    public function getType()
    {
        return get_class($this).'->'.$this->logTbl;
    }
    public function getMode()
    {
        return 'db';
    }
    public function close()
    {
        //$this->db->commit(); perhaps?
        $this->db = null;//deref db connection
    }
}

On the front of registries and serivice locators, I'll be brief. A registry is actually nothing more than a global in drag, dressed in the tinsel of OO code. A registry class is actually pretty much exactly the same as $_GLOBAL. Compare the following:

Zend_Registry::set('db', new PDO());//set PDO connection in registry
//compared to
$_GLOBALS['db'] = new PDO();

And then, anywhere in your code:

$db = Zend_Registry::get('db');
$db = $_GLOBALS['db'];

The advantages are that a registry allows you to implement some checks in the set method, to avoid accidentally reassigning a given dependency. Be it through your own code or third party dependencies. The drawbacks are: They still are global instances (thus their ref-count will always be 1 or more, and so they'll stay in memory, weather you need them to or not) and they allow for messy code: DB stuff in viewscripts is possible owing to a registry.
A service locator is pretty much the same thing, but it's a bit more refined, and easier to test, because you can simply use an ini, xml or yaml file to list all the services you need, and test weather or not they are available, without needing to create an instance.
So in a way, a service locator works like a registry, but it loads the dependencies when you need them (≃ lazy loading). Some more elaborate SL's allow for dependency injection on the services that it churns out (like Symfony2's container). The downsides are similar, though:
Harder to read/test (though still easier to test than Registry), easier to make messy code, At its heart, SL is likely to be just a factory.

IMO, If you want to know what is considered good practice, I'd say: a well written API, with plenty of injection, clear interfaces, and manually inject what you need, where you need it. Forget about registries, as they often are proof of "programming by afterthought". (as in: Oh, wait, I forgot to write that to the DB, I'll get a connection from the registry, or SL, and put together a quick query, possibly accompanied by a //TODO: find a better place for this comment)

\$\endgroup\$
9
  • 1
    \$\begingroup\$ Thank you for the (very) thorough answer. You've managed to shed a lot of light on the design principals I need to focus on. From your explanation my IoC container isn't really that, but more of a factory with a registry component, yes? In a practical example, let's say I am using the MVC pattern and I have 20 different models with 15 of them needing DB access. My goal would be to have a universal way of injecting the DB into all of those models but at the same time have the ability to swap out a DB connection for a mock or alternate DB without changing them all. Any suggestions for that?? \$\endgroup\$ Dec 30, 2013 at 16:37
  • \$\begingroup\$ @Crackertastic: In an MVC context, the Model bit should be treated as a layer, not an object. In my projects, models are pure data carriers, that are passed on via a service (that is accessed by the Controller), who then passes them on to a data mapper, which maps the model onto a database. These mappers can also contain queries that simply return Models, too. If you swap those out for mock objects, or return mock Models, you're home free... Injecting DB connections into data carriers is, IMO, a violation of the SRP (Single Responsibility Principle). \$\endgroup\$ Dec 30, 2013 at 16:58
  • \$\begingroup\$ @Crackertastic: On your IoC container thing: yup, it's just a factory + registry wrapper. It still requires you to IoC::set all dependencies you might need, which will then simply add those instances to an aptly named $registry property... Because it's all statics, the methods and properties essentially work in much the same way as globals do, only with the added noise (and overhead!) of object orientation (check google: static methods actually take longer to call than regular globals do, because of PHP having to create the self:: reference + specific scope \$\endgroup\$ Dec 30, 2013 at 17:15
  • \$\begingroup\$ Okay, I think is see what you mean with DB models. With models I didn't necessarily consider strict SRP, but instead kept responsibilities as low as possible. For example (non-DB related) in one project I had a model that I used to build an email. The model would accept user input, sanitize it, error check, and then store status/error info and build/send the email message if there were no errors. The view would query the model for indications that an error occurred (or succeeded) so something would be created on screen for the user. Do you think this is a bad approach in MVC context? \$\endgroup\$ Dec 30, 2013 at 19:16
  • 1
    \$\begingroup\$ Thanks for the tips, I will do that. As for the View/Model interaction and views doing logic: it seems rudimentary that the view can't ask the model if it failed/succeeded or get some status update. If my view needs to know something that the model is going to answer, what is the real point of using the controller for that? My views do not do logic related to data processing, but they aren't dumb templates either. Basically the view is asking the model if it succeeded so it knows to display an error message or success message. Perhaps we have different philosophies on how to utilize MVC. \$\endgroup\$ Dec 30, 2013 at 22:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.