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.

Reply via email to