Symfony2: Framework independent controllers part 3: Loose ends

Posted on by Matthias Noback

Thanks! Let me explain myself

First of all: thanks everybody for reading the previous parts of this series. Many people posted some interesting comments. I realized quickly that I had to explain myself a bit: why am I writing all of this? Why would you ever want to decouple controllers from the (Symfony2) framework? You probably don't need to bother anyway, because

The chances of you needing to move controllers to other frameworks is next to none. — Rafael Dohms

Well put! And I agree with that. You don't need to do all of this because you might switch frameworks later on. People don't do that. Though maybe they would like to use your code in their own application, which uses another framework, which is a good reason to decouple your controllers from your framework. But:

If your controller is slim and hands off everything to a service layer, rewriting it to fit a new framework is a piece of cake. — Rafael Dohms

Agreed, controllers should contain almost no code, just making some calls to services, retrieving some data from them, returning some kind of response... However, I think there should be no need to rewrite those controllers to work with another framework. Then again, if you don't intend to share your code, there's no need to decouple your controllers from the framework.

If you do however, it makes you really happy. Not only your controllers are independent, you as a developer have become more independent too. No need to rely on helper functions, quick annotations, or magic behind the scenes: you are perfectly able to do everything yourself. I really like this quote because it describes a recognizable experience:

Writing a new controller is now a greater commitment than in the past and because of this, I think about the code more. — Kevin Bond

So these are really my reasons for writing this series:

  1. They give Symfony developers a better insight into what's really going on behind the scenes, in particular all the things that the framework does for them.
  2. It enables them to be a lot more confident, as well as independent. They stop being "Symfony developers" and become better "general developers".
  3. Reading these articles is a great exercise in making dependencies explicit. Controller conventions are dependencies too, although they are pretty much hidden from sight, so the reader can practice their "coupling radar" skills.

Twig templates

Let's take some last steps toward framework independent controllers. In the previous part we eliminated all annotations and we used configuration files instead. We also injected some dependencies that we needed to fetch data and render a Twig template. One thing was still wrong: the template name used the bundle name as a namespace:

class ClientController
{
    ...

    public function detailsAction(Client $client)
    {
        return new Response(
            $this->templating->render(
                '@MatthiasClientBundle/Resources/views/Client/Details.html.twig',
                ...
            )
        );
    }
}

Since we intend to make this controller work in applications where there is no notion of bundles, we need to choose a more generic name, like MatthiasClient. Then we must also register that name as a namespace for Twig templates. This means calling Twig_Loader_Filesystem::addPath('/path/to/twig/templates', 'NamespaceOfTheseTemplates'). The good thing is, within a Symfony2 application you can do this very easily, using the twig.paths configuration key:

# in config.yml
twig:
    paths:
        "%kernel.root_dir%/../src/Matthias/Client/View": "MatthiasClient"

Once you have added the path in your config.yml file, you can change the code in the controller to:

return new Response(
    $this->templating->render(
        '@MatthiasClient/Client/Details.html.twig',
        ...
    )
);

Even better: prepend configuration

This is not a very elegant solution though, since it requires a manual step when you first enable the MatthiasClientBundle in your project. There is a better option: you can prepend configuration programmatically from within your bundle's extension class. You only need to make it implement PrependExtensionInterface and provide an array with values you'd like to add before the values defined in config.yml:

use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;

class MatthiasClientExtension extends Extension implements PrependExtensionInterface
{
    ...

    public function prepend(ContainerBuilder $container)
    {
        $container->prependExtensionConfig(
            'twig',
            array(
                'paths' => array(
                    '%kernel.root_dir%/../src/Matthias/Client/View' => 'MatthiasClient'
                )
            )
        );
    }
}

Now you can remove the extra line from config.yml since from now on it will be added automatically.

Removing the dependency on HttpFoundation

Based on the previous article, there were some other concerns:

Your controller code creates actual dependencies on Doctrine and the HttpFoundation, which the annotated version doesn't have — Gerry Vandermaesen

In my opinion, it's not such a big problem to depend on Doctrine. It is just my library of choice here (just like Twig is). Decoupling from your ORM/ODM is an interesting thing to pursue, and it is very well possible, but it's not within the scope of this series.

