Cleaning Up Laravel Controllers
Tags: laravel, php, programming, webdev
Published: 2021-06-25 16:10:30

## Introduction

Controllers play a huge role in any <a href="https://www.guru99.com/mvc-tutorial.html" target="_blank" rel="noopener noreferrer">MVC</a> (model view controller) based project. They're effectively the "glue" that takes a user's request, performs some type of logic, and then returns a response. If you've ever worked on any fairly large projects, you'll notice that you'll have a lot of controllers and that they can start to get messy quite quickly without you realising. In this article we're going to look at how we can clean up a bloated controller in <a href="https://laravel.com" target="_blank" rel="noopener noreferrer">Laravel</a>.

### The Problem with Bloated Controllers

Bloated controllers can cause several problems for developers. They can:

1. **Make it hard to track down a particular piece of code or functionality.** If you're looking to work on a particular piece of code that's in a bloated controller, you might need to spend a while tracking down which controller the method is actually in. When using clean controllers that are logically separated this is much easier.
1. **Make it difficult to spot the exact location of a bug.** As we'll see in our code examples later on, if we're handling authorization, validation, business logic and response building all in one place, it can be difficult to pinpoint the exact location of a bug.
1. **Make it harder to write tests for more complex requests.** It can sometimes be difficult to write tests for complex controller methods that have a lot of lines and are doing many different things. Cleaning up the code makes testing much easier. Check out this article if you're interested in finding out [how to make your Laravel app more testable](https://ashallendesign.co.uk/blog/how-to-make-your-laravel-app-more-testable).

### The Bloated Controller

For this article, we're going to use an example `UserController`:

```php
class UserController extends Controller
{
    public function store(Request $request): RedirectResponse
    {
        $this->authorize('create', User::class);

        $request->validate([
            'name'     => 'string|required|max:50',
            'email'    => 'email|required|unique:users',
            'password' => 'string|required|confirmed',
        ]);

        $user = User::create([
            'name'     => $request->name,
            'email'    => $request->email,
            'password' => $request->password,
        ]);

        $user->generateAvatar();
        $this->dispatch(RegisterUserToNewsletter::class);

        return redirect(route('users.index'));
    }

    public function unsubscribe(User $user): RedirectResponse
    {
        $user->unsubscribeFromNewsletter();

        return redirect(route('users.index'));
    }
}
```

To keep things nice and simple to read, I haven't included the `index()`, `create()`, `edit()`, `update()` and `delete()` methods in the controller. But we'll make the assumption that they are there and that we're also using the below techniques to clean up those methods too. For the majority of the article, we'll be focusing on optimizing the `store()` method.

## 1. Lift Validation and Authorization into Form Requests

One of the first things that we can do with the controller is to lift any validation and authorization out of the controller and into a <a href="https://laravel.com/docs/8.x/validation#form-request-validation" target="_blank" rel="noopener noreferrer">form request</a> class. So, let's take a look at how we could do this for the controller's `store()` method.

We'll use the following Artisan command to create a new form request:

```
php artisan make:request StoreUserRequest
```

The above command will have created a new `app/Http/Requests/StoreUserRequest.php` class that looks like this:

```php
class StoreUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}
```

We can use the `authorize()` method to determine if the user should be allowed to carry out the request. The method should return `true` if they can and `false` if they cannot. We can also use the `rules()` method to specify any validation rules that should be run on the request body. Both of these methods will run automatically before we manage to run any code inside our controller methods without us needing to manually call either of them.

So, let's move our authorization from the top of our controller's `store()` method and into the `authorize()` method. After we've done this, we can move the validation rules from the controller and into the `rules()` method. We should now have a form request that looks like this:

```php
class StoreUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return Gate::allows('create', User::class);
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'name'     => 'string|required|max:50',
            'email'    => 'email|required|unique:users',
            'password' => 'string|required|confirmed',
        ];
    }
}
```

Our controller should now also look like this:

```php
class UserController extends Controller
{
    public function store(StoreUserRequest $request): RedirectResponse
    {
        $user = User::create([
            'name'     => $request->name,
            'email'    => $request->email,
            'password' => $request->password,
        ]);

        $user->generateAvatar();
        $this->dispatch(RegisterUserToNewsletter::class);

        return redirect(route('users.index'));
    }

    public function unsubscribe(User $user): RedirectResponse
    {
        $user->unsubscribeFromNewsletter();

        return redirect(route('users.index'));
    }
}
```

Notice how in our controller, we've changed the first argument of the `store()` method from a `\Illuminate\Http\Request` to our new `\App\Http\Requests\StoreUserRequest`. We've also managed to reduce some of the bloat for the controller method by extracting it out into the request class.

