[cvs] [Wiki] changed: Doc/Dev/Injector

Chuck Hagenbuch chuck at horde.org
Fri Jan 15 17:16:58 UTC 2010


chuck  Fri, 15 Jan 2010 12:16:58 -0500

Modified page: http://wiki.horde.org/Doc/Dev/Injector
New Revision:  1.6
Change log:  first pass adding Gunnar's docs as well

@@ -1,9 +1,325 @@
  [[toc]]
+
++ Introduction
+
+The dependency injection pattern  
(http://en.wikipedia.org/wiki/Dependency_injection) is a useful  
approach that can help to avoid using global variables or state. If a  
class depends on a connection to a database then this connection is  
often pulled into the class using a singleton pattern or by using a  
global variable.
+
+Instead of providing the class with knowledge about the global state  
it is often preferable to "inject" the dependency into the class from  
the outside. This usually happens within the class constructor. To get  
hold of a database connection a class constructor could for example  
require an object that implements a database interface instead of  
using a singleton pattern.
+
+This way the dependencies of a class are immediately visible in the  
constructor. It is not necessary to search the code of the class for  
references to the global scope. This usually also helps to decouple
+dependencies between different code modules. Another major benefit of  
dependency injection is the fact that it facilitates unit testing of  
complex systems.

  + Horde_Injector

-Horde_Injector is a lightweight dependency-injection container.
+Horde_Injector provides a "Dependency Injection" framework.  For PHP  
there exist several dependency injection frameworks (e.g.  
http://stubbles.net,  
http://components.symfony-project.org/dependency-injection) with  
extensive feature lists. So there is hardly any need for another  
framework with similar capabilities.
+
+The essential part of dependency injection is the structure of  
classes with dependencies. They need to be amenable for an external  
management of their dependencies. If that is the case for a given  
class then most dependency injection frameworks should have no problem  
handling this class within the framework. The choice of the actual  
framework should not matter anymore.
+
+Horde_Injector provides only a minimal version of dependency  
injection.  It is somewhere in between the frameworks mentioned above  
and Twittee (http://twittee.org/). The primary goal is to drive  
refactoring of classes with complex dependencies so that their  
dependencies can be uncoupled and they can be used with a dependency  
injection framework.
+
+++ Making classes amenable to dependency injection
+
+As trivial as it may sound: a class can be managed by a dependency  
injection framework if the class allows the framework to inject its  
dependencies from the outside. That means that the class may **not**
+
+* pull in a dependency using global state via the singleton pattern:
+<code type="php">
+External_Class::singleton();
+</code>
+* create new objects with dependencies:
+<code type="php">
+$db = new DB();
+$b = new User($db);
+</code>
+* use global variables:
+<code type="php">
+global $conf;
+$db = new DB($conf['sql');
+</code>
+
+In most cases the class should receive dependencies and required  
parameters within the constructor.
+
+++ Using Horde_Injector
+
+The Horde_Injector class is a simple container that allows you to  
fill it with a number of elements that can be retrieved later:
+
+<code type="php">
+$a = new Horde_Injector(new Horde_Injector_TopLevel());
+$a->setInstance('a', 'a');
+echo $a->getInstance('a');
+</code>
+<code>
+string(1) "a"
+</code>
+
+Here we assigned a concrete instance to the injector. In fact not  
even an instance but a simple type: a string. Usually you would  
register an object.
+
+But there might be situations - and in fact these are what dependency  
injection is about - where you do not want to register a concrete  
instance. You might not already have all the dependencies for creating  
an instance in place. So all you want to do is to register the  
required build instruction for generating an instance.
+
+This is something that you can do by registering a wrapper object  
that implements the Horde_Injector_Binder interface. This wrapper  
object needs to be capable of creating the concrete instance:
+
+<code type="php">
+class Binder implements Horde_Injector_Binder
+{
+    public function create(Horde_Injector $injector)
+    {
+        return 'constructed';
+    }
+    public function equals(Horde_Injector_Binder $binder)
+    {
+        return false;
+    }
+}
+
+$a = new Horde_Injector(new Horde_Injector_TopLevel());
+$a->addBinder('constructed', new Binder());
+var_dump($a->getInstance('constructed'));
+</code>
+<code>
+string(11) "constructed"
+</code>
+
+The example above demonstrates this approach by using the dummy  
Binder class which implements Horde_Injector_Binder. Once  
getInstance('constructed') is called on the injector object it will
+determine that there is no concrete instance for 'constructed' yet.  
It then looks for any binders that might be capable of creating  
'constructed' and calls the create() function of such a binder.
+
+Here the binder is simple again and does not even return an object  
but a simple string. It also makes no use of the Horde_Injector  
instance delivered as argument to the create() function. Usually the  
provided injector will be used to retrieve any missing dependencies  
for the instance to be created.
+
++++ Default Binders
+
+Horde_Injector comes with two default Binder implementations so that  
you don't have to define your own binders.
+
+Lets look at the factory binder first:
+
+<code type="php">
+class Greet
+{
+    public function __construct($somebody)
+    {
+        $this->somebody = $somebody;
+    }
+
+    public function greet()
+    {
+        print 'Hello ' . $this->somebody;
+    }
+}
+
+class Factory
+{
+    static public function getGreeter(Horde_Injector $injector)
+    {
+        return new Greet($injector->getInstance('Person'));
+    }
+}
+
+$a = new Horde_Injector(new Horde_Injector_TopLevel());
+$a->setInstance('Person', 'Bob');
+$a->bindFactory('Greet', 'Factory', 'getGreeter');
+$a->getInstance('Greet')->greet();
+</code>
+<code>
+Hello Bob
+</code>
+
+This time the Factory in the example above really pulls a dependency:  
a person. We explicitly registered the string "Bob" with the injector  
and associated it with the interface name "Person".
+
+The Horde_Injector_Binder_Factory binder can be registered with the  
injector using the "bindFactory()" shortcut. It takes the interface  
name (here it is "Greet") and requires a class and a method name. This  
is assumed to be the factory creating the concrete instance.
+
+Once getInstance('Greet') is called the injector refers to the binder  
(as no concrete instance has been created yet). The binder delegates  
to the factory to actually create the object.
+
+The whole thing is also possible with a little bit more magic. The  
second approach implemented by Horde_Injector_Binder_Implementation  
requires type hinting to work:
+
+<code type="php">
+interface Person
+{
+    public function __toString();
+}
+
+class World implements Person
+{
+    public function __toString()
+    {
+        return 'World';
+    }
+}
+
+interface Greeter
+{
+    public function greet();
+}
+
+class Hello implements Greeter
+{
+    public function __construct(Person $somebody)
+    {
+        $this->somebody = $somebody;
+    }
+
+    public function greet()
+    {
+        print 'Hello ' . $this->somebody;
+    }
+}
+
+$a = new Horde_Injector(new Horde_Injector_TopLevel());
+$a->bindImplementation('Person', 'World');
+$a->bindImplementation('Greeter', 'Hello');
+$a->getInstance('Greeter')->greet();
+</code>
+<code>
+Hello World
+</code>
+
+The crucial part here is that the "Hello" class indicates in its  
constructor that it requires an object implementing the interface  
"Person". Horde_Injector is capable of detecting this via reflection.  
It will automatically search for the dependency and try to create an  
instance implementing this interface.
+
+In order for this to work we bind two classes to two interfaces:  
"World" to "Person" and "Hello" to "Greeter". Once the injector tries  
to create the "Greeter"-instance it will be able to fetch the required  
"Person" dependency by creating a "World" object.
+
+In case you remember that printing the little resulting string can be  
slightly easier while even using far less code: Dependency injection  
is meant for complex situations.
+
+Nevertheless the example hopefully demonstrates how to handle  
different implementation options using dependency injection: You may  
have different drivers that all fulfill a given interface. The  
Horde_Injector gives you an easy method to define which drivers you  
actually want to use without actually instantiating them. The concrete  
setup will only be build once you really need a concrete instance.
+
+++ Preparing a class for Horde_Injector
+
+Assume you have the following simple class that represents a common  
structure found in many of the Horde packages:
+
+<code type="php">
+class Horde_X
+{
+    /**
+     * Instance object.
+     *
+     * @var Horde_X
+     */
+    static protected $_instance;
+
+    /**
+     * Pointer to a DB instance.
+     *
+     * @var DB
+     */
+    protected $_db;
+
+    /**
+     * Attempts to return a reference to a concrete Horde_X instance.
+     *
+     * @return Horde_X  The concrete Horde_X reference.
+     * @throws Horde_Exception
+     */
+    static public function singleton()
+    {
+        if (!isset(self::$_instance)) {
+            self::$_instance = new Horde_X();
+        }
+
+        return self::$_instance;
+    }
+
+    /**
+     * Constructor.
+     */
+    public function __construct()
+    {
+        global $conf;
+
+        $this->_db = DB::connect($conf['sql']);
+    }
+}
+</code>
+
+The class obviously depends on a database connection. The constructor  
above does not allow for dependency injection as it constructs the  
database connection itself. It uses the global variable $conf in order  
to get the settings for this connection. A constructor allowing  
dependency injection would look like this:
+
+<code>
+    /**
+     * Constructor.
+     *
+     * @param DB $db A database connection.
+     */
+    public function __construct(DB $db)
+    {
+        $this->_db = $db;
+    }
+</code>
+
+Of course this connection must be provided from somewhere. The  
application using Horde_X might simply provide it when creating the  
Horde_X instance. If the application is however using a dependency  
injection framework then this framework would be required to provide  
the required database connection.
+
++++ Getting rid of singletons?
+
+From the viewpoint of dependency injection Horde_X can be used now as
+it allows external injection of its dependencies. We could throw away
+the singleton now. However there might be some reasons why we would
+like to keep the singleton() method. One of the reasons might be
+backward compatibility as some other classes or applications are bound
+to use the method. Another reason might be that we want to clarify how
+to get a functional instance of the class to somebody just looking at
+the Horde_X class.
+
+We could keep the following singleton method:
+
+     static public function singleton()
+     {
+         if (!isset(self::$_instance)) {
+             global $conf;
+
+             $db = DB::connect($conf['sql']);
+             self::$_instance = Horde_X($db);
+         }
+
+         return self::$_instance;
+     }
+
+
+Result
+======
+
+The final result that can be used with a dependency injection
+framework and still provides a backward compatible singleton method:
+
+ class Horde_X
+ {
+     /**
+      * Instance object.
+      *
+      * @var Horde_X
+      */
+     static protected $_instance;
+
+     /**
+      * Pointer to a DB instance.
+      *
+      * @var DB
+      */
+     protected $_db;
+
+     /**
+      * Attempts to return a reference to a concrete Horde_X instance.
+      *
+      * @return Horde_X  The concrete Horde_X reference.
+      */
+     static public function singleton()
+     {
+         if (!isset(self::$_instance)) {
+             global $conf;
+
+             $db = DB::connect($conf['sql']);
+             self::$_instance = Horde_X($db);
+         }
+
+         return self::$_instance;
+     }
+
+     /**
+      * Constructor.
+      *
+      * @param DB $db A database connection.
+      */
+     public function __construct(DB $db)
+     {
+         $this->_db = $db;
+     }
+ }
+
+
+

  ++ Dependency Injection Container FAQ

  +++ Where can {{Horde_Injector}} be used?



More information about the cvs mailing list