ramblings on PHP, SQL, the web, politics, ultimate frisbee and what else is on in my life
back 1  2  3  »  

Do you want DIC with that Controller?

At Liip "Symfony2" started in production a year earlier. Well I say "Symfony2" because it was just our internal framework OkAPI enhanced with the current crop of at the time PHP 5.2 Symfony components (actually we also included the symfony 1.x routing component). The results were so convincing that we decided to switch to Symfony2 entirely. The big feature back then and today is of course dependency injection (DI) and the dependency injection container (DIC). I remember many discussion with Jordi back then, where we went back and forth on if Controllers should be configured in the DIC or not. In the end we did go with making Controllers DIC services. The other alternative is of course to instead just inject the entire DIC and let the Controller pull whatever services it needs straight from the DIC.

Just to make it clear what we are talking about, here is an example without using a service:


<?php
class HelloController implements ContainerAwareInterface
{
    protected $container;

    public function setContainer(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function indexAction($name)
    {
        $template = 'LiipHelloBundle:Hello:index.html.twig';
        $params = array('name' => $name); 
        return $this->container->get('templating')->render($template, $params);
    }
}
?>

And here with a service:


<?php
// requires the following service definition:
// services:
//     liip_hello.world.controller:
//         class: Liip\HelloBundle\Controller\HelloController
//         arguments:
//             -@templating
class HelloController
{
    protected $templating;

    public function __construct(EngineInterface $templating)
    {
        $this->templating = $templating;
    }

    public function indexAction($name)
    {
        $template = 'LiipHelloBundle:Hello:index.html.twig';
        $params = array('name' => $name); 
        return $this->templating->render($template, $params);
    }
}
?>

Fabien disagree's with us and favors the later approach of not using a service and instead injecting the entire DIC. His main line of reasoning is that its simply not feasible to make Controllers reusable. All heavy lifting should be pushed into services anyway so when a Controller doesn't do what you need it should be trivial to just re-implement it from scratch.

Now I totally agree with the concept of slim Controllers, so I am totally in agreement that the heavy lifting should be pushed into services. We have done exactly that in the FOSUserBundle. We have created a UserManager that does most of the interactions with the model. We have added a process method into our form handlers that handles most of the form related code. We have created an email service etc. This makes it easy to implement a different behavior by changing the service. Also the Controller are very slim now, so if I for a example want to make a FOSRestBundle based Controller I will have to duplicate very little code. So far so good.

Now can you tell me which of the various controllers uses which form class? Which one uses the email service? You can't without reading the entire Controller code. The reason is that the dependencies of the Controller are now hidden in all of the actions. If I were to refactor the code I would have no easy way to determine what impact changing the email service would have for example. This to me is the biggest draw back of injecting the Container, you loose a big opportunity for documenting your dependencies.

This leads to other draw backs, like having no easy way to for example take one Bundle written to use the standard "mailer" service and another Bundle which created its own "mailer" service expecting the user to simply not register the default "mailer" service. Since both Bundle's would use the "mailer" service but expecting entirely different class instances I would now be forced to extend or rewrite from scratch one of the Bundles (*). If the Bundles were using services I would only need to change the service definitions on one of the Bundles. To make the issue clearer there is no way to actually know what you will get when you ask for a service from the DIC and there is no easy way to enforce anything without using type hinting along with DIC injection into a service.

Another side benefit is that when using a service to inject the services I need, I can make full use of type hinting. Furthermore the service instances are assigned as properties. So some of the boilerplate code at least leads to getting tab completion in an IDE.

Finally while agree that its not feasible to write reusable Controllers for every possible use case (it seems we get a new feature request for the FOSUserBundle Controllers every week), by pushing the heavy lifting to external services, it becomes feasible for more and more use cases because the main things people need to adjust will likely be in one of these services and not in the Controllers anymore.

(*) Actually I can also instead change my routes to use a service instead of the simplified syntax. Then I could define a service for the Controller even if it wasn't originally intended to be defined as a service. I can then use the LiipContainerWrapperBundle to inject a wrapped Container instance that is able to alias service ID's as shown in the here.

Update: Turns out the "templating" service was a bad example, so I changed it to "mailer", but really the issue affects any case where one Bundle expects something different than another Bundle under the same service name.

Comments



Re: Do you want DIC with that Controller?

I agree. The constraints of configuring controllers as services in the DIC help prevent "junk drawer" controllers. If you find yourself adding a bunch of dependencies to a controller that most of its actions don't use, you probably want to move the actions that do use those services to a new controller.

Re: Do you want DIC with that Controller?

Exactly.

Re: Do you want DIC with that Controller?

I've been looking at Igor's example and couldn't help wondering why anyone would do this.

For completeness: https://gist.github.com/1050974

Since controllers are meant to a very thin layer of glue between view and model, I guess the biggest challenge is to educate people to keep it thin.

Is this your primary objective?

Re: Do you want DIC with that Controller?

What @Kris pointed out is just another side benefit, for me the main reason is to keep an overview of my dependencies. This helps for refactoring, but also helps in spotting if a Controller becomes too unfocused. Its just a design method that help writing clean and maintainable code. Of course you can still write unmaintainable services :)

Re: Do you want DIC with that Controller?

Yeah, exactly.

This is interesting nevertheless. I have to explore this with silex a bit.

Re: Do you want DIC with that Controller?

Btw, since controllers are meant to thin. I don't follow the reasoning of making them re-usable.

I think in this case, I also favour something like a 'base controller' (or whatever that pattern is called). It's just a class doing the "heavy" lifting or offering helpers which all controllers in a module extend from.

Re: Do you want DIC with that Controller?

hm.. I (as developer of reusable bundles) was following the discussion mainly because I need some information to decide what's the best practice to provide controllers that can be easily modified in a project by another bundle.

mainly I miss this cookbook: http://symfony.com/doc/current/cookbook/bundles/inheritance.html ..I'm too Symfony2 newbee to find the best answers..

Of course the other question is what is the best choice to write a project from scratch (without tons of premade bundles)..

The possibility to monitor the dependencies and to define explicit class variables seems to be a good argument to me for using services as controllers.

Re: Do you want DIC with that Controller?

I think that your second aproach is just the way that it should be done from the very begining of MVC implementation in PHP. It's clearer - you see on what other objects a controller depends,. It also opens an oportunity to have a reusable controllers.

@till:

What you prefer in your aproach is not Object Oriented Programming but rather Class Oriented Programing and it ends with writing tons of boillerplate code and getting stuck with regresion bugs when trying to change some fundamental functionality within the "base controller" class. I tried this once on a big project and belive me it is a hell.

Re: Do you want DIC with that Controller?

@till igorw's example is a POC using silex but it does not make much sense in this case IMO due to the way Pimple (used by Silex) works. This blog post is about Symfony2, not about micro frameworks where a big DIC does not make much sense as it would be too complex (this is why Silex uses Pimple, not the Sf2 DIC).

Making a controller reusable is mainly useful when you share your code, to allow the user to reuse your controller instead of reimplementing from scratch each time something does not fit its need (even if he will likely reimplement some of them anyway when he has specific needs). The base controller is not a solution either: it does not solve the issue about replacing a service and hides dependencies a bit more as you need to look at the base class to find which are the dependencies (and look at all method calls to check if the method of the base class are used or not).

1  2  3  »