My point of view on DTOs pattern with Laravel

In our applications, we often need to transfer data inside our codebase, from point A to point B. Point A and point B can be different technical entities inside our code. Now days it's all about data.

Examples of data transfers that we may need on a Laravel project (from -> to):

  • Controller -> Service class
  • Service -> Job
  • Service -> Action
  • Controller -> Action
  • Controller -> Service -> Mail
  • Controller -> Service -> Resource

Of course a lot more.

Depends on your architecture and use cases, definitely.

First of all, I strongly believe using DTOs is some kind of over-engineering if your project is small. You can just save some time by not adding this extra layer. Keep things simple, if your project is tiny, especially if you know that it will stay like that.

When DTOs start to make sense, is when we are talking about apps that start to grow, have to be maintained through the years, and more than just a single person work on it.

Also I would never use them, for very small data needs. It doesn't make a lot of sense to surround your 1 property data to a DTO class, just use that damn property.

And don't get me wrong, this is my personal point of view. Searching on the Internet for DTOs in Laravel, can easily leave you extremely confused. Other people like them, other don't and swear by it. At the end of the day, you just want to decide with your team if it is a good idea to have them, and keep your codebase consistent, to produce the best software as possible. Don't blindly use whatever exciting you see.

Below is an example of DTOs usage. I will try to explain why I personally like them.

<?php

namespace App\DataTransferObjects\Users;

use App\Http\Requests\CreateUserRequest;
use App\Http\Requests\UserCreationWebhookRequest;
use App\Models\Country;
use App\Models\Industry;
use Carbon\Carbon;
use Illuminate\Support\Str;

readonly class CreateUserData
{
    public function __construct(
        public string $name,
        public string $email,
        public string $password,
        public Country $country,
        public Carbon $bornAt,
        public ?Industry $industry,
    )
    {

    }

    public static function fromRequest(CreateUserRequest $request): static
    {
        $validatedData = $request->validated();

        return new static(
            name: $validatedData['name'],
            email: $validatedData['email'],
            password: $validatedData['password'],
            country: Country::query()->find($validatedData['country']),
            bornAt: Carbon::createFromFormat('Y-m-d', $validatedData['born_at']),
            industry: ($validatedData['industry'] ?? null) ? Industry::query()->find($validatedData['industry']) : null,
        );
    }
}

It's a simple DTO class that holds data for user creation. We have got some string fields, a Carbon born date and 2 models, Country and Industry. Also note that industry can be null.

Let's see some benefits that we get.

  1. IDE auto-completion for our crucial data is nifty.

Who doesn't like:

Don't know about you guys, but being able to have auto-completion on my PHPStorm IDE, just feels awesome. I also know the types, and if any of them can be null or not.

  1. Passing data to another class becomes more manageable.

Otherwise we would have to pass, either different parameters for each property, or an array that contains everything.

The first one is OK, but it becomes a bit messy when you have many parameters (let's say more than 4-5).

The latter is far from ideal for me. Arrays are great, but when we have a lot of data to manage, they become messy too. Think if we had 15 properties. Error prone and slower to debug.

  1. Separation of concerns improved

Now:

  • Our request class validates the HTTP body
  • DTO maps and holds our final readonly ready to use data
  • Service executes our business logic, accepting as params, structured data.

Each class for their purpose. It feels cleaner. Single responsibility is there.

  1. Data mapping from another source, is more straight forward

What if we wanted to also create a user, not from a user's request, but let's say a webhook request. Having our DTO as first class citizen for our data, feels clean again. We clearly have a place to map our data:

  1. New developers can figure out data easier

Simply by going to CreateUserData class, looking on the constructor, they can figure out all the final type hinted data that this object can have.

Without the DTO, they would have to go on the request class & read the rules. Hopefully we would not transform the request's data later inside the service, which could make their life harder, to debug stuff. Especially with larger data. I like to code with the "new developer on the codebase" mindset, because I wish others did too, when I see new code.

Yes, we added another layer of complexity, our DTO object, where there we use the non type hinted request class properties (PHP is PHP). But it's just there. You are dealing with it in one single class, our DTO class.

Still feels like over engineering? Then simply don't use them and enjoy.

Subscribe to Lioy

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe