DEV Community

Cover image for Laravel — Your controllers should look like this
Thiago Maciel
Thiago Maciel

Posted on

Laravel — Your controllers should look like this

The scenario

It’s not rare to start working on a project and need to deal with god classes controllers. Do you know what I mean? Who never faced a controller store method that validates, persists, creates a relation, sends emails, …, and return a response?

I see it as a very common approach in the Laravel community, while the Rails devs tend to do almost the same in models classes. I don’t think it is that bad to have something else (in addition to handle the requests and responses) inside the controllers in some specific situations, but in general I would prefer to use the first principle of SOLID: Single-responsibility principle (SRP) doing a little more now to make it easier in the future.

The benefits of moving business logic from controllers to new classes are countless, but let me mention three of them:

  • You’ll be able to reuse your code
  • Your code will be easy to understand and maintain
  • You’ll be the responsible for part of the next developer happiness 😊

funny developer


Let’s see how to apply this concept refactoring a controller

To make it simple let’s see an example where a UserController just validates the data, creates a new user, his roles and redirects with a status message:

public function store(Request  $request)
{
  # Validate data
  $request->validate([
    'name' => 'required',
    'email' => 'required|unique:users',
    'phone_number' => 'required',
  ]);

  # Create a user
  $user = User::create([
      'name' => $request->name,
      'email' => $request->email,
      'phone_number' => $request->phone_number,
  ]);

  # Create the user's roles
  if(!empty($request->roles)) {
    $user->roles()->sync($request->roles);
  }

  #  Redirect with a status message
  return redirect()
    ->route('user.index')
    ->with('message','User created successfully.');
}
Enter fullscreen mode Exit fullscreen mode

It looks fine, right? But we can make it better. First of all, lets move this validation to a Form Request by running this command:
php artisan make:request StoreUserRequest

It will create a new file in this directory: App\Http\Requests. I’ll just authorize the use and cut my validation rules from controller and paste it in the rules method like this:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
          'name' => 'required',
          'email' => 'required|unique:users',
          'phone_number' => 'required',
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

How to use this class:

All you need to do is type-hint the request on your controller method. The incoming form request is validated before the controller method is called, meaning you do not need to clutter your controller with any validation logic

use App\Http\Requests\StoreUserRequest;


public function store(StoreUserRequest $request)
{
  # Create a user
  $user = User::create([
      'name' => $request->name,
      'email' => $request->email,
      'phone_number' => $request->phone_number,
  ]);

  # Create the user's roles
  if(!empty($request->roles)) {
    $user->roles()->sync($request->roles);
  }

  #  Redirect with a status message
  return redirect()
    ->route('user.index')
    ->with('message','User created successfully.');
}
Enter fullscreen mode Exit fullscreen mode

A little better. Now we are going to create a new service class. This one will be the responsible for creating Users.

Since we don’t have artisan commands to do it, we will need to create it manually. Inside the app directory I have created a service folder with my new class: app/Services/UserService.php and moved the logic from the controller to this class:

<?php
namespace App\Services;

use App\Models\User;
use Illuminate\Http\Request;

class UserService
{
    public function createUser(Request $request): User
    {
        # Create a user
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'phone_number' => $request->phone_number,
        ]);

        # Create user's roles
        if(!empty($request->roles)) {
          $user->roles()->sync($request->roles);
        }
        return $user;
    }
}
Enter fullscreen mode Exit fullscreen mode

And now, in the controller, lets import and type-hint the dependency we just created:

use App\Services\UserService;

public function store(StoreUserRequest $request, UserService $userService)
{
    $userService->createUser($request);

    return redirect()
      ->route('user.index')
      ->with('message','User created successfully.');
}
Enter fullscreen mode Exit fullscreen mode

Could you notice the difference?
Yeah, now we have more classes and files to work with, but in the other hand each one of them have it’s own responsibilities, they can be reused in other parts of the application and individually tested.

Not to mention the controller that is now responsible only to receive the request, call the right service and return a response.

Prettier, isn’t it?


Should I use service classes?

Well it’s up to you to evaluate and decide what will work for your project. I personally prefer it, but I already have worked on projects where the approach was totally different.

It is not a general rule, but when building a new application you should consider to keep each class as clean as possible including your controllers.

Thank you for reading!

thumbs up

Top comments (2)

Collapse
 
glorian profile image
Igor Biletskiy

In this case, your service is UseCase, and you should not pass Request there. The reason is simple, you violate SRP. Your UseCase (or in this context - service) must not know anything about external request (it is controller's responsibility to get data from outside and give only necessary data to your service).

You may also need to run UseCase in CLI mode (as a scheduled or asynchronous task). In this case, you have no request to pass to the service as a dependency.

To summarize: DTOs are better for transferring data between layers (in our case Application -> Infrastructure). You can also just pass the necessary data directly in the service arguments.

Collapse
 
dnsinyukov profile image
Denis Sinyukov

Hi, I would not use request in the service, because the service must work with persistent data