DRY principle with Laravel

Consider a simple module that is responsible for adding new users.

And by his example we will see what possibilities the application of the DRY principle opens up.

For me, the principle of DRY (Don't Repeat Yourself) has always been embodied in two basic definitions:

  1. Duplication of knowledge is always a violation of the principle
  2. Code duplication is not always a violation of the principle

Let's start with the controllers containing the minimum amount of logic.

class UserController
{
    public function create(CreateRequest $request)
    {
        $user = User::create($request->all());
        
        return view('user.created', compact('user'));
    }
}

class UserApiController
{
    public function create(CreateRequest $request)
    {
        $user = User::create($request->all());
        
        return response()->noContent(201);
    }
}

At the initial stage, such a repetition of the code seems rather harmless.
But we already have duplication of knowledge, and duplication of knowledge is prohibited.
To do this, we generalize user creation in the UserService class

class UserService
{
    public function create(array $data): User
    {
        $user = new User;
        $user->email = $data['email'];
        $user->password = $data['password'];
        $user->save();

        return $user;
    }
    
    public function delete($userId): bool 
    {
        $user = User::findOrFail($userId);
 
        return $user->delete();
    }    
}

Having moved all the logic of working with the model to the service, we get rid of its duplication in the controller. But we have another problem. Let's say we have to complicate the process of creating a user a little.

class UserService
{
    protected $blogService;
    
    public function __construct(BlogService $blogService)
    {
        $this->blogService = $blogService;
    }

    public function create(array $data): User
    {
        $user = new User;
        $user->email = $data['email'];
        $user->password = $data['password'];
        $user->save();
        
        $blog = $this->blogService->create();
        $user->blogs()->attach($blog);

        return $user;
    }
    
    //Other methods
}

Gradually, the UserService class will begin to grow and we run the risk of getting a super class with a huge number of dependencies.

CreateUser Single Action Class


In order to avoid such consequences, you can break the service into classes of a single action.

Basic requirements for this class:

  • Name representing the action to be performed
  • Has a single public method (I will use the __invoke magic method )
  • Has all the necessary dependencies
  • Ensures compliance with all business rules within itself, generates an exception when they are violated

class CreateUser
{
    protected $blogService;

    public function __construct(BlogService $blogService)
    {
        $this->blogService = $blogService;
    }

    public function __invoke(array $data): User
    {
        $email = $data['email'];
        
        if (User::whereEmail($email)->first()) {
            throw new EmailNotUniqueException("$email should be unique!");
        }
        
        $user = new User;
        $user->email = $data['email'];
        $user->password = $data['password'];
        $user->save();

        $blog = $this->blogService->create();
        $user->blogs()->attach($blog);

        return $user;
    }
}

We already have an email field check in the CreateRequet class, but it’s logical to add a check here too. This more accurately reflects the business logic of creating a user, and also simplifies debugging.

Controllers take the following form

class UserController
{
    public function create(CreateRequest $request, CreateUser $createUser)
    {
        $user = $createUser($request->all());

        return view('user.created', compact('user'));
    }
}

class UserApiController
{
    public function create(CreateRequest $request, CreateUser $createUser)
    {
        $user = $createUser($request->all());

        return response()->noContent(201);
    }
}

As a result, we have a completely isolated logic for creating a user. It is convenient to modify and expand.

Now let's see what advantages this approach gives us.

For example, there is a task to import users.

class ImportUser
{
    protected $createUser;
    
    public function __construct(CreateUser $createUser)
    {
        $this->createUser = $createUser;
    }
    
    public function handle(array $rows): Collection
    {
        return collect($rows)->map(function (array $row) {
            try {
                return $this->createUser($row);
            } catch (EmailNotUniqueException $e) {
                // Deal with duplicate users
            }
        });
    }
}

We get the opportunity to reuse the code by embedding it in the Collection :: map () method. And also process for our needs users whose email addresses are not unique.

Dressing


Suppose we need to register each new user in a file.

To do this, we will not embed this action in the CreateUser class itself, but use the Decorator pattern.

class LogCreateUser extends CreateUser 
{
    public function __invoke(array $data)
    {
        Log::info("A new user has registered: " . $data['email']);
        
        parent::__invoke($data);
    }
}

Then, using the Laravel IoC container , we can associate the LogCreateUser class with the CreateUser class , and the first will be implemented every time we need an instance of the second.

class AppServiceProvider extends ServiceProvider
{

    // ...

    public function register()
    {
        $this->app->bind(CreateUser::class, LogCreateUser::class);
    }

We also have the opportunity to make the user creation setting using a variable in the configuration file.

class AppServiceProvider extends ServiceProvider
{

    // ...

    public function register()
    {
         if (config("users.log_registration")) {
             $this->app->bind(CreateUser::class, LogCreateUser::class);
         }
    }

Conclusion


Here is a simple example. Real benefits begin to show up as complexity begins to grow. We always know that the code is in one place and its boundaries are clearly defined.

We get the following advantages: prevents duplication, simplifies testing
and opens the way to the application of other design principles and patterns.

All Articles