Single Responsibility Principle in a Laravel Project

oliveiratheone

Gabriel Oliveira

Posted on October 11, 2023

Single Responsibility Principle in a Laravel Project

This principle declare that a class must be specialized in a single subject and have a single responsibility within the software, in other words, a class must have a single or action to perform.

Violate that principle can bring some problems such as:

  • Lack of cohesion
  • High coupling
  • Difficulties reusing the code
  • Difficulty to implement automated tests

Example

class UserController extends Controller
{
    public function register(Request $request): JsonResponse
    {
        $request->validate([ // Request validation
            'name'  => ['required', 'string'],
            'email' => ['required', 'email'],
            'role'  => ['required', new Enum(UserRoles::class)],
        ]);

        $user = new User();
        $user->name = $request->input('name');
        $user->email = $request->input('email');
        $user->role = $request->input('role');
        $user->password = Hash::make($request->input('password'));
        $user->save(); // Interact with daatabase

        if ($user->role === 'customer') { // Business Rule
            Mail::to($user->email)
            ->send(new WelcomeCustomerEmail($user)); // Email Dispatch
        }

        return response()->json(['id' => $user->getKey()], 201);
    }
}
Enter fullscreen mode Exit fullscreen mode

As we can see above, the class UserController has several responsibilities:

  • Validate request body
  • Save user in the database
  • Business Rule
  • Send welcome email if user is customer

When actually, the function of a controller is just receive requests and return responses. We must separate these responsibilities into other layer, such as:

  • Layers
  • Requests validators (In laravel you can use Form Requests, but in other frameworks you can create some middlewares)
  • Controller - ( Receive requests and return response )
  • Repository - ( Interact with database )
  • Service - ( Business Rule )

Request validator (Form Request)

class UserRegisterRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'  => ['required', 'string'],
            'email' => ['required', 'email'],
            'role'  => ['required', new Enum(UserRoles::class)],
        ];   
    }
}
Enter fullscreen mode Exit fullscreen mode

Controller

class UserController extends Controller
{
    public function __construct(
        protected UserService $userService
    ) {
    }

    public function register(RegisterUserRequest $request): JsonResponse
    {
        $dto = new RegisterUserDto($request->all()); 

        $user = $this->userService->register($dto);

        return response()->json(['id' => $user->getKey()], Response::HTTP_CREATED);
    }
}
Enter fullscreen mode Exit fullscreen mode

I use a RegisterUserDto, where ‘Dto’ stands for ‘Data Transfer Object.’ This is employed when transferring data across different layers.

Service

class UserService
{
    public function __construct(
       protected UserRepository $userRepository,
    ) {
    }

    public function register(RegisterUserDto $dto): User
    {
        $user = $this->userRepository->save($dto);

        if ($user->isCustomer()) {
            $this->sendWelcomeEmail($user);        
        }

        return $user;
    }

    protected function sendWelcomeEmail(User $user): void
    {
        Mail::to($user->email)->send(new WelcomeUserEmail($user));
    }
}
Enter fullscreen mode Exit fullscreen mode

Repository

class UserRepository
{
    public function save(RegisterUserDto $dto): User
    {
        return User::create($dto->toArray());
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that project is more flexible to implement new features, your code became more readable, in tests, now you can easily utilize mocks for your repository, for example, so you don’t need to interact with the database, which would slow down your tests.

Tip

If you application use the pattern to return created responses with entity id, you can add a helper method in your Base Controller.

Base Controller

class Controller extends BaseController
{
    protected function created(int $id): JsonResponse
    {
        return response()->json(['id' => $id], Response::HTTP_CREATED);
    }    
}
Enter fullscreen mode Exit fullscreen mode

UserController

class UserController extends Controller
{
    public function __construct(
        protected UserService $userService
    ) {
    }

    public function register(RegisterUserRequest $request): JsonResponse
    {
        $dto = new RegisterUserDto($request->validated()); 

        $user = $this->userService->register($dto);

        return $this->created($user->getKey());
    }
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
oliveiratheone
Gabriel Oliveira

Posted on October 11, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related