Integrating Symfony Dependency Injection Service Container with Zend Framework

January 22, 2010

Now we can use annotations to load our services definitions into the Symfony Dependency Injection container.

For the second part of this series on Dependency Injection, we are going to see an elegant way to integrate the Symfony service container with your Zend Framework application.

During my researches, I found two blog posts that guided me for this integration:

  • The first one is from Benjamin Eberlei and shows how to replace the Zend_Application default bootstrap container with the Symfony Service Container.
  • The second one from Dolf Starreveld shows the same as Benjamin but with a more advanced integration by subclassing Zend_Application_Bootstrap_BootstrapAbstract class.

In the following post I will expose how to integrate the Symfony Dependency Injection container into Zend_Application bootstrap with a simple implementation of a ServiceContainerFactory. I will then expose how to inject your defined services into your controller with an action helper and with a simple @Inject annotation!

Replace the default boostrap container with Symfony’s Service Container

Since Zend Framework 1.8 bootstrapping applications is much more easier thanks to Zend_Application. It facilitates application bootstrapping and provides reusable resources to easily configure and initiate various components you will use such as Zend_Layout, Zend_Translate, Zend_Navigation or Zend_Db.

To be accessible throughout your application, the resources objects are registered in a container also called the resource registry. And as Matthew Weier O’Phinney (ZF’s project lead) said in Benjamin’s post comments, this explicit design was made to use a Dependency Injection container instead of the default Zend_Registry instance. So we are gonna replace this default registry with the full featured Symfony’s Dependency Injection container.

I wrote a class named LoSo_Zend_Application_Bootstrap_Bootstrap which extends the default Zend_Application_Bootstrap_Bootstrap class :

/**
 * Description of LoSo_Zend_Application_Bootstrap_Bootstrap
 *
 * @author Loïc Frering <loic.frering@gmail.com>
 */
class LoSo_Zend_Application_Bootstrap_Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    public function getContainer()
    {
        $options = $this->getOption('bootstrap');

        if(null === $this->_container && $options['container']['type'] == 'symfony') {
            $autoloader = Zend_Loader_Autoloader::getInstance();
            $autoloader->pushAutoloader(array('LoSo_Symfony_Components_Autoloader', 'autoload'));
            $container = LoSo_Symfony_Components_ServiceContainerFactory::getContainer($options['container']);
            $this->_container = $container;
            Zend_Controller_Action_HelperBroker::addHelper(new LoSo_Zend_Controller_Action_Helper_ServiceContainer());
        }
        return parent::getContainer();
    }
}

It overrides the getContainer method where I instantiate and return a Symfony Service Container if the option bootstrap.container.type value is set to symfony in your configs/application.ini file. Three main operations are done here:

  • Registering the autoloader for loading Symfony Components classes
  • Getting a Service Container instance from a factory
  • Adding a helper for dependency injection to the action helper broker.

The LoSo_Symfony_Components_Autoloader is a simple class I developed to easily autoload the classes of the two Symfony Components necessary here: Dependency Injection and YAML (for defining services into YAML files). I will not describe it here, see the code for implementation details.

The Service Container Factory

LoSo_Symfony_Components_ServiceContainerFactory class purpose is to instantiate and configure the Symfony Service Container. Just pass an array of options to getContainer method to get an instance:

class LoSo_Symfony_Components_ServiceContainerFactory
{
    protected static $_container;

    public static function getContainer(array $options)
    {
        self::$_container = new sfServiceContainerBuilder();
        foreach($options['configFiles'] as $file) {
            self::_loadConfigFile($file);
        }

        return self::$_container;
    }

    protected static function _loadConfigFile($file)
    {
        $suffix = strtolower(pathinfo($file, PATHINFO_EXTENSION));

        switch ($suffix) {
            case 'xml':
                $loader = new sfServiceContainerLoaderFileXml(self::$_container);
                break;

            case 'yml':
                $loader = new sfServiceContainerLoaderFileYaml(self::$_container);
                break;

            case 'ini':
                $loader = new sfServiceContainerLoaderFileIni(self::$_container);
                break;

            default:
                throw new Atos_Symfony_Exception("Invalid configuration file provided; unknown config type '$suffix'");
        }
        $loader->load($file);
    }
}