But Gerry is right, by removing the annotations, we introduced a dependency on the Request and Response classes from the HttpFoundation component. I think (in contrary to his opinion) that this is already a huge step in the direction of framework-independence since there are many frameworks already out there that also make use of the HttpFoundation as an abstraction layer for HTTP messages.

Nevertheless, we might choose to go one step further and remove the dependency on HttpFoundation too. We want to remove the use of Request and Response objects. We could change our controller into something like this:

public function detailsAction($id)
{
    $client = $this->clientRepository->find($id);
    if (!($client instanceof Client)) {
        return array(null, 404);
    }

    return array(
        $this->templating->render(
            '@MatthiasClient/Client/Details.html.twig',
            array('client' => $client)
        ),
        200
    );
}

No mention of anything from the HttpFoundation component there! Now we can wrap this controller using composition into a controller that makes use of classes from the HttpFoundation component:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class SymfonyClientController
{
    private $clientController;

    public function __construct(ClientController $clientController)
    {
        $this->clientController = $clientController;
    }

    public function detailsAction(Request $request)
    {
        $result = $this->clientController->detailsAction($request->attributes->get('id'));

        list($content, $status) = $result;

        if ($status === 404) {
            throw new NotFoundHttpException($content);
        }

        return new Response($content, $status);
    }
}

Note that this will definitely become a bit tedious and will make your controller code quite difficult to understand. So I don't recommend you to do this! But I just wanted to show you that it's possible.

Let go of "action methods"

One last subject to discuss: controller actions. It is considered a best practice to group related actions into one controller class. For instance all actions related to Client entities are put inside a ClientController class. It has methods like newAction, editAction, etc. Now if you inject all the required dependencies as constructor arguments, like I recommend, all the dependencies for all the actions in one controller will be injected at once, even though some of them are never used.

The way to solve this is also very easy and it greatly enhances your ability to read, change and even find a particular controller too! You only need to choose a different way to group your actions. Basically, every action should get its own class. The directory in which you put them now becomes the concept that binds them, e.g. Controller\Client\New, Controller\Client\Edit, etc. Each of those controllers has one public method that will be called when the controller is executed. Naming the method __invoke() makes a lot of sense here:

namespace Matthias\Client\Controller\Client;

class Details
{
    public function __construct(...)
    {
        // only inject dependencies needed for this particular action
    }

    public function __invoke(Client $client)
    {
        ...
    }
}

This technique is something I first read about in the Modernizing Legacy Applications in PHP book by Paul Jones. I've used it with success several times now, and it has provided me with a much better "controller experience"!

Conclusion

That's it. I've given you many ideas to play with next time you create a controller. I hope you've learned a lot about the Symfony2 framework. I also hope your mind is free now ;)

Please let me know if you have other suggestions. If you have, or if I think of something, I will definitely write a sequel.

PHP Symfony controller reuse coupling Twig
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Tomáš Votruba

I would like to see and update for this series.
Some reflection what works, what doesn't etc.

I might share my experience as post too :)

Cerad

Another fine series. Might want to tag it with Symfony 2 in place of Symfony.

It's a bit unfortunate that you marketed this as a way to move controllers between frameworks. You had to spend a lot of time explaining and defending it.

What you really accomplished was to set the stage for reducing the controller's responsibilities.

Consider the case of a game scheduling application. It can present many types of game schedules such as public, team, referee, assignor, scheduler, administrator game schedules. Each schedule can be shown as html, json,pdf etc. Each type deals with a set of games but there are just enough differences to make it challenging to share behavior between controllers. With controllers, I found myself duplicating quite a bit of code and when I could share code, it was difficult to track who was using what. The trick was to move functionality outside of the controller.

Here is a schedule route:


cerad_game__project__schedule_referee__show:
path: /project/{_project}/schedule-referee.{_format}
defaults:
_controller: cerad_game__project__schedule_referee__show_controller:__invoke
_model: cerad_game__project__schedule_referee__show_model_factory
_form: cerad_game__project__schedule_referee__show_form_factory
_template: '@CeradGame\Project\Schedule\Referee\Show\ScheduleRefereeShowTwigPage.html.twig'
_format: html
_views:
csv: cerad_game__project__schedule_referee__show_view_csv
xls: cerad_game__project__schedule_referee__show_view_xls
html: cerad_game__project__schedule_referee__show_view_html
requirements:
_format: html|csv|xls|pdf

