I consider this article deprecated. Controllers don't need decoupling, domain logic does. See A simple recipe for framework decoupling

In the previous part of this series we decreased coupling of a Symfony controller to the Symfony2 framework by removing its dependency on the standard Controller class from the FrameworkBundle.

Now we take a look at annotations. They were initially introduced for rapid development (no need to create/modify some configuration file, just solve the issues inline!):

namespace Matthias\ClientBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

/**
 * @Route("/client")
 */
class ClientController
{
    /**
     * @Route('/{id}')
     * @Method("GET")
     * @ParamConverter(name="client")
     * @Template
     */
    public function detailsAction(Client $client)
    {
        return array(
            'client' => $client
        );
    }
}

When you use these annotations, the details action will be executed when the URL matches /client/{id}. A param converter will be used to fetch a Client entity from the database based on the id parameter which is extracted from the URL by the router. The return value of the action is an array. These are used as template variables for rendering the Resources/views/Client/Details.html.twig template.

Well, very nice! All of that in just a couple of lines of code. However, all the things that happen auto-magically out of sight make this controller tightly coupled to the Symfony2 framework. Although it has no explicit dependencies (i.e. no type-hints to other classes), it has several major implicit dependencies. This controller only works when the SensioFrameworkExtraBundle is installed and enabled, for the following reasons:

  • It generates routing configuration based on annotations.
  • It takes care of the conversion of an array return value to an actual Response object.
  • It guesses which template needs to be rendered.
  • It converts the id request parameter to an actual entity.

This might not seem such a big problem at all, but the SensioFrameworkExtraBundle is a bundle, which means it only works in the context of a Symfony2 application. We don't want our controller to be coupled like this to the framework (at least, that is the point of this series!), so we need to remove the dependency.

Instead of using annotations for configuration, we will use actual configuration files and actual PHP code.

Use a proper routing file

First we make sure routes for this bundle are loaded from Resources/config/routing.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="https://symfony.com/schema/routing"
        xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://symfony.com/schema/routing
        https://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="client.details" path="/client/{id}" methods="GET">
        <default key="_controller">client_controller:detailsAction</default>
    </route>

</routes>

You can also use Yaml, but I prefer XML these days.

Make sure the client_controller service actually exists and don't forget to import the new routing.xml file in your application's app/config/routing.yml file:

MatthiasClientBundle:
    resource: @MatthiasClientBundle/Resources/config/routing.xml

Now we can remove the @Route and @Method annotations from the controller class!

Create the response object yourself

Next, instead of relying on the @Template annotation, you could easily render a template yourself and create a Response object containing the rendered template. You only need to inject the templating engine into your controller manually and explicitly provide the name of the template you want to render:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;

class ClientController
{
    private $templating;

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

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

In the service definition of this controller, make sure you inject the templating service as a constructor argument:

services:
    client_controller:
        class: Matthias\ClientBundle\Controller\ClientController
        arguments:
            - @templating

After making these small changes we can also remove the @Template annotation!

Fetch the required data yourself

There's one last step we can take to decrease the coupling of the ClientController to the framework even further: we still depend on the SensioFrameworkExtraBundle for the automatic conversion of an id to an entity. It can not be too hard to fix this! We might just as well fetch the entity ourselves using the entity repository directly:

...
use Doctrine\Common\Persistence\ObjectRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class ClientController
{
    private $clientRepository;
    ...

    public function __construct(ObjectRepository $clientRepository, ...)
    {
        $this->clientRepository = $clientRepository;
        ...
    }

    public function detailsAction(Request $request)
    {
        $client = $this->clientRepository->find($request->attributes->get('id'));

        if (!($client instanceof Client) {
            throw new NotFoundHttpException();
        }

        return new Response(...);
    }
}

The service definition needs to return the right entity repository, which can be accomplished in this way:

services:
    client_controller:
        class: Matthias\ClientBundle\Controller\ClientController
        arguments:
            - @templating
            - @client_repository

    client_repository:
        class: Doctrine\Common\Persistence\ObjectRepository
        factory_service: doctrine
        factory_method: getRepository
        public: false
        arguments:
            - "Matthias\ClientBundle\Entity\Client"

Please also read one of my previous articles about injecting repositories.

Now, finally, there's no need to use annotations anymore, which means our controller could definitely be used outside of a Symfony2 application (i.e. an application that doesn't depend on the Symfony FrameworkBundle, nor the SensioFrameworkExtraBundle). All dependencies are explicit, i.e. to execute the ClientController you need:

