marcelog / Ding

DI ( Dependency Injection: Setter, Constructor, Method), AOP ( Aspect Oriented Programming ), Events support, xml, yaml, and annotations (including some JSR 250 and JSR 330, like @Configuration and @Bean ala java configuration) , lightweight, simple, and quick MVC ( Model View Controller ), syslog, tcp client and server, with non blocking sockets, custom error, signal, and exception handling through events. Needs PHP 5.3, very similar to seasar, spring ( java ) . Can be deployed as a PHAR file.
http://marcelog.github.com/Ding
Apache License 2.0
121 stars 26 forks source link

DI in ZF controllers #96

Open marcelog opened 13 years ago

marcelog commented 13 years ago

Taken from #95, by jonathaningram

A question related to annotations: I want to inject a Service (say, my ProductsService) into my Controller (say, my ProductsController) via a property or method of the controller, however, the design of Zend is that when you request a resource (say abc.com/products/list), the controller is constructed automatically by Zend, which means that I cannot specify the controller as a bean in beans.xml and so the @Resource won't be injected. Without intercepting ZF's creation of the controller and checking the beans container for a matching controller bean, is it possible to have the @Resouce injected without having a bean of that class? Make sense?

marcelog commented 13 years ago

I think a friend of mine described to me the same problem just a few days ago, and his proposed solution was to make a special "plugin" or some kind of bootstrap code that can run when ZF bootstraps your application.

I dont see a way of DI happening without an explicit getBean() somewhere in the code. I'll try to give you a solution for this... however, I'll have to play a little with ZF myself since I'm not very familiary with it..

jonathaningram commented 13 years ago

OK thanks - yes I can imagine a kind of plugin doing this, and it's a possible solution. In fact, all I'm currently doing (not using annotations) is within the init() method of the controller, I am retrieving the IoC container (stored in Zend_Registry) and retrieving the service bean from it. So it's not show stopper, but I guess I'm thinking of the times of using Spring whereby you can simply specify the beans you need, but the controllers automatically picked up the annotations.

It's an interesting problem anyway. I just inspected the ZF dispatcher and it has a hard coded instantiation of the controller:

    $controller = new $className($request, $this->getResponse(), $this->getParams());

It would need some way to be able to get this from a bean instead...

marcelog commented 13 years ago

Jon..

In the meantime.. you say you're using your controller init() in order to get the beans you need (like the service). This leads me to an alternative option, using a helper. The reason for this, is that it is possible that you may not need the service in every action of your controller, so in these cases it may not even be necessary to bootstrap the container.

So maybe, instead of using something like $this->_service->something(), you would make $this->myContainerHelper->service()->something(). You may use magic methods to implement this and you can instantiate the container and the beans you need in a more lazy way. What do you think?

jonathaningram commented 13 years ago

You are definitely correct that I shoul have some sort if lazy instantiation.

FYI in my init method I've just used zend registry as a temporary measure to get the container. I develop iteratively and in a framework like zf where best practice is not always documented my zf apps slowly evolve and these "@todo"'s in my init method should evolve eventually.

In terms of a helper called, say, "container", I have a sneaking suspicion that this does not belong in the controller? What do you think? I mean, my controller needs a service and it shouldn't care where that service came from (I.e. It should not know that it comes via a container) except that it has a property called service that works. I'm interested in your comments on this dependency that I say should not exist?

marcelog commented 13 years ago

well, afaik, you have helpers along all the ZF code (like view helpers, and so on, that you use from your controller in a $this->helpername()->helpermethod() way).

These are implemented by using magic methods in the base controller class for ZF (please correct me if I'm wrong, since I don't have much experience with ZF), so you will always have them available, without having an explicit dependency on them (at least from your own code). We could say they are already "injected" in your controllers.

Since there is a hardcoded "new $classname" in the dispatcher code, we have to give up the dependency injection and inversion of control (at least for the controllers). But, as you did with your idea of using your init method to get your beans, we can still use ding as "service locator".

