[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