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:
Container is the DI container proper. The support objects are:
Config object for collection, retrieval, and merging of setters and constructor params
Forge for object creation using the unified
We will not need to use the support objects directly; we will get access to
their behaviors through
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
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
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
In this variation, we will move away from using the
new operator, and use
$di->newInstance() method instead. We still wrap the instantiation in a
closure for lazy-loading.
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
separately from the lazy-load instantiation of the
As part of the object-creation process, the
Forge examines the
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
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
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
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
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
WikiModel directly. Instead, the params for the
AbstractModel class are
automatically inherited by the child
WikiModel classes, so
'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
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
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
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 instance inherits its params from
PageController params get the
ModelFactory params get the
Database object, creating the
database connection at that time
The second step is the invocation of
ModelFactory::newInstance() creates a new class and passes in the
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
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
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
Forge object. We will never need to use the DI container itself
in any of the created objects.