  • The Symfony HttpFoundation Component (for Response and NotFoundHttpException).
  • The Symfony Templating Component (for the EngineInterface).
  • Some kind of Doctrine repository implementation (e.g Doctrine ORM, Doctrine MongoDB ODM, etc.).
  • Twig for rendering the templates.

There is one loose end:

  • The template name is still convention-based (it uses the bundle name as a namespace, e.g. @MatthiasClientBundle/...). This is an implicit dependency on the framework, since it registers these bundle namespaces on the Twig Filesystem loader for you. We will address this issue in the next post.
PHP Symfony controller reuse coupling
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).
uniquate

Thanks for the article, I also read all the comments, but did not find a single case, which would have affected the performance of the *DEV environment*.

I used a maximum of annotations in one of Symphony3 project: @Route, @Template, @ParamConverter, @Entities and etc etc etc. And the project has become very slow in DEV environment, about 1500 milliseconds per request. When I began to understand what was going on, I figured out that it was all about Annotations parsing.

I like annotations, and don't think that coupling to framework is a big problem, but now I'm really confused about this point of view. I know that in production all the annotations will be compiled to PHP, but we all are developing in DEV environment and 1500 msec per request is too much, right?

What do you guys think of this?

Marcin

Maybe you have a problem with cache on your dev machine then symfony runs very slow (problem exists on virtualized environment on os x or windows machines). Annotation are cached and /per se/ they do not cause performance problems. Despite this I also do not recommend use annotations (they cause strong couple with framework)

Tomáš Votruba

UPDATE: Thanks to Symfony 2.8 and it's autowiring feature, now it's quite easy to allow autowiring for controllers. With much less pain than in the past.

I recommend checking for anyone using Symfony 2.8+: www.tomasvotruba.cz/blog/20...

Glenn Quagmire

Well, the official symfony documents and the best practice recommendations say otherwise, annotations are considered as best practice, and yes the contoller must me tightly coupled with the framework to leverage the power of the framework. and no there is absolutely no performance difference between yml, annotation etc, because there is some processing that happens in between so the format does't matter. P.S update this article and stop spreading false knowledge

Tomáš Votruba

I'm actually not coupled to Symfony controllers yet, because I'm using another framework. I guess it's the reason I don't get caught by "why to decouple from Symfony?" feeling, which is natural when someone tries to steal your favorite toy.

I see great advantage in code sharing among different projects, build on different versions of different framework. At the moment, when you want to use some third party package, you need to integrate it manually, or use some your framework-bind extension/bundle/bridge whatever. One of these per framework.

In my opinion, what this approach suggest is not remove useful annotations from all your code, but to rather think less coupled, and THUS more opened to other sources of your project's code. When you're not coupled to your framework, suddenly your software ecosystem of packages and tools you can use is much much bigger.

Jay Kapor

Hi do you have String51, String52, etc also? Made by you, not a
TypeProvider? Then you convert these like old school object-mapping? The
cost is lines of code, vs. how likely it is to happen. Thanks .. .
IT Outsourcing Dallas

Victor Osório

About the performance? What's the best solution?

Andrzej Ośmiałowski

Matthias,

good reading and interesting point of view.

