Dice - PHP Dependency Injection Container

Introduction

Last updated 28/02/2014

Dice is a minimalist Inversion of Control (IoC) container (Often called a Dependency Injection Container) for PHP.

Dice allows developers to move object creation logic out of their application logic. This has many advantages when it comes to OOP theory,
but also makes the developers life easier. It tries to be as minimal and unobtrusive as possible.
Everything is done to minimise and simplify application code, making the application developers job easier
and making the learning curve as shallow as possible.

Introduction

Dice is a minimalist Inversion of Control (IoC) container (Often called a Dependency Injection Container) for PHP.

Dice allows developers to move object creation logic out of their application logic. This has many advantages:

1) It makes your life as a developer easier!

Imagine this:

PHP Code:

$a = new A(new B, new C, new D(new E, new F));

With Dice and zero configuration, this can be expressed as:

PHP Code:

$dice = new \Dice\Dice;
$a = $dice->create('A');

All the dependencies (And dependencies of those dependencies) are automatically resolved.

And if you add a class to the system? Imagine you want to add a class to an existing project which has a dependency on your existing PDO shared
dependency. With Dice, you simply define the class with the dependency:

PHP Code:

class MyClass {
public function __construct(PDO $pdo) {
}
}

If PDO is already in use elsewhere in the project, this is all you need to do to have PDO passed to MyClass' constructor!
You don't need to configure Dice at all or tell it anything about what dependencies MyClass has!

2) Improved maintainability.

Take the example above. If the definition of the class C is modified during the development
lifecycles and now has a dependency on a class called X,
instead of finding everywhere that C is created and passing an instance of X to it,
this is handled automatically by the IoC Container.
You can add dependencies to any class any any time during development and only have to
alter the class definition without needing to scout your codebase for "new" keywords to add the dependencies
or even reconfigure Dice!

3) Avoid "couriers"

Only objects which use the dependency will have it. It's very easy to accidentally use
the courier anti-pattern
when using Dependency Injection.

4) Higher flexibility

By using Dependency Injection, your application isn't hardcoded to a particular instance.

Consider this:

PHP Code:

With this code, it's impossible to use a subclass of B in place of the instance of B. A is very tightly coupled to its dependencies. With dependency injection, any of the components can be substituted:

PHP Code:

Here, B could be any subclass of B configured in any way possible. To get technical, this allows for a far greater Separation of Concerns because A never has to worry about configuring its dependencies, they're given to it in a state that is ready to use. By giving less responsibility to the class, flexibility is greatly enhanced because A can be reused with any variation of B, C and D instances.

5) You don't have to worry about locating dependencies or changing constructor arguments ever again.

Forget service locators, registries and 99% of factories. By using Dice you can change the constructor parameters adding/removing dependencies on a whim without worrying about reconfiguring Dice or
side-effects throughout your code. This enables far better encapsulation, objects will never know or need to know what dependencies other objects have! Because object creation is all abstracted to the IoC container,
and Dice doesn't need reconfiguring each time you alter a constructor or add a class, making changes is incredibly easy!

Once Dice is up and running and handling your application's dependencies, you can simply add a class to the system and it will just work
without even telling Dice anything about it. Just add:

PHP Code:

class B {
public function __construct(PDO $pdo, C $c) {
}
}

And require it in one of your existing classes:

PHP Code:

class ExistingA {
public function __construct(B $b) {
}
}

And it will just work without any additional configuration! You don't need to worry that you've changed
an existing class' constructor as it will automatically be resolved and you don't need to worry about locating
or configuring the dependencies that the new class needs!

PHP Code:

The dependency of B is automatically resolved and the string in the second parameter is passed as the second argument. You can
pass any number of additional constructor arguments using the second argument as an array to $dice->create();

Please note: It is preferable to do this using a rule and constructParams. See the section on rules for more information.

2. Shared dependencies

By far the most common real-world usage of Dependency Injection is to enable a single instance of
an object to be accessible to different parts of the application. For example, Database objects and
locale configuration are common candidates for this purpose.

Dice makes it possible to create an object that is shared throughout the application.
Anything which would traditionally be a global variable, a singleton, accessible statically
or accessed through a Service Locator / Repository is considered a shared object.

Any class constructor which asks for an instance of a class that has been marked as shared will be passed
the shared instance of the object rather than a new instance.

2.1 Using rules to configure shared dependencies

The method of defining shared objects is by Rules. See the section on Rules below for more information. They are used to configure the container. Here's how a shared object is defined using a rule.

This example uses PDO as this is a very common use-case.

PHP Code:

//create a rule to apply to shared object
$rule = new \Dice\Rule;
$rule->shared = true;
//Apply the rule to instances of PDO
$dice->addRule('PDO', $rule);
//Now any time PDO is requested from Dice, the same instance will be returned
$pdo = $dice->create('PDO');
$pdo2 = $dice->create('PDO');
var_dump($pdo === $pdo2); //TRUE
//And any class which asks for an instance of PDO will be given the same instance:
class MyClass {
public $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
}
$myobj = $dice->create('MyClass');
var_dump($pdo === $myobj->pdo); //TRUE

