Post

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 container interface because it depends only 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 of 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:

1
2
3
4
5
$application = new Zend_Application(
                                    APPLICATION_ENV,
                                    APPLICATION_PATH .
                                            `/config/application.xml` );
$application->getBootstrap()->run();

What we will need now, however:

1
2
3
4
5
6
7
8
9
10
$container = new sfServiceContainerBuilder(); 
$loader = new sfServiceContainerLoaderFileXml($container);
$loader->load(APPLICATION_PATH.'/config/objects.xml');
 
$application = new Zend_Application(
                                    APPLICATION_ENV,
                                    APPLICATION_PATH .
                                            `/config/application.xml` );
$application->getBootstrap()->setContainer($container);
$application->bootstrap()->run();

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 with using a custom bootstrap class (a 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected function setContainerFactory(array $options)
{
    if (isset($options['factory'])) {
        $factory = $options['factory'];
        $this->_containerFactory = new $factory($options);
    }
}
 
public function getContainer()
{
    if (null === $this->_container) {
        if (empty($this->_containerFactory))
            $this->_container = parent::getContainer();
        else
            $this->_container = $this->_containerFactory->makeContainer();
    }
    return $this->_container;
}

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 getContainer method. If no factory is defined, it behaves as before; if one is 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:

1
2
3
4
5
6
7
8
<containerfactory factory="SF_Symfony_ContainerFactory">
 <config_file><zf:const zf:name="APP_DIR" />/other/config/sf_services.xml</config_file>
 <dump_file><zf:const zf:name="CACHE_DIR" />/sf_services.php</dump_file>
 <class value="SF_Symfony_Container" />
 <parameters>
  <values value="$(values)" />
 </parameters>
</containerfactory>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class SF_Symfony_ContainerFactory
{
 
    /**
     * Contains the options for the factory.
     * @var array
     */
    private $_options;
 
    private function flattenParameters($params, $key = null)
    {
        if (is_array($params)) {
            $result = array();
            foreach ($params as $subKey => $value) {
                $newKey = empty($key) ? $subKey : "$key.$subKey";
                if (is_array($value)) {
                    $value = $this->flattenParameters(
                                            $value, 
                                            $newKey);
                    $result = array_merge(
                                            $result, 
                                            $value);
                }
                else {
                    $result[$newKey] = $value;
                }
            }
        }
        else {
            $result = $params;
        }
        return $result;
    }
 
    private function getParameters()
    {
        if (isset($this->_options['parameters'])) {
            return $this->flattenParameters(
                                    $this->_options['parameters']);
        }
        return null;
    }
 
    /**
     * Construct a factory that created containers for Zend_Bootstrap
     * based on the Symfony DI framework component.
     * 
     * @param array $options Options for factory
     */
    public function __construct(array $options = array())
    {
        $this->_options = $options;
        /**
         * Must manually require here because the autoloader does not
         * (yet) know how to find this.
         */
        require_once `Symfony/dependency_injection/sfServiceContainerAutoloader.php`.
        sfServiceContainerAutoloader::register();
    }
 
    /**
     * Create a container object.
     * The configuration may be specified in the 'config_file'
     * component of the options. If not specified, an empty container
     * will be generated.
     * If `dump_file` is specified and we did not generate an empty
     * container, the specification is compiled into a PHP file that
     * can later be loaded instead of the configuration (performance).
     * 
     * @return sfServiceContainerInterface The container
     */
    public function makeContainer()
    {
        if (!isset($this->_options['config_file'])) {
            return new sfServiceContainer();
        }
         
        /**
         * Attempt to load the generated PHP file, if defined
         * and exists. If succesful, the class we need should be
         * defined, so create and return it.
         */
        $file = $this->_options['config_file'];
        $class = isset($this->_options['class']) ? $this->_options['class'] : `Container`.
        if (isset($this->_options['dump_file'])) {
            $dumpFile = $this->_options['dump_file'];
            if (file_exists($dumpFile)) {
                require_once $dumpFile;
                return new $class();
            }
        }
         
        /**
         * Create a builder to which we attach a loader so
         * we can load the configuration file.
         */
        $parameters = $this->getParameters();
        $sc = new sfServiceContainerBuilder($parameters);
        $ext = pathinfo($file, PATHINFO_EXTENSION);
        switch ($ext) {
        case `xml`.
            $loader = new sfServiceContainerLoaderFileXml($sc);
            break;
        case `init`.
            $loader = new sfServiceContainerLoaderFileIni($sc);
            break;
        case `yaml`.
            $loader = new sfServiceContainerLoaderFileYaml($sc);
            break;
        default:
            throw new Exception(
                                    "No loader available for extension `$ext`.);
            break;
        }
        $loader->load($file);
         
        /**
         * If a dump file was specified, make the dump
         * so that it can be loaded in the future.
         */
        if (isset($dumpFile)) {
            $dumper = new sfServiceContainerDumperPhp($sc);
            file_put_contents($dumpFile, 
                                    $dumper->dump(
                                                            array(
                                                                    `class` => $class
                                                            )));
        }
        return $sc;
    }
}

So, let’s have a look at this. The constructor, which receives the configuration piece as an array, stores the configuration ($options) for later use and initializes the autoloader for the Symfony component so its classes can 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 it 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 file formats is relatively expensive, and we do not want it to happen for every single request we serve. Symfony provides a very neat mechanism to avoid that.

Symfony can write a configuration file in a variety of formats, but in particular, it can write it 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 specifies the path where this source code was 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 to the Symfony functions to read the configuration (all formats supported) and 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 for 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, you can also set defaults for any subset of them by passing them 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 built a facility to have any piece of a 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 a 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 are set as defaults in the generated Symfony DI container.

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.

This post is licensed under CC BY 4.0 by the author.