As you can see in the extended Bootstrap class, the bootstrap.container.* options retrieved from application.ini are passed to the factory. You can specify definition files to load thanks to the configFiles array option:

bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"
bootstrap.container.type = "symfony"
bootstrap.container.configFiles[] = APPLICATION_PATH "/configs/services.yml"

Then the factory loads to the Service Container each configuration file by instantiating the correct loader according to the file extension.

Dependency Injection action helper

We have now integrated Symfony service container with Zend_Application and loaded configuration files easily defined in our application.ini. The last operation done in the extended getContainer method was to register a service container action helper. Let’s see now how to retrieve services from our controllers with this action helper.

The direct function allows to retrieve a service or a parameter by his identifier while the getContainer method simply returns the Symfony service container instance:

class LoSo_Zend_Controller_Action_Helper_ServiceContainer extends Zend_Controller_Action_Helper_Abstract
{
    /**
     * @var sfServiceContainer
     */
    protected $_container;

    public function __construct()
    {
        $this->_container = $this->getActionController()->getInvokeArg('bootstrap')->getContainer();
    }

    public function direct($name)
    {
        if($this->_container->hasService($name)) {
            return $this->_container->getService($name);
        }
        else if($this->_container->hasParameter($name)) {
            return $this->_container->getParameter($name);
        }
        return null;
    }

    public function  getContainer() {
        return $this->_container;
    }
}

Here are some examples on how you can use this helper in your controllers:

class MyController extends Zend_Controller_Action
{
    protected $_testService;

    public function init()
    {
        $this->_testService = $this->_helper->serviceContainer('testService');
    }

    public function indexAction()
    {
        $serviceContainer = $this->_helper->serviceContainer->getContainer();
        $myService = $serviceContainer->getService('myService');
        $myService = $serviceContainer->myService;
        $param = $serviceContainer->getParameter('param');
        // ....
    }
}

Depency Injection with annotations within our controllers

To ease and automate dependency injection in our controllers, we are going to use annotations to declare the services we want to be automatically injected. To achieve this task, we can think about declaring our controller classes in the service container by loading them with the ServiceContainerAnnotationsLoader I described in my previous post.

But we are not directly in charge of the controller instantiation, that’s the dispatcher‘s main mission. In a first approach I did not wanted to extend the standard dispatcher to let him manage the controllers lifecycle. Instead I decided to use introspection on the controller instance to inject the annotated dependencies. The best way to achieve this is in the preDispatch hook of an action helper because at this stade: the controller has been instantiated by the standard dispatcher and you can retrieve it via the getActionController method of Zend_Controller_Action_Helper_Abstract class.

Helper class? Hey we’ve just developed one for our service container! So here’s the complete implementation I made of my LoSo_Controller_Action_Helper_ServiceContainer:

class LoSo_Zend_Controller_Action_Helper_ServiceContainer extends Zend_Controller_Action_Helper_Abstract
{
    /**
     * @var sfServiceContainer
     */
    protected $_container;

    public function __construct()
    {
        $this->_container = $this->getActionController()->getInvokeArg('bootstrap')->getContainer();
    }

    public function preDispatch()
    {
        $actionController = $this->getActionController();

        $r = new Zend_Reflection_Class($actionController);
        $properties = $r->getProperties();

        foreach($properties as $property) {
            if($property->getDeclaringClass()->getName() == get_class($actionController)) {
                if($property->getDocComment()->hasTag('Inject')) {
                    $injectTag = $property->getDocComment()->getTag('Inject');
                    $serviceName = $injectTag->getDescription();
                    if(empty($serviceName)) {
                        $serviceName = $this->_formatServiceName($property->getName());
                    }
                    if($this->_container->hasService($serviceName)) {
                        $property->setAccessible(true);
                        $property->setValue($actionController, $this->_container->getService($serviceName));
                    }
                }
            }
        }
        
    }

    protected function _formatServiceName($serviceName)
    {
        if(strpos($serviceName, '_') === 0) {
            $serviceName = substr($serviceName, 1);
        }
        return $serviceName;
    }

    public function direct($name)
    {
        if($this->_container->hasService($name)) {
            return $this->_container->getService($name);
        }
        else if($this->_container->hasParameter($name)) {
            return $this->_container->getParameter($name);
        }
        return null;
    }

    public function  getContainer() {
        return $this->_container;
    }
}

