Symfony2: How to create framework independent controllers?
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.