Symfony2: Framework independent controllers part 3: Loose ends
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:
- 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.
- It enables them to be a lot more confident, as well as independent. They stop being “Symfony developers” and become better “general developers”.
- 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.