Saturday, November 14, 2009

Using Symfony dependency injection Container with Zend_Bootstrap

I recently converted an old(er) Zend Framework based application to the new Zend_Application/Zend_Bootstrap style bootstrap approach. In doing so, I of course started down the path of creating components (resources in Zend speak) that are more easily used for dependency injection approaches. As I did so, I found myself writing a fair amount of code in subclasses of existing resource loaders to create the setup(s) I wanted from configuration files. Surely there had to be a better way.

I encountered the Symfony dependency injection container which looks very promising. For one it is compatible with Zend's notion of what a container should be. Zend doesn't formalize the interface to a container, because the interface only depends on "magic" methods. Yet the Symfony DI container is compatible. I found an article by Benjamin Eberlei that shows how to do this. There was one thing I did not like about the solution: it requires modification by an otherwise basic index.php file.

The reason for the modification is that you need to use the setContainer method on the bootstrap instance to inject the container into the bootstrap. A typical index.php will contain the following code:

What we will need now, however:

The need to create the container inside the index.php was not very flexible to me (I like to develop a general code base that I can use over and over, and not all projects might use this approach). So here is what I did about it.

It all starts by using a custom bootstrap class (subclass of Zend_Application_Bootstrap_BootstrapAbstract). I have such a class anyway that I use to do various other things. The bootstrap class calls a set method on any top level element it encounters in the configuration. I added these two methods to my class:

As you can see, setContainer (called from setOptions when a 'containerfactory' element is present) will store a factory instance in the bootstrap object, where its class was specified as the 'factory' option in the configuration ($options). We've overridden the standard version of 'getContainer'. If no factory was defined, it behaves as before, but if the factory was installed, it is used to create the container. This makes this new bootstrap class completely independent from the specifics of how to actually create the container.

Now suppose we feed 'setContainerFactory' the following configuration:

Everything inside the 'containerFactory' element is passed on to the constructor of the factory. To understand the meaning of the options, let's look at the factory class itself. For now, ignore the 'flattenParameters' and 'getParameters' methods. They'll be explained later.

So, let's have a look at this. The constructor, which receives the piece of the configuration as an array, stores the configuration ($options) for later, and initializes the autoloader for the symfony component so that, later on, its classes may be found automatically. When it comes time for the bootstrap to create the container it is going to call 'makeContainer'. While we could have simply hard-coded the creation there, I have chosen to make it more flexible, again.

First off we extract some parameters from the configuration. 'config_file' contains the path to the (top level) configuration for the DI container. This can be any file in XML, YAML, or INI format. If that were it, we would just use the appropriate method to load the configuration and be done with it. Parsing any of these files formats is relatively expensive and we do not want to have it happen for every single request we serve. Symfony provides a very neat mechanism to avoid that.

Symfony is able to write out a configuration, once read in, in a variety of formats, but in particular, it can do so as a php file containing a custom container class. This class will be named after the 'class' parameter of the configuration, or will be named 'Container' by default. If the 'dump_file' parameter is defined it contains the path where this source code will have been generated and if the file exists, we simply include/require it. The class will now be loaded, so we can call 'new' and be done. Very fast!

If the file does not (yet) exist, we will use appropriate calls the Symfony functions to read the configuration (all formats supported), and the dump it to the dump file (if configured). If no dump_file was configured things still work, but we'll load (slowly) from the configuration every request.

With this approach in place, simply adding a 'containerfactory' element to the bootstrap configuration and setting it up appropriately (and creating the corresponding factory class), you can now hook in any kind of DI container, not just the Symfony one.

So what about this 'getParameters' method? If you check out the documentation for the Symfony DI container, you will find that while you can specify parameters in the configuration, it is also possible to specify defaults for any subset of them by passing those defaults to the builder. That is exactly what we're doing here, but why?

In the rest of my infrastructure code, not discussed here, I have build a facility to have any piece of an configuration file (passed to the application object), refer to other pieces of the file and have values substituted. It allows me to define certain items, essentially as constants, once in the configuration file, and reuse them in different places. With the introduction of a DI container with its own configuration, I did not want to have to repeat these constants in its configuration, and that is what is achieved here. The '<parameters>' section refers (using the notation $(values) to a section in the overall configuration called <values> '. The whole 'values' array is flattened into a one-dimensional array, concatenating array keys with period in between to make the format consistent with that used by Symfony. The end result is that the parameters from the main application configuration file make it as defaults into the Symfony DI container that was generated.

That's it. Nothing else needs changing. You create your DI configuration in XML, YAML or INI format (or a mix using imports) and off you go.


marsbomber said...

Hi Dolf, is it possible to be a bit more specific on where the class file "class SF_Symfony_ContainerFactory" and the symfony di library should be placed in a project tree? I followed your post and having issues with loading the libraries ... i'm quite a novice in this kind of advanced topics ...

dolf said...

You can put them pretty much anywhere, provided your include path is setup properly.

Myself, I use Zend autoloading and so SF_Symfony_ContainerFactory could be in any path SF/Symfony/ContainerFactory.php that could be found via the include path. Of course you should feel free to name the class different altogether.

Same answer for the DI library.