So I spent some time over the holidays working on my proof of concept framework. One of the key features I added was something I have always been curious about but never really got around to playing with,
Dependency Injection. I thought the idea seemed to be powerful but I never really
grokked it until I sat down and started to implement my own DI container. The first thing I did was hit google and search for "PHP Dependency Injection". It turns out there are a lot of frameworks out there now that support some form of injection but I wanted to make sure I was taking the best ideas from all of them and rolling them into my own. I decided to start with Constructor Injection as it is the easiest to implement, but I also wanted to introduce the concept of a context. One of the things we have to contend with frequently in software development is the transition of our code from development environments to production environments, I wanted my framework to knit itself together automatically for whatever environment it was being deployed in. For example, when I am in development I want my logger to use the
FirePHP extension and have it log at the debug level. In production I want my logger to be completely silent for all but critical errors which are logged to a remote monitoring server. Most of the time I see this implemented in some form of Environment via an init call
As you can see, there are a lot of implementation details that the environment needs to know about in order to get logging together. Even worse is the fact that if you need to get at the logger in code other than in the Environment class itself you need to get it from a registry every time $logger = Zend_Registry::get('logger');
Although this works and is pretty much how every
Zend Framework application I have seen built is put together it can make for some unwieldy code. The problem is that rarely does an object live alone in the application. If you need to log stuff in your object, then you can use a registry to pull in the logger on demand and use it.
This seems okay, but what if the logger wasn't put into the registry yet? Or if it was set in the Environment but then destroyed by some other code? What you get now is a "fatal error call to method log on non-object" message. Not good. One way you can get around it is to make the logger part of the Foo class and pass it in manually during construction.
Now we can be sure that when we go to use the logger in the doSomethingThatRequiresLogging method, that we do have a valid logger since we couldn't even build a Foo without one. The problem is that every time you want a Foo, you need to also make sure you have a logger kicking around in order to pass it in. Using dependency injection all I have to do is this:
This allows the dependency injection container (DIC) to examine the Foo class for any annotations, focusing on the @Inject annotation. Oh, yeah I implemented annotations as well over the last few days. When the DIC sees that it should inject into the constructor it looks at the type hints and figures out what it needs. It then looks to see if it knows how to build those dependencies. If it doesn't have a specification or a factory function then it just tries to instantiate each one and then passes them into the constructor using ReflectionClass::newInstanceArgs(). The crazy part is that it can continue up the object graph building all of the dependencies as it needs them. You don't have to init anything in an Environment or pass things into a registry. You just specify the builder in the dependencies folder files and that's it. Here is a more complicated example of something that is typical in many frameworks
class Logger
{
/**
* Constructor
*/
public function __construct()
{}
}
abstract class Cache
{
/**
* Constructor
*
* @Inject
*/
public function __construct(\Naked\Configuration $configuration)
{
$this->configuration = $configuration;
}
}
class Memcached extends Cache
{}
class UnitOfWork
{
/**
* Constructor
*
* @Inject
*/
public function __construct(\Naked\Cache $cache, \Naked\Logger $logger)
{
$this->cache = $cache;
$this->logger = $logger;
}
}
abstract class DomainObject
{
/**
* Constructor
*
* @Inject
*/
public function __construct(\Naked\UnitOfWork $unitOfWork)
{
$this->unitOfWork = $unitOfWork;
}
}
class Car extends DomainObject
{
protected $driver;
protected $model;
public function setDriver(\models\Driver $driver)
{
$this->driver = $driver;
}
}
class BMW extends Car
{}
class Bugatti extends Car
{}
class Driver extends DomainObject
{
protected $name;
}
class Racer extends Driver
{}
class Commuter extends Driver
{}
I have chopped out the guts of the classes so that you can clearly see the dependencies between objects. Let's say in some controller action I wanted to get a car for racing. This is what the code might look like, but first we have to do some set up in our Environment:
Once we have the main objects that we are going to need in order to process the action method we can proceed. You may be wondering why I have two types of caching set up, one is for caching in the normal sense and the other is for shared memory caching in APC or Zend Cache. I use memcached to store db results and all of that junk and I use shared memory for things like caching the annotations for each class so I don't have to parse them for each request. Here's a question though, what if we aren't actually going to use any of this stuff? We have gone through all the work of instantiating this junk and it turns out the user hit a 404 action... That's a serious waste of effort in my opinion, after all we should be trying to keep in mind the
YAGNI principle as we code. Let's take a look at the action method to see how we end up using these objects
As you can see, we basically want to create a different type of Car based on the type that is passed in by the request. There is a fair amount of work involved in a seemingly simple task of getting a racing car driver for a racing car and a regular commuter for a regular car. With dependency injection, I can just create a new PHP file modules/
/configuration/dependencies/cars.php which contains the following codeThis file as well as any other PHP files in this directory is read in by the dependency injection builder and used to configure the container. Nothing is instantiated until it's actually needed, so if we are being sent to a 404 action there are no caches, no unit of work, and not even any logging. Better still, here is what my action looks likeIf the user has not specified a car type, we set things up using the commuting context. This grabs a Car, which we know we should build as a BMW for commuting. We don't have a driver for the commuting context though, but the container is smart enough to look in the default context for an object where it finds the driver Matthew Purdon. If the user chooses a racing car then using that context we build the Bugatti and use the racing context driver, Mario Andretti. Now, this example is a bit contrived in order to try to use the most features if the container in the smallest space so it may seem that we had to do a fair amount of work in cars.php in order to specify how the cars and drivers should be built. Bear in mind that we now have no initialization code in place in the Environment at all so we saved that tedium. More importantly, any time you need a car, no matter how many times, you simply ask for it using DI::container()->get('Car', $context); and it will show up ready to do work without you having to wire up its dependencies each time.
The really crazy part about using this stuff is just how little code there is in the guts of things. For example, you often want to have access to the environment, configuration and a request object. normally you'd have to pass all of that junk in and then dispatch the action method. With DI, you don't even have to think about it. In fact as long as @inject is in the abstract controller constructor you don't even need a build specification for the controllers since getting the controller through the DI will automatically inject any constructor parameters it knows how to build. Even cooler is that you can specify a context for the Request object so that you can pass in a different request object type based on the environment. Adding this to configuration/dependencies/core.phpWe can then add this small bit of code to the Dispatcher in order to have the controller be passed a request that knows how to extract parameters from argv for command line interfaces or one that knows about $_GET and $_POST for handling web requests.Talk about painless... If you want to use another type of request, it's a simple matter of changing the config. One other thing that I really like since I am using PHP 5.3.x is the fact that I can specify a lambda function as a factory method to build an object. I decided to do this so that I could use builders to get objects like the routes. Each module in my application can have it's own route specifications file located in /modules//configuration/routes.php. The route builder uses the Environment (which is injected into the constructor) to get the installed modules and their paths and spiders over them slurping in the route definition files and merges them into the routes composite. After the routes are built, we throw away the builder. Using the container we simply create a FactoryMethod object in /configuration/dependencies/core.php that looks like this:Now when the DIC needs to instantiate the routes for the first time, it executes the lambda function and uses the return value as the object we wanted to build. With constructor and setter injection, contexts and an easy way (ie not XML) to configure the container I feel that the dependency injection used in the Naked Framework has everything I need to get a lot of work done without the clutter of having to manage dependencies myself. Even better is the fact that my object models clearly indicate their dependencies and yet can be completely self-wired on the fly. Dependency Injection, I'm sold.
PS: This blog template is seriously ugly...