I maintain one quite big Symfony based project: bunch of services, circa 80 entities (can't remember the exact amount at the moment), a lot of relations between entities. While developing the initial release it was my first Symfony project I used annotations in. What can I say from perspective of about two years? It was good decision. I didn't end up with 80 entities mapping files, I've ended up with 80 entities containing mapping metadata.

From my perspective, it's acceptable to use annotations for Doctrine mapping, validation constraints or routing, but not for DI or advanced security configuration.

I think some developers concentrate too much about decoupling components recently. I presume, it would be faster and less painful to rewrite SF controller in ZF than trying to move & adjust it (I have no experience in this area as I always try to rewrite things if needed - if you have some experience I'd like to hear your opinion).

For example, the idea of framework-agnostic controllers is good, but what are its real-world benefits? Most of framework controllers are coupled (less or more) with an application or a framework.

The main problem is that every developer should be aware of pros/cons of using annotations and don't overuse them. I'm not against the whole concept of annotations. I really would like to see the annotations to be supported in PHP core (and don't have to write them in docblock), but for now, we are forced to use the tools & methods we have.

SymfonyDeveloper

Good joke :)

Eduardo Oliveira

IMO create the response is actually pretty bad idea you are coupling your code to the HttpFoundation Response object. If you return and regular object or array, then if you change framework you can have a listener transforming that in the object of response that framework uses.

duane_gran

Annotations are a wonderful concept. There are some clumsy areas, but generally they enable you to document the interface and keep the code lightweight. I guess they do make it hard to interplay with other frameworks, but I hope to keep using them long term.

nanocom

You forgot to point out the dependency to the routing component by the use of the xml file

Kevin Bond

Great post.

I used to use annotations but got bogged down when something wasn't working right. I recall a major pain point when trying to use FOSRestBundle with SensioFrameworkExtraBundle. There were some conflicts that I got stuck on for quite some time. I ended up scraping annotations and using "pure" PHP.

I like the idea of writing my controllers without "magic" and wiring them up in config. This is of course more time consuming but I feel the quality of my code has increased because of it. Writing a new controller is now a greater commitment than in the past and because of this, I think about the code more.

Marc Morera Merino

Great post, Matthias.

Here my point of view.

We come from a time where we did not mind the code, and now we are try our lines of code are compatible with all brands of microwave, casio watches and gameboy color ...

As a teaching and educational exercise is great, but following a pragmatic context, I think we're starting to oversize the decoupling of our projects.

The annotations have helped many businesses grow and focus on more important things, making people understand of the code and the location thereof. Thanks to the annotations, probably, we can say that companies have grown much faster than if they had not existed, and hope to remain so.

As always we say, the annotations are a very useful tool, while you know how to use it and be aware of what it takes to have them in your project.

The problem I think is still at this point, the way that people use the tools provided to them. Maybe this post should emphasize the fact that if you want to use an annotation should, first, learn how it works inside, so you have complete control of the process.

And is that ... how many projects in the world do you think that change their framework?

Still very good post :)

Guest

You could argue that fetching the data yourself is a violation of the dependency injection principle and will anyway result in a lot of duplicated code.

mnapoli

Data is not a dependency. Fetching data is not violating dependency injection.

Gerry Vandermaesen

I don't fully agree with this part of the series.

First of all, I don't believe any configuration format should really be seen as a dependency. Even though my controllers are annotated with @Template, @ParamConverter, ... it doesn't mean they aren't decoupled from the framework, in fact for example using ParamConverters will make it much easier to switch ORM's for example, while your controller code creates actual dependencies on Doctrine and the HttpFoundation, which the annotated version doesn't have. Sure, if I want to eliminate SensioFrameworkExtraBundle, I'll have some work in writing the appropriate listeners to inject the correct parameters and handling my controller response, but in general my controller code will be more agnostic.

Matthias Noback

Interesting point of view Gerry. Annotations are a dependency because they are classes. But relying on external behavior like replacing arguments and converting a return value is not necessarily a "dependency". In fact, one option would be to provide a decorator (e.g. a "SymfonyControllerDecorator") for those controllers which would make it work as expected.

Gerry Vandermaesen

Indeed, technically it wouldn't be very hard to accomplish the same external behaviors with external config files too, eliminating the "dependency" on the annotation in your code, that's maybe a nice idea for a SensioFrameworkExtraBundle extension or improvement.

Rafael Dohms

i think i see Gerry's point as: rewriting configs to a new format is pretty much the same as rewriting annotations to a new format. After all annotations are just metadata. Unless the new framework has no annotation support at all, then configs would be better.

cordoval

the configs are better. Matthias what your last step is is removing the lines to autoload register the annotations on autoload.php or bootstrap.php and we are done ^_^

Rafael Dohms

I have the same mindset as Bart. You are sacrificing a lot of productivity to achieve what i would call "academic value" since the chances of you needing to move controllers to other frameworks is next to none.

Also, if you controller is slim and hands off everything to a service layer, rewriting it to fit a new framework is a piece of cake.

But i see the academic value in this. The architect nirvana.

Daniel Ribeiro

People tend to think the application layer should be isolate like the domain layer should be. The application layer is dependent upon the framework, as the framework is the entry point of the application in terms of the delivery-mechanism. It's generally a waste of time to try to write framework-agnostic controllers. It's the nirvana of the architect nirvana.

Matthias Noback

See also my comment to Bart too. Switching frameworks is not something people do, I realize that :) Though I also feel that too many things are being rebuilt for different frameworks and it is generally a good thing to be aware of all the ways you depend on a framework and how you could easily get rid of those dependencies.