**Note**: For this to work automatically, you'll need to make sure that your controller is using the `\Illuminate\Foundation\Auth\Access\AuthorizesRequests` and `\Illuminate\Foundation\Validation\ValidatesRequests` traits. These come automatically included in the controller that Laravel provides you in a fresh install. So, if you're extending that controller, you're all set to go. If not, make sure to include these traits into your controller.

## 2. Move Common Logic into Actions or Services

Another step that we could take to clean up the `store()` method could be to move out our "business logic" into a separate action or service class.

In this particular use case, we can see that the main functionality of the `store()` method is to create a user, generate their avatar and then dispatch a queued job that registers the user to the newsletter. In my personal opinion, an action would be more suitable for this example rather than a service. I prefer to use actions for small tasks that do only particular thing. Whereas for larger amounts of code that could potentially be hundreds of lines long and do multiple things, it would be more suited to a service.

So, let's create our action by creating a new `Actions` folder inside our `app` folder and then creating a new class inside this folder called `StoreUserAction.php`. We can then move the code into the action like this:

```php
class StoreUserAction
{
    public function execute(Request $request): void
    {
        $user = User::create([
            'name'     => $request->name,
            'email'    => $request->email,
            'password' => $request->password,
        ]);

        $user->generateAvatar();
        $this->dispatch(RegisterUserToNewsletter::class);
    }
}
```

Now we can update our controller to use the action:

```php
class UserController extends Controller
{
    public function store(StoreUserRequest $request, StoreUserAction $storeUserAction): RedirectResponse
    {
        $storeUserAction->execute($request);

        return redirect(route('users.index'));
    }

    public function unsubscribe(User $user): RedirectResponse
    {
        $user->unsubscribeFromNewsletter();

        return redirect(route('users.index'));
    }
}
```

As you can see, we've now been able to lift the business logic out of the controller method and into the action. This is useful because, as I mentioned before, controllers are essentially the "glue" for our requests and responses. So, we've managed to reduce the cognitive load for understanding what a method does by keeping the code logically separated. For example, if we want to check the authorization or validation, we know to check the form request. If we want to check what's being done with the request data, we can check the action.

Another huge benefit to abstracting the code out into these separate classes is that it can make testing a lot easier and faster. I've briefly talked about this in my past article about [how to make your Laravel app more testable](https://ashallendesign.co.uk/blog/how-to-make-your-laravel-app-more-testable); so I'd definitely recommend giving that a read if you haven't already.

### Using DTOs with Actions

Another great benefit of extracting your business logic into services and classes is that you can now use that logic in different places without needing to duplicate your code. For example, let's assume that we have a `UserController` that handles traditional web requests and an `Api\UserController` that handles API requests. For the sake of argument, we can make the assumption that the general structure of the `store()` methods for those controllers will be the same. But, what would we do if our API request we doesn't use an `email` field, but instead uses an `email_address` field? We wouldn't be able to pass our request object to the `StoreUserAction` class because it would be expecting a request object that has an `email` field.

To solve this issue, we can use <a href="https://en.wikipedia.org/wiki/Data_transfer_object" target="_blank" rel="noopener noreferrer">DTOs</a> (data transfer objects). These are a really useful way of decoupling data and passing it around your system's code without it being tightly coupled to anything (in this case, the request). To add DTOs to our project, we'll use Spatie's `spatie/data-transfer-object` <a href="https://github.com/spatie/data-transfer-object" target="_blank" rel="noopener noreferrer">package</a> and install it using the following Artisan command:

```
composer require spatie/data-transfer-object
```

Now that we have the package installed, let's create a new `DataTransferObjects` folder inside our `App` folder and create a new `StoreUserDTO.php` class. We'll then need to make sure that our DTO extends `Spatie\DataTransferObject\DataTransferObject`. We can then define our three fields like so:


```php
class StoreUserDTO extends DataTransferObject
{
    public string $name;
    
    public string $email;
	
    public string $password;
}
```

Now that we've done this, we can add a new method to our `StoreUserRequest` from before to create and return a `StoreUserDTO` class like so:

```php
class StoreUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return Gate::allows('create', User::class);
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'name'     => 'string|required|max:50',
            'email'    => 'email|required|unique:users',
            'password' => 'string|required|confirmed',
        ];
    }
  
    /**
     * Build and return a DTO.
     *
     * @return StoreUserDTO
     */
    public function toDTO(): StoreUserDTO
    {
        return new StoreUserDTO(
            name: $this->name,
            email: $this->email,
            password: $this->password,
        );
    } 
}
```

We can now update our controller to pass the DTO to the action class:

