Put your Symfony 3 controllers on a New Year's diet!

"I'm the leader of a totalitarian regime. Why do I need to diet?"

There's a mantra that's commonly repeated when using MVC frameworks - "Thin controllers, Fat models". It's a nice statement that's easy to remember but depending on your idea of thin, can be quite hard to achieve.

Preferably, we don't want logic in our controllers. In the perfect "Model 2" world, they should take a request, understand what it's requesting, retrieve data for the response, and respond with it.

3 of those 4 are required for a controller, but it's that "understand what it's requesting" bit that can fatten up your controllers like an unsuspecting turkey before Christmas.

A bit of validation of the input as we can never trust anyone - especially on the web, some mappings to turn those inputs into something we're going to use, a database check to see if access is allowed, and so on and so on. Soon we're facing a controller that would have made the late Alfred Hitchcock look like a super model.

To put simply, maintaining code like that sucks. Our controller's doing 2 separate tasks - validating the request and then using the valid request to get a response, controllers tend not to get unit tested - which includes any logic inside them, and the controller's longer and more complex than it needs to be.

And really, who likes spending time maintaining code? ... Go away Maintainer Man, you're the worst superhero ever.

That's where the custom request bundle comes in. Instead of using $this->request() or MyIndex(Request $request), you can use MyIndex(MyCustomRequest $request) and do all of your checks before the request object even gets to your controller. This means your controller now has a single responsibility (to take the valid request data and return a response from it) and your new custom request object can be easily unit-tested.

So now you're completely sold on using this bundle for every project (no backsies), let's go through how it works.

First, let's install it to your codebase using composer:

composer require dittto/symfony-custom-request

After that's installed, we're going to add the bundle to the app kernel, so we can use it:

// app/AppKernel.php

class AppKernel extends Kernel {
    public function registerBundles() {
        $bundles = [
            ...
            new \Dittto\CustomRequestBundle\DitttoCustomRequestBundle(),

Next we'll create our custom request object. This is going to be a simple check that looks for a query string containing token=allowed. If this is missing then the request will fail validation. Create the following file:

// src/MyStuff/AppBundle/Request/TestRequest.php

namespace MyStuff\AppBundle\Request;
use Dittto\CustomRequestBundle\Request\AbstractRequestType;

class TestRequest extends AbstractRequestType
{
    public function validate():bool
    {
        return $this->getOriginalRequest()->query->get('token') === 'allowed';
    }
}

Now we need to register our request so Symfony knows it exists. To do this update the services for our new TestRequest. The tag is important as this enables us to know this is a custom request and can be used as a controller parameter.

services:
    test_request:
        class: MyStuff\AppBundle\Request\TestRequest
        tags: [ { name: dittto.request } ]

Lastly, we need to tell our controller to use our custom request via it's parameters.

class DefaultController extends Controller {
    public function indexAction(TestRequest $request):Response {
        ...
    }
}

And there we go - a basic request object. Granted this test doesn't save you much, but using services you can easily add more checks using more-complex rules. As the TestRequestobject itself is available in the controller, you can also easily pass back extra data using getters.

You can also add Filters to the response from the request, to allow you to return all GET failures as a Symfony not found exception. For more information on these, have a look at the https://github.com/dittto/symfony-custom-request.

One note with this code, it's currently PHP 7.1 only. If there are requests I may release a PHP 5.6 version but as that's now end-of-life, I haven't created a version for it. Mainly because Strict typing is just so pretty.