Dependency Injection: Me likey

Code Naked

  • Homepage
  • About
  • Contact

Dec 29: Dependency Injection: Me likey

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 frameworksI 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 objectsAs 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...
Posted by Matthew Purdon in PHP Comments: (2) Trackbacks: (0)

Trackbacks
Trackback specific URI for this entry

No Trackbacks

Comments
Display comments as (Linear | Threaded)

#1 - Julian 2010-01-13 18:35 - (Reply)

This is cool, and it sounds good, but way over my head. Where the frig is the actual environment being set?

What does DI::container()->get() look like?

- Julian

#1.1 - Matthew Purdon 2010-01-14 09:09 - (Reply)

Hey Julian,

It is pretty heavy stuff. DI is basically a manager that maintains two internal objects. One is a Registry and the other is a builder. When you ask the Di to get you a particular service it looks in the Registry first and of it's not there it asks the Builder to build a new one which is then placed in the Registry.

You can see the source for the get method here:

http://github.com/mpurdon/Naked/blob/master/deploy/lib/Naked/DI.php#L168


Add Comment

Standard emoticons like :-) and ;-) are converted to images.
 
 

Subscribe

Archives

  • March 2010 (0)
  • February 2010 (1)
  • January 2010 (4)
  • December 2009 (6)
  • Recent...
  • Older...

Categories

  • XML Technology
  • XML Databases
  • XML MySQL
  • XML Software Development
  • XML Being a Contractor
  • XML Client Side
  • XML PHP
  • XML State of the Art


All categories

Blog Administration

Open login screen

Recommended Reading

Amazon.com: Patterns of Enterprise Application Architecture (0076092019909): Martin Fowler: Books
Amazon.com: The Nomadic Developer: Surviving and Thriving in the World of Technology Consulting (9780321606396): Aaron Erickson: Books
Amazon.com: Refactoring: Improving the Design of Existing Code (9780201485677): Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts: Books
Amazon.com: Domain-Driven Design: Tackling Complexity in the Heart of Software (0076092019565): Eric Evans: Books

Feedburner

Numeric Feedburner ID Required!




 

Layout by Andreas Viklund | Serendipity template by Carl