```php
class UserController extends Controller
{
    public function store(StoreUserRequest $request, StoreUserAction $storeUserAction): RedirectResponse
    {
        $storeUserAction->execute($request->toDTO());

        return redirect(route('users.index'));
    }

    public function unsubscribe(User $user): RedirectResponse
    {
        $user->unsubscribeFromNewsletter();

        return redirect(route('users.index'));
    }
}
```

Finally, we'll need to update our action's method to accept a DTO as an argument rather than the a request object:

```php
class StoreUserAction
{
    public function execute(StoreUserDTO $storeUserDTO): void
    {
        $user = User::create([
            'name'     => $storeUserDTO->name,
            'email'    => $storeUserDTO->email,
            'password' => $storeUserDTO->password,
        ]);

        $user->generateAvatar();
        $this->dispatch(RegisterUserToNewsletter::class);
    }
}
```

As a result of doing all of this, we have now completely decoupled our action from the request object. This means that we can reuse this action in multiple places across the system without being tied to a specific request structure. We would now also be able to use this approach in a CLI environment or queued job that isn't tied to a web request. As an example, if our application had the functionality to import users from a CSV file, we would be able to build the DTOs from the CSV data and pass it in to the action.

To go back to our original problem of having an API request that used `email_address` rather than `email`, we would now be able to solve it by simply building the DTO and assigning the DTO's email field the request's `email_address` field. Let's imagine that the API request had it's own separate form request class. It could look like this as an example:


```php
class StoreUserAPIRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return Gate::allows('create', User::class);
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'name'          => 'string|required|max:50',
            'email_address' => 'email|required|unique:users',
            'password'      => 'string|required|confirmed',
        ];
    }
  
    /**
     * Build and return a DTO.
     *
     * @return StoreUserDTO
     */
    public function toDTO(): StoreUserDTO
    {
        return new StoreUserDTO(
            name: $this->name,
            email: $this->email_address,
            password: $this->password,
        );
    } 
}
```

## 3. Use Resource or Single-use Controllers

A great way of keeping controllers clean is to ensure that they are either "<a href="https://laravel.com/docs/8.x/controllers#resource-controllers" target="_blank" rel="noopener noreferrer">resource controllers</a>" or "<a href="https://laravel.com/docs/8.x/controllers#single-action-controllers" target="_blank" rel="noopener noreferrer">single-use controllers</a>". Before we go any further and try to update our example controller, let's take a look at what both of these terms mean.

A resource controller is a controller that provides functionality based around a particular resource. So, in our case, our resource is the `User` model and we want to be able to perform all <a href="https://www.codecademy.com/articles/what-is-crud" target="_blank" rel="noopener noreferrer">CRUD</a> (create, update, update, delete) operations on this model. A resource controller typically contains `index()`, `create()`, `store()`, `show()`, `edit()`, `update()` and `destroy()` methods. It doesn't necessarily have to include all of these methods, but it wouldn't have any methods that weren't in this list. By using these types of controllers, we can make our routing RESTful. For more information about REST and RESTful routing, <a href="https://www.codecademy.com/articles/what-is-rest" target="_blank" rel="noopener noreferrer">check out this article here</a>.

A single-use controller is a controller that only has one public `__invoke()` method. These are really useful if you have a controller that doesn't really fit into one of the RESTful methods that we have in our resource controllers.

Based off the above information, we can see that the our `UserController` could probably be improved by moving the `unsubscribe` method to its own single-use controller. By doing this, we'd be able to make the `UserController` a resource controller that only includes resource methods.

So let's create a new controller using the following Artisan command:

```
php artisan make:controller UnsubscribeUserController -i
```

Notice how we passed `-i` to the command so that the new controller will be an invokable, single-use controller. We should now have a controller that looks like this:

```php
class UnsubscribeUserController extends Controller
{
    public function __invoke(Request $request)
    {
        //
    }
}
```

We can now move our method's code over and delete the `unsubscribe` method from our old controller:


```php
class UnsubscribeUserController extends Controller
{
    public function __invoke(Request $request): RedirectResponse
    {
        $user->unsubscribeFromNewsletter();

        return redirect(route('users.index'));
    }
}
```

Make sure that you remember to switch over your route in your `routes/web.php` file to call the use the `UnsubscribeController` controller rather than the `UserController` for this method.

## Conclusion

Hopefully this article has given you an insight into the different types of things you can do to clean up your controllers in your Laravel projects. Please remember though that the techniques I've used here are only my personal opinion. I'm sure that there are other developers that would use a totally different approach to building their controllers. The most important part is being consistent and using an approach that fits in with your (and your teams) workflow.

I'd love to hear in the comments what types of techniques you use for writing clean controllers.

If you also found this article useful, feel free to [sign up to my newsletter](https://ashallendesign.co.uk/blog) so that you can get notified whenever I release new posts similar to this one.

Keeping on building awesome stuff! 🚀