Writing Clean Skinny Controllers in Laravel using the '__invoke()'   method cover image

Writing Clean Skinny Controllers in Laravel using the '__invoke()' method

Oliver Sarfas • January 10, 2020

laravel programming

Laravel: Decluttering your Controllers is easier than you think

One of the most common things I find when inheriting a code base or talking to developers, is that their controllers are way too bloated and cause headaches.

API endpoints become more complex. Logic is added, amended, removed, commented out. Multiple hands touch the code over the project duration, and now it's a mess.

How do we handle this? Can controllers be given the "new year, new me" treatment? Let's have a crack at it.

What are Controllers?

Controllers are part of the MVC concept used widely in Object Orientdeped Programming. For these examples and considerations, I'll be using Controllers in a Laravel application.

The controller is responsible for taking the request, and returning the necessary and applicable response to the request.

What is a "Fat Controller"

When you're modifying, maintaining, or writing your application - you're working with a lot of variables. The request, server variables, third party integrations, error handling, security, etc., and more.

Due to this, it's common (not recommended) for developers to put a lot of conditional logic into their controllers, such as;

The next example is an index controller that takes a request, and does an action based on the request mode.`

The route declaration for this would look something like, $router->get('/{mode?}', 'TestController@index')->name('test');

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController extends Controller
{
    public function index(Request $request, $mode = null)
    {
        switch($mode) {
            case 'create':
                return view('pages.create');
            case 'delete':
                // Do deleting stuff
                return back()->with('flash', 'Deleted');
            case 'update':
                // Do update stuff
                return back()->with('flash', 'Updated');
            case 'edit':
                return view('pages.editing');
            case 'view':
                return view('pages.view');
        }
    }
}

Right now, this isn't really a fat controller. Mainly due to the fact I've put comment lines in the update, and delete clauses. Let's imagine those are larger.

I've had codebases before, one that I'm working on today as a matter of fact, that have controllers methods in excess of 600 lines in length. The classes themselves are over 2000 lines. No exaggeration.

Slim down those lines!

Less is more. Get your methods on a 2020 slimming diet!

We can clean up this controller a bit by splitting up the index method into each mode, and update the route registration to point to the necessary endpoint.

## Route Declaration

// old
$router->get('/{mode?}', 'TestController@index')->name('test');

// New
$router->get('/{mode?}', [TestController::class,request()->get('mode', 'view')])->name('test');

This new route declaration gives us the ability to define new methods on our controller based on the mode presented by the request. Our controller now looks like this

## Controller

[...]
    public function create(Request $request, $mode)
    {
        return view('pages.create');
    }

    public function delete(Request $request, $mode)
    {
        // Do deleting stuff
        return back()->with('flash', 'Deleted');
    }

    public function update(Request $request, $mode)
    {
        // Do editing stuff
        return back()->with('flash', 'Updated');
    }

    public function edit(Request $request, $mode)
    {
        return view('pages.editing');
    }

    public function view(Request $request, $mode)
    {
        return view('pages.view');
    }

[...]

We now have got full control over each and every mode, in it's own method. This makes our code far easier to test, read, and maintain.

Using something as simple as splitting out the request logic will also mean less merge conflicts when working in Git or another VCS.

Too many methods? 🤔

You can take this one further, and I'd heavily recommend that you do if you this from the start if you can.

You might have seen some articles lately talking about "Invokable controllers". I personally think they're great for isolating functionality and being very verbose with your route declarations.

The concept here is that your routes do not link to a method of a controller - but to the controller itself. This leaves you with an __invoke() method in each instance, and perhaps a few protected or private methods therein to cleanup the logic.

In our example, we'd end out with 5 different controllers, each with their own __invoke() method. But as they are now 100% isolated we can say we conform to SOLID's "Single Responsibility Principle"; A class should only have on reason to change.

Previously, our class could change for one of at least 5 reasons.

This is how the invoke version would look. To save on scrolling I've only shown the route registration for 2 of the routes and the breakdown of their respective controller.

## Route Registration

$router->get('/view', App\Http\Testing\ViewController::class)->name('test.view');
$router->get('/update', App\Http\Testing\UpdateController::class)->name('test.update');
## Testing\ViewController

namespace App\Http\Controllers\Testing;

use Illuminate\Http\Request;
use App\Http\Controllers;

class ViewController extends Controller
{
    public function __invoke()
    {
        return view('pages.view');
    }
}
## Testing\UpdateController

namespace App\Http\Controllers\Testing;

use Illuminate\Http\Request;
use App\Http\Controllers;

class UpdateController extends Controller
{
    public function __invoke()
    {
        // Do Update Logic
        return back()->with('flash', 'Updated');
    }
}

Summary

Questions? Want to talk? Here are all my social channels