Rafael Dohms

I totally agree with the "mental exercise" involved here and the added value to people who don't really know the "inner workings", i'm 100% with you on the aggregated value to knowledge there.

From a business side i would not do this in a work project unless i had very specific requirements, but in side-projects or research projects this is an excellent exercise, and very good if you are learning new frameworks, to be able to switch between them with the same code.

By no means do i remove the value of this article, just the applicability in everyday work. It is a topic that you need to be aware of.

Jan Kramer

While I fully agree with respect to the productivity argument, the decoupling is not only beneficial when switching frameworks, but also when upgrading to a newer version of the same framework that introduces BC-breaks. That scenario will very likely occur for projects that span a larger time span.

Matthias Noback

Thanks for the elaboration Rafael. You are right. For me it's particularly important to question applicability in everyday work. ;) I must say that I don't go as far as I describe here in most real life projects. At the moment I have more time available for pet projects, which makes me hungry for trying out new things, and indeed for reaching architect's nirvana.

Guest

While I fully agree with @rdohms:disqus with respect to the productivity argument, the decoupling is not only beneficial when switching frameworks, but also upgrading to a newer version of the same framework that introduces BC-breaks. That scenario will very likely occur for projects that span a larger time span.

cordoval

i fully disagree with @rdohms:disqus and aspects of productivity. In the legacy run these coupled annotations will not be easy to upgrade for. Now you depend on other libraries, doctrine annotations, and 2 bundles, what if they have bugs and don't tag them, what if there is something you need to do that does not support it, then you have to dig deep. So reducing this gap and making it well refactored and plain simple is a gain for good true legacy apps. you don't loose business because the legacy is crap but you are able to keep adding new features even if the code is more extensive, but well named and organized in a manner that the good maintainer knows best.

cordoval

I think people like to throw quick code, whereas this better approach pays off with less bugs, less time consumed on them, and same speed or even faster since there is no magic or assumptions and things are faster to trace.

Christian Soronellas

I think it's not a matter of decoupling of the framework nor a matter of productivity. After all, you're talking about Symfony, a "delivery mechanism" or an implementation detail. And they all reside in the infrastructure boundary. In that sense, adding a SensioFrameworkExtraBundle's annotation to a Symfony's Controller makes sense. Because Symfony comes with SensioFrameworkExtraBundle by default. I think that what truly matters is the separation of concerns. Said this, to me the point about using annotations is the side-effects they trigger. Annotations like @Route or @Method are useful annotations and they don't trigger side-effects. On the other hand, annotations like @ParamConverter or @Template are annotations that change the way my code behaves (If I remove it, I have to change my code).

Bart Guliker

When are you ever practically going to save more time by being able to move a controller from one framework to another without changing anything, versus writing (and repeating) a bunch of code in every controller action? Nice that it's possible, but it seems to me it's more trouble than it's worth.

Matthias Noback

Thanks for bringing this up, Bart. I don't think many people will switch between frameworks and I don't think that most people will end up doing all of the above (even though I do, I really like it very much this way). So why am I writing about this subject?

1. It gives Symfony users a great insight into what's really going on behind the scenes.
2. It enables them to be a lot more confident, as well as independent. They stop being "Symfony developers" and become better "plain developers"
3. It is a great exercise in making all dependencies explicit. Controller conventions are dependencies too.

I do think I should elaborate on this subject a bit more in the next part of this series. Otherwise I'm afraid people might put this aside for being merely theoretical, academic, etc.

cordoval

fully agree with @matthiasnoback:disqus, and i even go beyond. I have seen many times that people want to handle things, not just plug them and bam. this is a big mistake the up front practicality does. I mean it is like candy, deceiving up front :)