Here, both instances of PDO would be the same. However, because this
is likely to be the most commonly referenced piece of code on this page,
to make this example complete, the PDO constructor
would need to be configured as well:

PHP Code:

//Firstly create a rule
$rule = new \Dice\Rule;
$rule->shared = true;
//PDO will be constructed by the container with these variables supplied to PDO::__construct
$rule->constructParams = ['mysql:host=127.0.0.1;dbname=mydb', 'username', 'password'];
//Apply the rule to the PDO class
$dice->addRule('PDO', $rule);
//Now any time PDO is requested from Dice, the same instance will be returned
$pdo = $dice->create('PDO');
$pdo2 = $dice->create('PDO');
var_dump($pdo === $pdo2); //TRUE
//And any class which asks for an instance of PDO will be given the same instance:
class MyClass {
public $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
}
class MyOtherClass {
public $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
}
$myobj = $dice->create('MyClass');
$myotherobj = $dice->create('MyOtherClass');
//When constructed, both objects will have been passed the same instance of PDO
var_dump($myotherobj->pdo === $myobj->pdo); //TRUE

The constructParams rule has been added to ensure that every time
an instance of PDO is created, it's given a set of constructor arguments.
See the section on constructParams
for more information.

3. Configuring the container with Dice Rules

In order to allow complete flexibility, the container can be fully configured using rules provided by instances of the
\Dice\Rule class.
Rules are passed to the container using:

$newInstances (array) - A list of dependencies which will always be passed as new instances, ignoring $shared. View Example

$call (multidimensional array) - A list of methods and their arguments which will be called after the object has been constructed. View Example

$instanceOf (string) - The name of the class to initiate. Used when the class name is not passed to $dice->addRule(). View Example

$shareInstances (array) - A list of class names that will be shared throughout a single object tree. View Example

3.1 Substitutions

When constructor arguments are type hinted using interfaces or to enable polymorpsim, the container needs
to know exactly what it's going to pass. Consider the following class:

PHP Code:

class A {
public function __construct(Iterator $iterator) {
}
}

Clearly, an instance of "Iterator" cannot be used because it's an interface. If you wanted to pass an instance of B:

PHP Code:

class B implements Iterator {
//...
}

The rule can be defined like this:

PHP Code:

$rule = new \Dice\Rule;
//When a constructor asks for an instance of Iterator pass it an instance of B instead
$rule->substitutions['Iterator'] = new \Dice\Instance('B');
$dice->addRule('A', $rule);
$a = $dice->create('A');

The DiceInstance class is used. This tells the DIC to create an instance of 'B' in place of 'Iterator'.
new \Dice\Instance('B') can be read as 'An instance of B created by the injection container'.

The reason that $rule->substitutions['Iterator'] = $dice->create('B') is not used is that this creates a B object there and then.
Using DiceInstance means that an instance of B is only created at the time it's required.

However, what if If the application required this?

PHP Code:

$a = new A(new DirectoryIterator('/tmp'));

There are three ways this can be achieved using Dice.

1 Direct substitution, pass the fully constructed object to the rule:

PHP Code:

You can pass a closure into a \Dice\Instance and it will be called and the return value will be used as the substitution. Please note this is done just-in-time so will be called as the class it's been applied to is instantiated.

PHP Code:

3 Named instances. See the section on Named instances for a more detailed explanation of how this works.

PHP Code:

$namedDirectoryIteratorRule = new \Dice\Rule;
//An instance of the DirectoryIterator class will be created
$namedDirectoryIteratorRule->instanceOf = 'DirectoryIterator';
//When the DirectoryIterator is created, it will be passed the string '/tmp' as the constructor argument
$namedDirectoryIteratorRule->constructParams = ['/tmp'];
//Create a rule under the name "$MyDirectoryIterator" which can be referenced as a substitution for any other rule
$dice->addRule('$MyDirectoryIterator', $namedDirectoryIteratorRule);
$rule = new \Dice\Rule;
//This tells the DI Container to use the configuration for $MyDirectoryIterator when an Iterator is asked for in the constructor argument
$rule->substitutions['Iterator'] = new \Dice\Instance('$MyDirectoryIterator');
//Apply the rule to the A class
$dice->addRule('A', $rule);
//Now, when $a is created, it will be passed the Iterator configured as $MyDirectoryIterator
$a = $dice->create('A');

3.2 Inheritance

By default, all rules are applied to any child classes whose parent has a rule. For example:

3.5 Setter injection

Objects often need to be configured in ways that their constructor does not account for. For example:
PDO::setAttribute() may need to be called
to further configure PDO even after it's been constructed.

To account fo this, Dice Rules can supply a list of methods to call on an object after it's been constructed
as well as supply the arguments to those methods. This is achieved using $rule->call:

3.6 Default rules

Dice also allows for a rule to apply to any object it creates by setting the name to '*'. As it's
impossible to name a class '*' in php this will not cause any compatibility issues.

