Symfony2: How to create framework independent controllers?

Posted on by Matthias Noback

Part I: Don't use the standard controller

The general belief is that controllers are the most tightly coupled classes in every application. Most of the time based on the request data, they fetch and/or store persistent data from/in some place, then turn the data into HTML, which serves as the response to the client who initially made the request.

So controllers are "all over the place", they glue parts of the application together which normally lie very far from each other. This would make them highly coupled: they depend on many different things, like the Doctrine entity manager, the Twig templating engine, the base controller from the FrameworkBundle, etc.

In this post I demonstrate that this high level of coupling is definitely not necessary. I will show you how you can decrease coupling a lot by taking some simple steps. We will end with a controller that is reusable in different types of applications, e.g. a Silex application, or even a Drupal application.

Unnecessary coupling: extending the base controller

Most Symfony controllers I come across extend the Controller class from the FrameworkBundle:

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class MyController extends Controller
{
    ...
}

The base controller class offers useful shortcuts, like createNotFoundException() and redirect(). The base class also makes your controller ContainerAware, which means the service container will be automatically injected. You can then pull from it any service you need.

Don't use the helper methods

This all sounds really nice, but being just a little less lazy will decrease coupling a lot. It's absolutely no problem to leave those "highly useful" shortcut methods out. They mostly are simple one-liners (take a look at the standard Controller class to see what is really going on). You can simply replace the function calls with their actual code (e.g. make it an "inline method").

class MyController extends Controller
{
    public function someAction()
    {
        throw $this->createNotFoundException($message);

        // becomes

        return new NotFoundHttpException($message);
    }
}

Use dependency injection

If you don't use any of the helper functions from the parent Controller class anymore, you need to take one more step before you can remove the extends Controller part from the class definition: you have to inject the dependencies of your controller manually instead of fetching them from the container:

class MyController extends Controller
{
    public function someAction()
    {
        $this->get('doctrine')->getManager(...)->persist(...);
    }
}

// becomes

use Doctrine\Common\Persistence\ManagerRegistry;

class MyController extends Controller
{
    public function __construct(ManagerRegistry $doctrine)
    {
        $this->doctrine = $doctrine;
    }

    public function someAction()
    {
        $this->doctrine->getManager(...)->persist(...);
    }
}

Turn your controller into a service

This means you also need to make sure that the controller is not instantiated by just using the new operator, like the ControllerResolver does, since that would prevent you from injecting any dependencies at all. Instead, define it as a service:

services:
    my_controller:
        class: MyController
        arguments:
            - @doctrine

Now you need to modify your routing configuration too. If you use annotations:

/**
 * @Route(service="my_controller")
 */
class MyController extends Controller
{
    ...
}

If you use a configuration file:

my_action:
    path: /some/path
    defaults:
        _controller: my_controller:someAction

Finally, there is no need to extend from the standard Framework controller: we don't need it to be container-aware since we inject everything that's needed using constructor arguments. And we also don't use any of the helper functions offered by the standard controller class, so we can truly remove the extends Controller part of the class definition, including the use statement we introduced for it:

class MyController
{
}

This earns us a lot of decoupling points!

In the next post, we will discuss the use annotations and how getting rid of them will decrease coupling even further.

PHP Symfony controller reusability coupling

I have removed Disqus from this website, so for now, you can't comment on articles. In the near future you will be able to send comments by email. For now, if you want to say something, you can always send an email to me personally: info@matthiasnoback.nl.

Comments
cordoval

typo from "return new NotFoundHttpException($message);" -> "throw new NotFoundHttpException($message);"

cordoval

oh i love to get rid of annotations! looking forward to the part II now!

mnapoli

This is a bit of a pedantic question but: "why?" :)

Why decouple controllers from the framework, if controllers are ultra-slim (as they should be) and not contain domain logic? In the end, why would you want to reuse those controllers elsewhere?

Myself I am often torn between putting some effort into having decoupled controllers, or on the other hand not putting too much effort into it given they are supposed to be as small as possible.

Matthias Noback

Thanks for your suggestion! The "why" needs to be addressed, I will do that in a later post. For now a quick response:

1. The point of HttpFoundation and HttpKernel was that if other frameworks used those components, it would become possible to share code between applications written for different frameworks. I think this doesn't happen much right now, but it could happen more often if people would not need to rewrite controllers for every framework. They could put them in their library package if they write them in this decoupled way.

2. Controllers are supposed to be very slim. Still, there is some code in them which could very well be reused. This series addresses the question how to accomplish such a thing.

The kind of code that arises from these exercises is quite reusable (as much as possible I think), but in your regular projects you may choose not to do it this way. Although I don't feel that it's too much of a hassle, I've done it recently, and it really didn't slow me down.

mnapoli

I would answer a bit like @drgomesp:disqus 's comment:

1. You mean like "bundles", but cross-frameworks (i.e. not limited to Symfony)? I believe this is a sane goal, but I don't believe it should go through generic controllers. IMO the framework today is more of a lib to handle HTTP requests, just like Symfony Console is a lib to handle CLI commands. It's a lib, it's ok to be dependent on a lib outside of the Domain (just like you can be dependent on Doctrine or whatever). By the way, here is an illustration on how "bundles" written with different frameworks can work in the same application: https://github.com/mnapoli/...

2. If there is any code in them that is supposed to be reused, I would put it in a service (domain or not), or in a controller helper for example.

On the contrary, I find it very slow to write controllers as services in Symfony, because of the container (yaml declarations are very painful). I like to use annotations instead (e.g. http://mnapoli.fr/controlle... ), which is not a problem IMO because controllers are not supposed to be reused.

tim glabisch

thanks, great post, but your controllers are framework aware.

1. it's not about symfony but you're injection doctrine for example.
2. annotations are a dependendency, too.
3. it's a good idea to just use dependencies on self defined interfaces. Injecting vendors could end up in vendor-lock-in.

4. also notice that conventions like forcing a "Action / Controller" suffix is a implicit framework detail. Instead of forcing "Action" Methods take care about method visibility. Don't misunderstand me, it's fine to use this convention but not to force it.
5. the return value of a controller is also a very important hidden implementation detail. Even if you just return an array, it's a detail the Framework (or better, selfwritten listeners) have to know.
6. also note that using a base controller is not bad, the problem is using the frameworks base controller :)

mnapoli

Very good points.

I disagree only with the first one: Doctrine is unrelated to Symfony. So coupling yourself to Doctrine is not the same thing as coupling yourself to the framework. It's a dependency.

tim glabisch

we are talking about the same thing. i even wrote "it's not about symfony", but Doctrine is a dependency.

Matthias Noback

Right, so there's more to do in later posts. Part 2 which I will publish tomorrow is about getting rid of annotations, moving configuration to XML files and returning only actual Response objects from the controller.

By the way I'm a big fan of one-action-controllers (or "true" controllers), which I will mention in another post too.

Sebastiaan Stok

A great way to use make your Controllers reusable is to use the routing (external only) for passing configurations to the Controller, Sylius uses this in the ResourceBundle.
So you don't need to create a BackendController and FrontendController.

But you can decouple the Configuration handling from the Bundle as well, and use an interface for a flexible implementation.

I've created something that is highly based on the SyliusResourceBundle, but less coupled to Sylius and Repository usage (you can use a Hexagon, CQRS of plain-old RAD Repository pattern).

And I hope to release this next month, its currently part of a proprietary project but I'm slimming that down to make the various parts more reusable and open-source (MIT).

In the end you can use this for any framework/application, like, Symfony, Zend, Silex.
Its currently coupled to Symfony but I'm planning on changing this (as recommended in Principles of PHP Package Design).

Keep up the good work.

Luciano Mammino

Great post.
Probably all controllers included within "public" bundles should follow a similar approach. I am very curious to read the other posts from this series.

Prophet777

To counter balance,

Set a controller as service does not make it more independant, you just move the dependency injection from inside to outside but the dependencies remain the same. Just change the container behavior to NULL_ON_INVALID_REFERENCE or use Container::has() and dont extend from
FrameworkBundle::Controller but your own.
In reusable context, controller as service it's may be more easier to implement, because you dont need to create an adaptater / decorator to implement ContainerInterface on your own dependency system whereas as service you inject it directly.

Secondly that create a service for nothing and makes your container fat. The controller as service is register to get benefit of outer dependency injection but he is never used as a service himself.

Finally, controller as service do not resolve this problematic, it's only makeup IMO. In other hand, make controller reusable outside from symfony application is strange because a controller is specific to an application especially since it does not contain any business logic and thus easily portable from one application to another. Inside symfony application, you can overload / override controller in all case (as service or not) according to your business.

Note that I am not against controller as service, i'm in favor but not for the reasons.

Daniel Ribeiro

Exactly. The refactoring is good and the result is very, very good. But the intention is based on the non-sense argument of decoupling controllers from the framework.

Daniel Ribeiro

There's no point on decoupling controllers from frameworks – in fact, I think that is very, very wrong. Controllers exist because we need to solve the *Delivery Mechanism* problem, and that is often – or always – related to some sort of framework.

If you need to deliver your application to the web, you need a framework that allows you to write controllers. If you need to deliver as a CLI, you need a different framework that allows you to write CLI commands.

Controllers are not and will never be part of the domain – with that said, there's no point on making effort to decouple them from the framework. I can agree that this refactoring improves code quality by a lot, but framework coupling is not a problem here – as it's not a problem on the view layer as well.

Mihai Stancu

I agree that unless you're testing the code on each framework you intend to be using it, decoupling the code from a framework as if you could drop-in-replace the framework is pretty useless.

If push comes to shove and your only compatibility issues are those shortcut methods you can replace the FrameworkBundle controller with a custom one that supplies you with the exact same shortcut methods and the refactoring is finished.

Mihai Stancu

Sevices should contain business logic while controllers should contain technology-specific logic (service calls, flushing, transactions, locking).

Controllers may or may not include framework specific stuff and they may or may not include request-context specific stuff.

As an example my goal would be to make my controllers request-context agnostic. Ex.: one controller action could serve the same logic for multiple routes with the same input/output-parameters but different input/output-protocols:
- /entity/save Plain -- HTTP (GET/POST) Request => HTML Response
- /entity/save?ajax -- AJAX HTTP (GET/POST) Request => AJAX specific HTML Response
- /entity/save?ajax_json -- AJAX JSON Request => AJAX JSON Response
- /json-rpc/entity/save -- JSON-RPC Request (via API) => JSON-RPC Response
- /xml-rpc/entity/save -- XML-RPC Request (via API) => XML-RPC Response
- ...etc.

With some custom paramConverters and controller listeners to fetch the responses and convert those accordingly this should be pretty doable.

Mohamed Ragab Dahab

I am afraid, but this is one of the worst articles i have ever read, the reason is: you spent a long time to explain and demonstrate some thing completely wrong.

i think you forget that controller are born to be thin and just a glue between model and view nothing more, hence no meaning to reuse it as it already has a very tiny data.

Henry Vallenilla

Also, you can extend another BaseController specifying in a service with argument @doctrine.orm.entity_manager

Tomáš Votruba

UPDATE: In case you look for autowiring in combination with controllers, since Symfony 2.8+ there is nice bundle for that: www.tomasvotruba.cz/blog/20...

ntraykov

I cannot understand the real benefit of this. I agree that this way controllers are decoupled. But you are still using them in symfony. Where else you can use them? Can I use this controller in Laravel or Yii2? Can I create business logic layer or model that does not depend on Symfony? Can I call Eloquent instead of Doctrine in the business logic layer?

Tomáš Votruba

The point's it's easier to understand to everybody, not only Symfony programmers.

Most Symfony programmers don't find this useful for such reasons, but for me it's super easy to start using any framework that support decoupled controller.

ntraykov

Yes, what you say is very correct - "any framework that supports decoupled controller" - so, Symfony and Laravel can take advantage of this. But Yii2 (since I am currently using it) can't. Because I have to name the controller and the action in a specific way.

Tomáš Votruba

Welcome to framework-vendor-lock :) eventual nightmare of every long-lasting project.

Well, it is possible in Yii as well with bit of internal service overloading. I did the same fo Nette: https://www.tomasvotruba.cz...

ntraykov

Nice! Thanks a lot! I will check it!

Tomáš Votruba

Glad to help