I have a model listener (KernelEvents::CONTROLLER) which creates an action model. The model is in charge of retrieving and (in some cases) updating games. I can share the same model between the various schedule controller's by injecting the model with different configuration options.

A form listener does the same for the search form.

The model and form are both injected into the controller action method. The controller is now very slim indeed. If form->isValid then call model update and return a redirect. Otherwise, return null.

A view listener (KernelEvents::VIEW) then creates a view based on _fomat, passes it the model and returns it's rendered response.

Just something I thought I would throw out there.

Matthias Noback

Thanks for sharing this. It certainly is an interesting approach. This reminds me of SyliusResourceBundle and the SonataAdminBundles. It is highly convention-based though, while I personally lean more towards a configuration-based approach. So, yes, the controller should be responsible for almost nothing, but I like the few responsibilities it has to be handled quite explicitly, in plain sight.

sudin manandhar

I am facing problem with multiple entity manager. I have followed the symfony blog and googled my problem but I could not get satisfactory answer. The problem is I am getting Unknown entity namespace from non-default entity manager.

More detail about my issue: http://forum.symfony-projec...

You can check my codes here : https://github.com/sudinem/...

Daniel Ribeiro

It's nice to see that you were following one path at the beginning of this article series and now you pivoted to a whole different path just by absorbing the community feedback. That takes a lot of humility, which is something particularly rare in developers in general.

Overall, I don't agree on decoupling frameworks – I tend to be clear about the delivery-mechanism and the application layer, and keeping controllers at those layers makes me more confident about the layers of my application. Still, good article series.

Matthias Noback

Thanks for your kind words, Daniel! I somehow overlooked them.

Christian Flothmann

Your extension class now assumed that the bundle lives inside the src directory. Wouldn't it be better to build the path dynamically?

Matthias Noback

Yes, that is a problem... The bundle is coupled to the filesystem and in particular its location (as well as the location of the templates in this particular example). The solution would be to use something like the resource locator suggested (and developed) by Bernhard Schüssek (see also http://webmozarts.com/2013/.... Then you could leave wiring of resources like this to some external component. It's actually dependency inversion for data!

Christian Flothmann

What do you think about using something like "__DIR__.'/../View" instead I in this context?

Matthias Noback

Well, this still has some hardcoding to it. If library and bundle are in separate packages, inside the bundle you don't know where the library is installed. Well, all of this is "strictly speaking" anyway. ;)

cordoval

the EBI with __invokes is something i have seen with igorw and davedevelopment and beberlei. I have been using it since the time in Portland sflive

Kevin Bond

When configuring a single-action controller class as described above in your routing config, should it look like:

my_route:
path: /path
defaults: { _controller: my.controller.service:__invoke }

?

jwrobeson

and thanks to kevin bond you don't have to add that __invoke anymore in symfony master
https://github.com/symfony/...

Paul M. Jones

Very good series, Matthias, and thanks for the book plug. :-)

Matthias Noback

Thank you too, Paul!

Kevin Bond

For decoupling from Doctrine, I always create repository interfaces that my Doctrine repositories implement. Now I can switch to another ORM or something else entirely:

- UserRepository (interface)
- DoctrineUserRepository
- ElasticsearchUserRepository
- FlatFileUserRepository
- RedisUserRepository
- ...

And not have to change my controllers at all.

As for decoupling from HttpFoundation, I created a small library ( https://github.com/kbond/Co... ) that allows your controller to return simple "response context" objects: Template, Forward, Redirect, FlashRedirect. There is a bundle for integration with Symfony and a ServiceProvider for integration with Silex - others could be easily added. Of course, this now couples your project to this library and it doesn't cover every possible situation. The main purpose of the library is to reduce the number of services you need to inject into your controllers.

Matthias Noback

Interesting idea, Kevin! I will look into this.

Paul M. Jones

> I created a small library ... that allows your controller to return simple "response context" objects

That seems like it fits into the Responder portion of Action-Domain-Responder: https://github.com/pmjones/...

Couple that with the single-Action class as described by Matthias near the end of the article, and it sounds like a full ADR implementation to me.

cordoval

will look into this PMJones

What about symfony forms? Is there an easy way to decouple from them?