DEV Community

A0mineTV
A0mineTV

Posted on

Using Data Transfer Objects (DTO) in Laravel for a Clean and Scalable Architecture

In a Laravel application, it's important to maintain readable, maintainable, and scalable code. One approach you can take to improve your code structure is by using Data Transfer Objects (DTO). This technique helps separate concerns by creating objects that only carry data between different layers of an application.

In this article, we'll walk through how to implement a DTO in Laravel to organize user creation, while utilizing a Service to encapsulate business logic.

Why Use DTOs ?

DTOs offer several advantages:

  • Separation of Concerns: They help keep different layers of your application properly separated.
  • Code Clarity: By sending simple objects that only contain data, you improve the readability and maintainability of your code.
  • Scalability: DTOs make your code easier to test and extend without duplicating logic.

Now, let's see how we can integrate this approach into a Laravel application.

Application Structure

Let’s imagine we have an application that manages users. We’ll create an API route to add a new user, using a DTO and a dedicated Service for business logic.

1. The DTO (UserDTO)

A Data Transfer Object contains only data and should not have any business logic. Here's a simple example of a DTO for a user:

<?php

declare(strict_types=1);

namespace App\DTO;

final class UserDTO
{
    public function __construct(
        public string $name,
        public string $email,
        public string $phone,
    )
    {}
}
Enter fullscreen mode Exit fullscreen mode

In this case, we have a UserDTO class with three properties: name, email, and phone. This class is simple and does not contain any business logic. It is used solely to transport the user data between layers.

2. The Service (UserService)

Now that we have our DTO, it's time to introduce business logic within a dedicated Service. The idea is that our controller should contain only the logic related to receiving and responding to the API, while all business logic should be placed in dedicated services.

Here’s an example of a Service for creating a user:

<?php

declare(strict_types=1);

namespace App\Services;

use App\DTO\UserDTO;
use App\Models\User;

final class UserService
{
    public function createUser(UserDTO $userDTO): User
    {
        $user = new User();

        $user->name = $userDTO->name;
        $user->email = $userDTO->email;
        $user->phone = $userDTO->phone;

        $user->save();

        return $user;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this service, we receive a UserDTO object and then use the data from that DTO to create a new user in the database. This service encapsulates all the business logic related to creating a user, which allows the controller to remain lightweight and simple.

3. The Controller (UserController)

The controller is responsible for managing the HTTP request and response. It shouldn’t contain complex business logic. Here, we validate incoming data with a FormRequest, create a UserDTO, and call our service to perform the business logic.

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\DTO\UserDTO;
use App\Http\Requests\UserRequest;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;

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

    public function store(UserRequest $request): JsonResponse
    {
        $request->validated();

        $userDTO = new UserDTO(
            $request->input('name'),
            $request->input('email'),
            $request->input('phone'),
        );

        $user = $this->userService->createUser($userDTO);

        return response()->json($user);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this controller, the store method:

  1. Validates the request using a FormRequest (UserRequest).
  2. Creates a UserDTO with the validated data.
  3. Uses the UserService to create a user in the database.
  4. Returns a JSON response with the created user's data.

4. The FormRequest (UserRequest)

The FormRequest is used to validate data sent by the user. You can also use it to transform the data before sending it to the DTO.

<?php

declare(strict_types=1);

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users,email',
            'phone' => 'nullable|string|max:15',
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Conclusion

In summary, using Data Transfer Objects (DTOs) in Laravel helps encapsulate data and cleanly separate business logic from the application. We’ve seen how to use a DTO in a controller, how to separate business logic into a service, and how to validate and transform data with a FormRequest.

This approach makes your code cleaner, more modular, and easier to maintain. Additionally, it facilitates the addition of future features while ensuring code readability and testability.

Top comments (0)