- A Very Brief Introduction to DI
- A Very Brief Introduction to DI Containers
- Simplest Usage Case (2 classes, one consumes the other)
- Simplest Usage Case Without Type-Hints
- Simplest Usage Case With Compiled Definitions
- Creating A Precompiled Definition For Others To Use
- Using Multiple Definitions From Multiple Sources
- Generating Service Locators
|Available since 2.0.0dev3|
Please note that the DI component has only been available starting in 2.0.0dev3 and forward. Also, please note that the component is still under review, and the API and functionality offered may change before the first beta release.
Dependency Injection is a concept that has been talked about in numerous places over the web. For the purposes of this quickstart, we'll explain the act of injecting dependencies simply with this below code:
Above, A is a dependency of B, and A was injected into B. If you are not familar with the concept of dependency injection, here are a couple of great reads: Matthew Weier O'Phinney's Analogy, Ralph Schindler's Learning DI, or Fabien Potencier's Series on DI.
In the simplest use case, a developer might have one class (A) that is consumed by another class (B) through the constructor. By having the dependency injected through the constructor, this requires an object of type A be instantiated before an object of type B so that A can be injected into B.
To create B by hand, a developer would follow this work flow, or a similar workflow to this:
If this workflow becomes repeated throughout your application multiple times, this creates an opportunity where one might want to DRY up the code. While there are several ways to do this, using a dependency injection container is one of these solutions. With Zend's dependency injection container Zend\Di\DependencyInjector, the above use case can be taken care of with no configuration (provided all of your autoloading is already configured properly) with the following usage:
Moreover, by using the DependencyInjector::get() method, you are ensuring that the same exact object is returned on subsequent calls. To force new objects to be created on each and every request, one would use the DependencyInjector::newInstance() method:
Let's assume for a moment that A requires some configuration before it can be created. Our previous use case is expanded to this (we'll throw a 3rd class in for good measure):
With the above, we need to ensure that our DependencyInjector is capable of seeing the A class with a few configuration values (which are generally scalar in nature). To do this, we need to interact with the InstanceManager:
Now that our container has values it can use when creating A, and our new goal is to have a C object that consumes B and in turn consumes A, the usage scenario is still the same:
Simple enough, but what if we wanted to pass in these parameters at call time? Assuming a default DependencyInjector object ($di = new Zend\Di\DependencyInjector() without any configuration to the InstanceManager), we could do the following:
Constructor injection is not the only supported type of injection. The other most popular method of injection is also supported: setter injection. Setter injection allows one to have a usage scenario that is the same as our previous example with the exception, for example, of our B class now looking like this:
Since the method is prefixed with set, and is followed by a capital letter, the DependencyInjector knows that this method is used for setter injection, and again, the use case $c = $di->get('C'), will once again know how to fill the dependencies when needed to create an object of type C.
Other methods are being created to determine what the wirings between classes are, such as interface injection and annotation based injection.
If your code does not have type-hints or you are using 3rd party code that does not have type-hints but does practice dependency injection, you can still use the DependencyInjector, but you might find you need to describe your dependencies explicitly. To do this, you will need to interact with one of the definitions that is capable of letting a developer describe, with objects, the map between classes. This particular definition is called the BuilderDefinition and can work with, or in place of, the default RuntimeDefinition.
Definitions are a part of the DependencyInjector that attempt to describe the relationship between classes so that DependencyInjector::newInstance() and DependencyInjector::get() can know what the dependencies are that need to be filled for a particular class/object. With no configuration, DependencyInjector will use the RuntimeDefinition which uses reflection and the type-hints in your code to determine the dependency map. Without type-hints, it will assume that all dependencies are scalar or required configuration parameters.
The BuilderDefinition, which can be used in tandem with the RuntimeDefinition (technically, it can be used in tandem with any definition by way of the AggregateDefinition), allows you to programmatically describe the mappings with objects. Let's say for example, our above A/B/C usage scenario, were altered such that class B now looks like this:
You'll notice the only change is that setA now does not include any type-hinting information.
This above usage scenario provides that whatever the code looks like, you can ensure that it works with the dependency injection container. In an ideal world, all of your code would have the proper type hinting and/or would be using a mapping strategy that reduces the amount of bootstrapping work that needs to be done in order to have a full definition that is capable of instantiating all of the objects you might require.
Without going into the gritty details, as you might expect, PHP at its core is not DI friendly. Out-of-the-box, the DependencyInjector uses a RuntimeDefinition which does all class map resolution via PHP's Reflection extension. Couple that with the fact that PHP does not have a true application layer capable of storing objects in-memory between requests, and you get a recipe that is less performant than similar solutions you'll find in Java and .Net (where there is an application layer with in-memory object storage.)
To mitigate this shortcoming, Zend\Di has several features built in capable of pre-compiling the most expensive tasks that surround dependency injection. It is worth noting that the RuntimeDefition, which is used by default, is the only definition that does lookups on-demand. The rest of the Definition objects are capable of being aggregated and stored to disk in a very performant way.
Ideally, 3rd party code will ship with a pre-compiled Definition that will describe the various relationships and parameter/property needs of each class that is to be instantiated. This Definition would have been built as part of some deployment or packaging task by this 3rd party. When this is not the case, you can create these Definitions via any of the Definition types provided with the exception of the RuntimeDefinition. Here is a breakdown of the job of each definition type:
- AggregateDefinition - Aggregates multiple definitions of various types. When looking for a class, it looks it up in the order the definitions were provided to this aggregate.
- ArrayDefinition - This definition takes an array of information and exposes it via the interface provided by Zend\Di\Definition suitable for usage by DependencyInjector or an AggregateDefinition
- BuilderDefinition - Creates a definition based on an object graph consisting of various Builder\PhpClass objects and Builder\InectionMethod objects that describe the mapping needs of the target codebase and …
- Compiler - This is not actually a definition, but produces an ArrayDefinition based off of a code scanner (Zend\Code\Scanner\DirectoryScanner or Zend\Code\Scanner\FileScanner).
The following is an example of producing a definition via a DirectoryScanner:
This definition can then be directly used by the DependencyInjector (assuming the above A, B, C scenario was actually a file per class on disk):
One strategy for persisting these compiled definitions would be the following:
Since Zend\Code\Scanner does not include files, the classes contained within are not loaded into memory. Instead, Zend\Code\Scanner uses tokenization to determine the structure of your files. This makes this suitable to use this solution during development and within the same request as any one of your application's dispatched actions.
If you are a 3rd party code developer, it makes sense to produce a Definition file that describes your code so that others can utilize this Definition without having to Reflect it via the RuntimeDefintion, or create it via the Compiler. To do this, use the same technique as above. Instead of writing the resulting array to disk, you would write the information into a definition directly, by way of Zend\CodeGenerator:
In all actuality, you will be using code from multiple places, some Zend Framework code, some other 3rd party code, and of course, your own code that makes up your application. Here is a method for consuming definitions from multiple places:
In production, you want things to be as fast as possible. The Dependency Injection Container, while engineered for speed, still must do a fair bit of work resolving parameters and dependencies at runtime. What if you could speed things up and remove those lookups?
The Zend\Di\ServiceLocator\Generator component can do just that. It takes a configured DI instance, and generates a service locator class for you from it. That class will manage instances for you, as well as provide hard-coded, lazy-loading instantiation of instances.
The method getCodeGenerator() returns an instance of Zend\CodeGenerator\Php\PhpFile, from which you can then write a class file with the new Service Locator. Methods on the Generator class allow you to specify the namespace and class for the generated Service Locator.
As an example, consider the following:
The above code will write to ../Application/Context.php, and that file will contain the class Application\Context. That file might look like the following:
To use this class, you simply consume it as you would a DI container:
One note about this functionality in its current incarnation. Configuration is per-environment only at this time. This means that you will need to generate a container per execution environment. Our recommendation is that you do so, and then in your environment, specify the container class to use.