In developing a number of prototypes for the ZF2 MVC, one common issue has presented itself: how do the various layers of the application – controllers, the view, etc – gain access to resources and services they need in order to operate (or to ensure consistent configuration)?
ZF1 has "solved" this with the Bootstrap object, which is injected into the front controller, and then the individual controllers. Action controllers then can pull resources as follows:
While this approach works, it breaks in other areas: plugins, action helpers, and view helpers end up needing to access the front controller singleton in order to gain access to the bootstrap – which is not an ideal situation.
When looking at prototypes, the issue has again raised itself. Consider the following controller:
Where is $service defined? We have a few options. In ZF1, you'd typically use the init() or preDispatch() methods:
This isn't terribly great, however, as you likely want to pass in some configuration. To do that, we need to grab the bootstrap, get the appropriate options, and pass them in. And what if we want to test, and mock the service?
So, the better approach is to write mutators and accessors:
This approach has the benefit that we can inject a service object when testing, and also lazy-load the service only when an action needs it. We're still left with an essential problem: how do we get the configuration?
Essentially, the question then, is: within our application code, such as controllers, how do we gain access to required, configurable resources, such as data access components, service clients, etc.?
There are two standard answers to these problems: Service Locators and Dependency Injection.
In the case of Service Locators, we configure our services up front and aggregate them in the locator object (or write a locator that defines and lazy-loads the services on demand). This object is then injected into the layers that need them, and services are pulled out. The ZF1 Bootstrap object is essentially a service locator; the only difference now is formalizing usage of a service locator.
Dependency Injection Containers are a special form of Service Locator that allow you to define what dependencies each class or service has, and ensure that these dependencies are injected. Thus, when you retrieve an object from the DI container, you can ensure that it has all the dependencies it needs. Using this approach, we could either pass a DI container to controllers – or simply retrieve controllers from the DI container, ensuring they have all dependencies immediately upon usage.
We hereby propose inclusion of components offering Service Locator and Dependency Injection functionality.
Service Locators: Overview
A Service Locator is incredibly simple: it's simply a registry for objects. Objects are registered with the Locator, and given a common, short name by which they will be referenced. The Service Locator is then injected or consulted in order to retrieve objects.
To illustrate:
In some implementations, explicit methods for each service will exist:
These have the benefit of code assist and documentation, as they are explicit code. Typically, a Service Locator implementation will support both explicit setter and getter methods, as well as generic "get" and "set" methods as illustrated earlier.
Service Locators may be static, but most implementations, particularly in PHP, focus on per-instance locators that are then injected into objects that need them.
As an example of using a locator within an object:
Service Locators are particularly useful in application paradigms (versus libraries) as they allow configuration of the application in the application bootstrap, and then injection of the service locator into the objects that need them (controllers, views, etc.).
The chief drawback is that they often require a fair amount of explicit coding – either extending the base ServiceLocator class, or configuring and seeding it in the application bootstrap.
Dependency Injection: Overview
Dependency Injection is really quite simple. You design your class in such a way that dependencies are injected either in the constructor or via setters; often these will be paired with lazy-loading so that if nothing is injected, sane defaults are used. The Service Locator example already shows this to a degree – the Service Locator is injected when the FooController class is instantiated.
As a more complete example, the original bad example could be refactored as follows:
In order to use this class, then, you need to pass in valid arguments to the constructor, and optionally setters:
This solves several problems, but introduces a new one: where and when does injection happen? Often, such objects are created within the application architecture, and we have no control over it.
As such, a common solution is to combine Dependency Injection with a Service Locator. When a request is made for a service, if a service definition has been created, it is instantiated and returned.
A service definition typically consists of several things:
- The actual class name being referenced.
- Constructor arguments (if any), and the values you wish to use (if any). These may be references to other services.
- Optionally, a list of setters or other methods you wish to inject. In the above code, setDb and setService are considered setters, and the definition might define these methods so that they may be injected with values from the container.
As such, the typical setup consists of:
- One or more Definitions (configuration), which are then passed to...
- A Dependency Injection manager, which then seeds...
- A Service Locator.
The benefit of tying a Service Locator to a Dependency Injection manager is that you wire all your dependencies together. For instance, if you have an EntryService that consumes a data access object, which in turn consumes a specific database connection, you might do the following:
Requirements
- MUST contain separate interfaces for:
- Service Locators
- Dependency Injection managers
- Rationale: In most cases, a service locator is all consuming code really needs to utilize. As an example, a controller or view object might receive the service locator as an argument, and pull services from it as needed. The dependency injection aspects may be assumed, or it may be assumed the service objects are fully configured when placed in the container. Additionally, configuration of the DI definitions is usually done once, at application initialization; typehinting on the DI aspects then becomes a moot point.
- Service Locator
- MUST allow injecting objects using short names
- COULD allow registering callbacks/closures that return an object
- MUST allow retrieving objects using the short names provided at registration
- SHOULD allow passing an array of constructor arguments that will be used (these would be passed on to the underlying DI container, if any)
- If the solution allows registering callbacks/closures, the
return value of that operation would be returned
- SHOULD provide a DI-enabled implementation
- MUST allow injecting objects using short names
- Dependency Injection
- MUST allow configurable service definitions
- Definitions:
- MUST specify the class
- MUST allow specifying named constructor arguments
- MUST allow manually specifying argument order
- SHOULD allow using Reflection to determine argument order
- SHOULD allow caching the definition such that Reflected arguments return an argument name => order map
- MUST allow specifying setters and other methods
- MUST allow specifying all arguments to these methods
- COULD allow specifying named arguments
- MUST allow specifying whether new instances should always be returned
- MUST default to using shared instances (i.e., maintaining a registry of named services)
- MUST allow referencing other services for purposes of DI
- When instantiating or calling the method receiving the argument, the service will then be retrieved from the container.
- COULD allow specifying "tags"
- All services "tagged" could be retrieved and iterated
- Definitions:
- COULD allow specifying aliases
- When an alias is encountered, the service it references would be returned instead
- MUST provide a method for retrieving object instances
- The method MUST accept a class name as the first parameter
- If the class name does not match a definition, the container would simply return a new instance of that class
- The method SHOULD allow specifying an associative array of arguments
- These arguments would be merged with any constructor arguments, and override those in any definitions.
- SHOULD allow specifying method arguments as well
- For any referenced services, the manager MUST retrieve the given service and inject according to the Definition.
- The method MUST accept a class name as the first parameter
- MUST allow configurable service definitions
Interfaces
- Service Locator
- Dependency Injection Manager
- Service Definition
- Method Collection
- Method Definition
- Reference
- DI-enabled
Implementations
In addition to the above interfaces, ZF2 would provide standard implementations of each. In particular, a DI-enabled Service Locator implemenation would be provided; the DI container would allow seeding of definitions via configuration.
The Service Locator implementation would also provide capabilities for extending the implementation to provide explicit getter methods for services, and the ability to map services to these methods.
Usage
- Basic service locator:
- Extending the service locator:
- DI-enabled service locator:
- DI definitions.
- Assume the following definitions
- Assume the following definitions
MVC Usage
This proposal began with a discussion of the MVC use case. The following examples show different ways that Service Locators or DI Containers could be used to solve a common issue found in MVC applications: how do the various layers of the application receive their dependencies?
Two solutions present themselves. In the first case, the Front Controller could compose a Service Locator instance, and pass it to the constructor of controllers it instantiates. This would allow each controller to pull dependencies and pass them into various service objects, view objects, and domain entities.
In the second case, controllers could define required components via constructor arguments or setters, and the front controller would then retrieve the controllers via a DI container, ensuring that all dependencies are injected.
In the case of composing a Service Locator, the convention would be that controllers would optionally accept a Service Locator in their constructor, or via a setter. In the case of retrieving controllers via a DI container, we would recommend that controllers have a setter for the DI container itself, so that they may retrieve other controllers and ensure they have all dependencies satisfied.
References
A prototype implementation using the interfaces listed in this proposal has been created:
The MVC prototype I was working off of that spurred this proposal:
Some literature on Service Locators and Dependency Injection:
- http://en.wikipedia.org/wiki/Hollywood_principle
- http://martinfowler.com/articles/injection.html
- Symfony 2 Dependency Injection: http://components.symfony-project.org/dependency-injection/
- Aura PHP DI: https://github.com/auraphp/aura.di
- Lithium talk from tek-x: http://www.slideshare.net/jperras/tekx-a-framework-for-people-who-hate-frameworks-lithium
- A bunch of Java and .NET stuff I really don't want to link to
3 Comments
comments.show.hideMar 09, 2011
Wil Moore III (wilmoore)
Looks extremely straight-forward. Nice work.
Mar 13, 2011
Keith Pope
It would be good to see how complex the DIC configuration of the framework would become using this approach, hopefully we wouldn't end up with a huge configuration that was hard to track/learn.
Also how would cyclic dependencies be handled and the size of the object graph? Is this proposal for framework level DI/SL or userland as well?
Mar 18, 2011
osebboy
Great work.
I was wondering... if this is implemented in ZF2, what would be the difference between ZF2 and Symfony2?