Here’s what the preDispatch method do:

  • Introspect the action controller class
  • For each property of the class, if the property is tagged @Inject:
    • Determine which service identifier we want to inject:
      • If a service name is explicitly defined in the @Inject description (eg @Inject myService), use this name as identifier
      • If not, use property name (with no _ if it begins with) as identifier
    • Check if the service is declared in the container
    • Then inject the service instance into the property

Here we are, we can now easily inject services into our controllers:

class MyController extends Zend_Controller_Action
{
    /**
     * @Inject
     */
    private $_testService;

    public function indexAction()
    {
        // ....
    }
}

The whole code base and examples are available on my Bitbucket.

Coming soon

My next post will describe how to automatically load your services classes into the container with the annotation loader. You will then just have to tag @Service the classes you want to be managed by the service container with no overhead XML or YAML configuration file.

Advertisements

15 Responses to “Integrating Symfony Dependency Injection Service Container with Zend Framework”

  1. Juozas Says:

    Very good post! Exactly what I have been working on for quite some time (phpdoc comments based configuration).

    Only one thing I’d add is caching for reflection results as they are said to be expensive, so something like apc should play nicely. Maybe you have tried those?


    • Hello Juozas,

      Thanks for your feedbacks!

      Concerning performances, I have two solutions in mind:

      • The first would be, as you said, using caching to store the services identifiers which a controller depends on with Zend_Cache. Then in a production environment, the helper would load from annotations the dependencies in the first request and look in the cache for the following requests.
      • The second solution would be to extend the standard dispatcher, as I mentioned in the post, to let the controllers being loaded from the Dependency Injection Container instead of being instantiated. As a consequence, the container would be aware of the controller dependencies thanks to the annotations loader and would load them automatically. Then the “Need for Speed” paragraph I wrote in my previous post would apply there to store the container description in a PHP file after the first introspection.

      The first solution would be quite easy to implement but the second is a bit more tricky because it implies completely rewriting the dispatch() method of the standard dispatcher.


  2. […] a new post to his blog, Loïc Frering is looking into integrating Symfony Dependency Injection Service with […]


  3. […] Loïc Frering berichtet über die Integration von Symfonys Dependency Injection Service Container […]


  4. […] Integrating Symfony Dependency Injection Service Container with Zend Framework Wer eine Antwort auf die Frage “Symfony DI + Zend Framework” sucht, der wird hier fündig. […]

  5. Michael Says:

    This is indeed a very good approach and it works pretty well with my application.

    I was wondering how to use that in conjunction with Zend_Test_PHPUnit_ControllerTestCase. Now, where all controller dependencies are injected using a controller plugin at run time, it seems to be useful to inject mock objects of those dependencies when running a ControllerTestCase. Do you already found a smart way do this?

    From my point of view, it would be nice to directly inject those dependencies from tests’ setUp() method and not running do it via plugins.

    Thanks,
    Michael

  6. Michael Says:

    Another issue that I am faced with that the dependency injector helper seems to mask certain errors that happen within the controller or view. In fact, I now very often see error messages like:

    Fatal error: Call to a member function hasTag() on a non-object in /[…]/App/Controller/Helper/DependencyInjector.php on line 19

    However, the problem never has to do with the Helper methode itself, much more an issue e.g. with database access happend within the controller or the model.

    Does anyone have similar problems?

    Thanks,
    Michael


  7. Hello Michael,

    Concerning the bug you encounter, please note that I migrated my sources from Bitbucket to Github. You’ll see in my Github repository that I resolved this bug, so feel free to check out losolib’s last version!

    Concerning testing your service layer, your controllers and mocking your services, I’ll be working on this subject very soon so keep following my blog and my Github repository!


  8. If only more than 75 people could read this.

  9. tdh Says:

    Nice work mate, but i could find autolader http://bitbucket.org/loic_frering/losolib/src/925e533444d2/library/LoSo/Symfony/Components/Autoloader.php, looks link are broken.

    I just check on git hub and can’t find either :). Newbie here so i decide to track back older version first.

    Thanks for nice article, GBU.

  10. Jp Says:

    Thx alot, exactly what i need !

  11. Learn More About Save Energy Says:

    It’s actually a cool and helpful piece of info. I’m glad that you just shared this useful info with us. Please keep us informed like this. Thanks for sharing.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: