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.
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?
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.
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.
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...
Nice! Thanks a lot! I will check it!
Glad to help
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...
Also, you can extend another BaseController specifying in a service with argument @doctrine.orm.entity_manager
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.
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.
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.
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.
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.
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.
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.
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.
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 :)
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.
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.
we are talking about the same thing. i even wrote "it's not about symfony", but Doctrine is a dependency.
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.
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.
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.
oh i love to get rid of annotations! looking forward to the part II now!
typo from "return new NotFoundHttpException($message);" -> "throw new NotFoundHttpException($message);"