The Aura DI package provides a dependency injection container system with the following features:
native support for constructor- and setter-based injection
lazy-loading of services
inheritable configuration of setters and constructor params
When combined with factory classes, you can completely separate object configuration, object construction, and object usage, allowing for great flexibility and increased testability.
Fully describing the nature and benefits of dependency injection, while desirable, is beyond the scope of this document. For more information about “inversion of control” and “dependency injection” please consult http://martinfowler.com/articles/injection.html by Martin Fowler.
The Aura DI package comes with a instance script that returns a new DI instance:
Alternatively, you can add the Aura DI 'src/'
directory to your autoloader,
and then instantiate it yourself:
The Container
is the DI container proper. The support objects are:
a Config
object for collection, retrieval, and merging of setters and constructor params
a Forge
for object creation using the unified Config
values
We will not need to use the support objects directly; we will get access to
their behaviors through Container
methods.
For the following examples, we will set a service that should return a database connection. The hypothetical database connection class is defined as follows:
We will proceed from naive service creation to a more sophisticated idiom in four steps. Each of the variations is a valid use of the DI container with its own strengths and weaknesses.
In this variation, we create a service by instantiating an object with the
new
operator.
This causes the database object to be created at the time we set the service into the container. That means it is always created, even if we never retrieve it from the container.
In this variation, we create a service by wrapping it in a closure, still
using the new
operator.
This causes the database object to be created at the time we get the service
from the container, using $di->get('database')
. Wrapping the object
instantiation inside a closure allows for lazy-loading of the database object;
if we never make a call to $di->get('database')
, the object will never be
created.
In this variation, we will move away from using the new
operator, and use
the $di->newInstance()
method instead. We still wrap the instantiation in a
closure for lazy-loading.
The newInstance()
method uses the Forge
object to reflect on the
constructor method of the class to be instantiated. We can then pass
constructor parameters based on their names as an array of key-value pairs.
The order of the pairs does not matter; missing parameters will use the
defaults as defined by the class constructor.
In this variation, we define a configuration for the Database
class
separately from the lazy-load instantiation of the Database
object.
As part of the object-creation process, the Forge
examines the $di->params
values for the class being instantiated. Those values are merged with the
class constructor defaults at instantiation time, and passed to the
constructor (again, the order does not matter, only that the param key names
match the constructor param names).
At this point, we have successfully separated object configuration from object instantiation, and allow for lazy-loading of service objects from the container.
In this variation, we call the lazyNew()
method, which encapsulates the
“use a closure to return a new instance” idiom.
In this variation, we override the $di->params
values that will be used at
instantiation time.
The instantiation-time values take precedence over the configuration values, which themselves take precedence over the constructor defaults.
To get a service object from the container, call $di->get()
.
This will retrieve the service object from the container; if it was set using a closure, the closure will be invoked to create the object at that time. Once the object is created, it is retained in the container for future use; getting the same service multiple times will return the exact same object instance.
For the following examples, we will add an AbstractModel
class and two
concrete classes called BlogModel
and WikiModel
. The idea is that all
AbstractModel
classes need a Database
connection to interact with one or
more tables in the database.
We will create services for the BlogModel
and WikiModel
, and inject the
database service into them as part of the service definition. Using config
inheritance provided by the DI container, we can define the database service
injection through class configuration.
We do not need to set the value of the 'db'
param for the BlogModel
and
WikiModel
directly. Instead, the params for the AbstractModel
class are
automatically inherited by the child BlogModel
and WikiModel
classes, so
the 'db'
constructor param for all Model
classes automatically gets the
'database'
service. (We can override that at instantiation time if we like.)
Note the use of the lazyGet()
method. This is a special method intended for
use with params and setters. If we used $di->get()
, the container would
instantiate the service at that time. However, using $di->lazyGet()
allows
the service to be instantiated only when the object being configured is
instantiated. Think of it as a lazy-loading wrapper around the service (which
itself may be lazy-loaded).
We do not need to write our classes in any special way to get the benefit of
this configuration system. Any class with constructor params will be
recognized by the configuration system, so long as we instantiate it via
$di->newInstance()
or $di->lazyNew()
.
Creating a service for each of the model objects in our application can become tiresome. We may need to create other models, and we don’t want to have to create a separate service for each one. In addition, we may need to create model objects from within another object. Finally, we don’t want to create model objects until we actually need them. This is where we can make use of factories.
Below, we will define three new classes: a factory to create model objects for
us, an abstract PageController
class that uses the model factory, and a
BlogController
class that needs an instance of a blog model. We will
populate the ModelFactory
with a map of model names to factory objects that
will create the mapped objects.
Now we can set up the DI container as follows:
When we create an instance of the BlogController
and run it …
… a series of events occurs to fulfill all the dependencies in two steps.
The first step is the instantiation of the BlogController
:
The BlogController
instance inherits its params from PageController
The PageController
params get the 'model_factory'
service
The ModelFactory
params get the Database
object, creating the
database connection at that time
The second step is the invocation of ModelFactory::newInstance()
within
BlogController::exec()
:
BlogController::exec()
invokes ModelFactory::newInstance()
ModelFactory::newInstance()
creates a new class and passes in the
Database
object
At the end of all this, the BlogController::exec()
method has been able to
retrieve a fully-configured BlogModel
object without having to specify any
configuration locally.
Until this point, we have been working via constructor injection. However, we can work via setter injection as well.
Given the following example class …
… we can define values that should be injected via setter methods:
Note that we use lazyGet()
for the injection. As with constructor params, we
could tell the class to use a new Database
object instead of the shared one
in the Container
:
Setter configurations are inherited. If you have a class that extends
Example\Package\Foo
like so …
… you do not need to add a new setter value for it; the Forge
reads all
parent setters and applies them. (If you do add a setter value for that class,
it will override the parent setter.)
If we construct our dependencies properly with params, setters, services, and
factories, we will only need to get one object directly from DI container. All
object creation will then happen through the DI container via factory objects
and/or the Forge
object. We will never need to use the DI container itself
in any of the created objects.