The default rule will apply to any object which isn't affected by another rule.

The primary use for this is to allow application-wide rules. This is useful for type-hinted arguments.
For example, you may want any class that takes a PDO object as a constructor argument to use a substituted
subclass you've created. For example:

PHP Code:

class MyPDO extends PDO {
//...
}

Dice allows you to pass a "MyPDO" object to any constructor that requires an instance of PDO
by adding a default rule:

The default rule is identical in functionality to all other rules. Objects could be set to shared by default, for instance.

3.7 Named instances

One of Dice's most powerful features is Named instances.
Named instances allow different configurations
of dependencies to be accessible within the application. This is useful when
not all your application logic needs to use the same configuration of a dependency.

For example, if you need to copy data from one database to another you'd need two database objects
configured differently. With named instances this is possible:

$dataCopier will now be created and passed an instance to each of the two databases.

Once a named instance has been defined, it can be referenced using new \Dice\Instance($name) by
other rules using the Dependency Injection Container in either substitutions or constructor parameters.

Named instances do not need to start with a dollar, however it is advisable to prefix them with a character
that is not valid in class names.

3.8 Sharing instances for a specific tree

In some cases, you may want to share a a single instance of a class between every class in one tree but if another instance
of the top level class is created, have a second instance of the tree.

For instance, imagine a MVC triad where the model needs to be shared between the controller and view, but if another
instance of the controller and view are created, they need a new instance of their model shared between them.

The best way to explain this is a practical demonstration:

PHP Code:

class A {
public $b, $c;
public function __construct(B $b, C $c) {
}
}
class B {
public $d;
public function __construct(D $d) {
$this->d = $d;
}
}
class C {
public $d;
public function __construct(D $d) {
$this->d = $d;
}
}
class D {}
By using $rule->shareInstances it's possible to mark D as shared within each instance of an object tree.
The important distinction between this and global shared objects is that this object is
only shared within a single instance of the object tree.

PHP Code:

$rule = new \Dice\Rule;
$rule->shareInstances = ['D'];
$dice->addRule('A', $rule);
//Create an A object
$a = $dice->create('A');
//Anywhere that asks for an instance D within the tree that existis within A will be given the same instance:
//Both the B and C objects within the tree will share an instance of D
var_dumb($a->b->d === $a->c->d); //TRUE
//However, create another instance of A and everything in this tree will get its own instance of D:
$a2 = $dice->create('A');
var_dumb($a2->b->d === $a2->c->d); //TRUE
var_dumb($a->b->d === $a2->b->d); //FALSE
var_dumb($a->c->d === $a2->c->d); //FALSE

4 Advanced features

4.1 Callbacks

Please note: This feature was removed in Dice 1.2 as there is no real use-case for the feature.
The original use-case was to allow external code to track injected dependencies to pass them back into Dice.
This functionality is now availalbe using $rule->shareInstances so this feature has been removed.

It can be useful for high level parts of the application to know exactly which dependencies an object has been passed as it's created.
Dice allows the developer to
get a list of all the dependencies which were given to an objects constructor. This is possible using the $callback parameter in $dice->create();

PHP Code:

The callback returns the exact parameters that have been passed to A following any rules
which have been supplied to the container.

4.2 Rule cascading

In most cases it's useful to extend current rules rather than completely overwrite them. For instance,
when adding an arbitrary rule it would be useful to inherit from the default rule instead of writing a new one for a given class. For example:

PHP Code:

All references to classnames in instances, substitutions and any other configuration option
must contain the full namespace information but no leading slash.

N.b. because the \ character is an escape character in strings it must be double escaped, which is why
these examples show Foo\\Bar instead of Foo\Bar

Defining DiceRules using XML

To enable a better separation of concerns as well as a cleaner method of configuration, Dice supports defining rules using XML.
Dice Rules can be completely configured using XML and has all the power (and more) that defining DiceRules in PHP does.

PHP Code:

Note: The XML Loader can take either a path to an XML file as a string or a SimpleXmlElement. This is by design, it allow applications
to potentially store Dice configuration along with other XML based application metadata in the same file. By not forcing application developers to
create a file specifically for Dice congiguration, it's able to offer greater flexibility.

Basic usage

Every rule can be defined using XML. Here is a complete XML file. With the exception of <name> every tag is optional.

{Config::dbServer} is evaluated as the value from $dice->create('Config')->dbServer; Allowing you to reference instances
of classes within the XML file. It follows existing rules, so where Config is marked as shared, it will read from a shared instance of config.

Probably not. Dice is designed to be minimal and lightweight. It loosely follows the X.org design principles; specifically Do not add new functionality unless an implementor cannot complete a real application without it..
However, if a feature is is genuinely useful and not repeating something which is already possible, please request it on github.

Features such as additional configuration syntaxes may be added but these will be optional and not part of Dice's core.

About the author

All content is by Tom Butler, a 28 year old Web Developer, Ph.D student and part time University Lecturer based in Milton Keynes, UK. Interests: Programming, best practices, PC Gaming, live music, gradually improving at Flying Trapeze.