Hi all,
Last week, I took the time to play with the implementation of the main
approaches discussed in the controllers RFC. I also wrote some unit
tests to see how it feels. During this journey, I discovered that
writing a RFC without any implementation is quite difficult (my
apologizes for this). But I learned a lot, and discovered that some
approaches look rather impractical in real-world scenarios.
This email is an attempt to explain my findings and my concerns about
some approaches and why they are not as good as they seem to be in the
RFC. If you don't want to read the whole email, which is quite long, let
me give you the main conclusions right away:
* For the common cases, the controller needs the Container object (if
not, you loose a lot of shortcuts provided by the Controller base class,
or you are forced to inject a lot of services you don't need because
they are used by the parent class methods);
* Approach 1 (yes, the one currently implemented) is the best
implementation (simple, fast, easy);
* If you like to have the Request object in the controller signatures
instead of just the routing variables, then Approach 1, where we change
the controller method arguments to just the Request objet, is also a
working approach (see my previous email to understand why I think it is
a less attractive approach);
* Other approaches are not exciting anymore as the Container should be
injected at some point. So, injecting services in the controller
constructor or methods is just a convenient shortcut, nothing more.
Now, for the longer story.
My initial point of view was to be able to test a controller as any
other method; just by calling it and checking the result. That's
possible for simple controllers like the following one:
public function showAction($name)
{
return new Response('Hello '.$name);
}
If you like to have the Request object injected, here is the equivalent:
public function showAction(Request $request)
{
return new Response('Hello '.$request->getPathParameter('name'));
}
With such a controller, writing a unit test is straightforward:
class HelloControllerTest extends \PHPUnit_Framework_TestCase
{
public function testShowAction()
{
$container = new Container();
$controller = new HelloController($container);
$response = $controller->indexAction('Fabien');
$this->assertRegexp('/Hello/', $response->getContent());
}
}
The controller only relies on the request, and as such, if you use some
approaches where you inject services manually, it will still be easy (or
even easier to test as we remove the need for the container):
class HelloControllerTest extends \PHPUnit_Framework_TestCase
{
public function testShowAction()
{
$request = new Request();
$request->setPathParameters(array('name' => 'Fabien'));
$container = new Container();
$controller = new HelloController($container);
$controller = new HelloController($container);
$response = $controller->indexAction($request);
$this->assertRegexp('/Hello/', $response->getContent());
}
}
Also, if you need a database connection to do your job, it's still easy
enough:
public function showAction(Request $request, DatabaseConnection $conn)
{
// do something with the $conn
return new Response('Hello '.$request->getPathParameter('name'));
}
In such cases, testing is also still easy:
class HelloControllerTest extends \PHPUnit_Framework_TestCase
{
public function testShowAction()
{
$request = new Request();
$request->setPathParameters(array('name' => 'Fabien'));
$conn = new DatabaseConnection(...);
$controller = new HelloController();
$response = $controller->indexAction($request, $conn);
$this->assertRegexp('/Hello/', $response->getContent());
}
}
But easiness stops here. As soon as you write "richer" controllers, problems
comes very fast. The main problem is with "hidden" dependencies.
Consider the
following example:
public function indexAction()
{
return $this->render('BlogBundle:Post:index');
}
Behind the scene, the `render()` method needs the `templating` service;
no big deal. And if you want to forward to another action, you need the
`controller_loader` service. That's easy enough, even if it feels a bit
magic as the end user controller does not really use the service directly:
public function __construct($templating)
{
$this->templating = $templating;
}
public function indexAction()
{
return $this->render('BlogBundle:Post:index');
}
But when it comes to testing, things are much more complex. Because the
templating needs a loader, helpers, and some more services. So,
re-creating the dependency graph (with mock objects or not) is a more
difficult. At this point, you will probably just use the container in
your test to create the dependencies automatically (with a special
configuration for the test environment of course).
For this reason, and after having played with all approaches and their
corresponding unit tests, I think that a controller needs the full
container object. With this in mind, testing becomes easy again no
matter what:
public function setUp()
{
$kernel = new \BlogKernel('dev', true);
$kernel->boot();
$this->container = $kernel->getContainer();
}
public function testIndexAction()
{
$controller = new PostController($this->container);
$response = $controller->indexAction();
$this->assertRegexp('/Hello/', $response->getContent());
}
Of course, the boilerplate code can be abstracted in a sub-class of the
PHPUnit Test Case class.
That works for most controllers. And if you want to do "functional"
tests, it's as easy as unit tests, as you can just ask the request
handler to handle the request for you.
public function setUp()
{
$this->kernel = new \BlogKernel('dev', true);
$this->kernel->boot();
}
public function testIndexAction()
{
$request = new Request($get, $post, $...);
$response = $this->kernel->handle($request);
$this->assertRegexp('/Archives/', $response->getContent());
}
As before, we will provide a sub-class of Test Case to abstract the
boilerplate code.
Sorry for the very long post but the controller implementation is at the
heart of an MVC framework and as such the final decision about which
approach to use should be taken carefully.
As always all comments are very welcome.
Fabien
--
Fabien Potencier
Sensio CEO - symfony lead developer
sensiolabs.com | symfony-project.org | fabien.potencier.org
Tél: +33 1 40 99 80 80
--
If you want to report a vulnerability issue on symfony, please send it to
security at symfony-project.com
You received this message because you are subscribed to the Google
Groups "symfony developers" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/symfony-devs?hl=en
To unsubscribe from this group, send email to symfony-devs+unsubscribegooglegroups.com or
reply to this email with the words "REMOVE ME" as the subject.