So where should be the right place to do this "service location"? I suppose it depends on the architecture and how complex your application is. You might code some magic methods in your own controllers that can return what you need (i,e: issuing a $this->customerService()->add($customer)) would trigger the container instantiation (if needed) and also the requested getBean(customerService).

Another way to do it would be to have your own base controller class that extend from the ZF Controller but has this "container helper" embedded (just like the view helpers I mentioned earlier). So from any controller that extend this base class, you would do $this->containerHelper->customerService()->add($customer). Just like the above example, customerService() is handled by a magic method that issues a getBean(customerService).

The advantage of this last approach is that your dependency to the helper is somewhat concealed and the "service location" pattern is still achieved but from a completely different class that you can change any way any time you need to, providing encapsulation and separation of concerns and feels a little more like ZF way of doing things.

agvstin commented 13 years ago

Hi Jon, I started using Ding on a Zend Framework project and needed to address the same issue. Here's what I came up with:

Bootstrap my Ding Container

This is done in my bootstrap class.

This will initiate a Ding container and register it as a resource (name: ding)

protected function _initDing() { require_once 'Ding.phar';

$dingPropsPath = realpath(APPLICATION_PATH .'/configs/ding.properties');
$dingProperties = parse_ini_file($dingPropsPath);

$properties = array(
        'ding' => array(
            'factory' => array(
                'bdef' => array(
                    'xml' => array('filename' => APPLICATION_PATH ."/configs/beans.xml"),
                    ),
                'properties' => $dingProperties
                ),
            'cache' => array(
                'proxy' => array('impl' => 'dummy'),
                'bdef' => array('impl' => 'dummy'),
                'beans' => array('impl' => 'dummy')
                )
            )
        );

return  \Ding\Container\Impl\ContainerImpl::getInstance($properties);

} I'm planning on making an Application Resource with this, so I can configure and bootstrap my Ding container straight up from my application.ini file. I'll post back when I have it done :)

Ding Controller Action Helper

This helper will let me access the Ding container from any Zend_Controller (or any subclass of it :) ). class ZF_Action_Helper_Ding extends Zend_Controller_Action_Helper_Abstract {

protected $ding = null;

public function init()
{
    // just once...
    if (null !== $this->ding) {
        return;
    }

    // get ding bootstrapped resource
    $bootstrap = $this->getActionController()->getInvokeArg('bootstrap');

    $ding = $bootstrap->getResource('ding');

    if (!$ding) {
        throw new Zend_Controller_Action_Exception(
            'Ding resource not bootstrapped'
        );
    }

    $this->ding = $ding;
}

public function getBean($bean)
{
    return $this->ding->getBean($bean);
}

public function direct($bean)
{
    return $this->getBean($bean);
}

}

Now, to access the conatiner from any controller, all you need to do is // A controller action public function indexAction() { // using the direct() strategy $someService = $this->_helper->ding('someServie');

// if you plan to get several resources, it's better to get the helper instance
$container = $this->_helper->ding;
$bean = $container->getBean('someBean');
$bean2 = $container->getBean('otherBean');

}

Note: Make sure to add the prefix path to your helper broker. You can do this by adding this to your application.ini file.

resources.frontController.actionHelperPaths.ZF_Action_Helper = "ZF/Action/Helper"

Note 2: Make sure ZF namespace is registered in your autoloader (or register Zend autoloader as the fallback autoloader). You can accomplish the first adding this to your application.ini file

autoloadernamespaces.zf = "ZF"
jonathaningram commented 13 years ago

Thank you for your suggestion! And sorry for the late reply :)

Your solution seems good. Personally, I would not call the action helper "Ding" because all it is, is an DI container, so perhaps it could be named "DingDiContainer" or something and perhaps implement a "DiContainer" interface. I'm just thinking that it would be nice to refer to the DI container in our controllers, but not have to be dependent that it's a Ding DI container :)

Did you manage to get Ding annotations working with ZF? E.g. using an "@Resource" annotation to inject the services?

Thanks again!