DEV Community

Thomas Emad
Thomas Emad

Posted on

DTO Design Pattern: Is It Over-Engineered?

Banner

Some people, when they try to explain DTOs, make it too simple just to be easy to understand — but they focus only on the explanation itself.

That makes it easy to understand, but anyone will ask:

  • Why should I use it?
  • Can't I just use the normal way? Isn't this over-engineering?

Like this:

namespace App\DTO;

class UserDTO
{
    public string $name;
    public string $email;
    public string $password;

    public function __construct(string $name, string $email, string $password)
    {
        $this->name = $name;
        $this->email = $email;
        $this->password = $password;
    }
}
Enter fullscreen mode Exit fullscreen mode

And when we want to use it:

public function createUser(Request $request)
{
    $userDTO = new UserDTO(
        $request->input('name'),
        $request->input('email'),
        $request->input('password')
    );

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

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

Anyone will say: Why am I using a DTO? I can just do this:

$user = $this->userService->createUser($request->all()); // $request->validate()
Enter fullscreen mode Exit fullscreen mode

And anyway, $fillable will protect me from mass assignment.

And to be honest with you: You are right.


Real-World Case: Big Data and Layers

Now, let's see a real-world case.

If we have big data and we transfer it across many layers, we usually use arrays. Just look at the $history structure in this example.

I use the same structure for:

  • HolidayShift
  • PublicHoliday
  • Errands
  • Leaves
  • OvertimeRequest

Some items may be 0, null, or may not exist at all.

public function getHolidayHistory($timeManagement, $overtime_schedule, $contract, $work_schedule, $publicHolidays, $attend)
{
    $holiday = [];

    foreach ($publicHolidays as $holiday) {
        if (Carbon::parse($attend->date)->between($holiday->start_date, $holiday->end_date)) {
            $latesSignInMinutes = $this->calculator->calculateLateSignIn($attend, $work_schedule->start_time);
            $earlyLeaving = $this->calculator->calculateEarlyLeaving($attend, $work_schedule->end_time);

            $history[] = [
                'absent'      => false,
                'type'        => 'holiday',
                'date'        => $attend->date,
                'item'        => $holiday,
                'attend_item' => $attend,
                'data'        => [
                    'sign_in'                        => $attend->sign_in,
                    'sign_out'                       => $attend->sign_out,
                    'late'                           => $latesSignInMinutes,
                    'late_deducted_percent'          => 0,
                    'early_leaving_deducted_percent' => 0,
                    'early_leaving'                  => $this->calculator->checkFromFingerprintOut($timeManagement, $earlyLeaving),
                    'deducted_percent'               => 0,
                    'overtime'                       => Carbon::parse($attend->duration)->hour,
                    'amount_overtime'                => $this->presentPublicHolidays->getPresentPublicHolidayAmount(
                        $overtime_schedule,
                        $contract,
                        $attend->duration
                    ),
                ],
            ];
        }
    }

    $history[] = [
        'absent'      => true,
        'type'        => 'holiday',
        'date'        => $attend->date,
        'attend_item' => $attend,
    ];

    return $history;
}
Enter fullscreen mode Exit fullscreen mode

Do you see the potential problems here?

From a coding perspective, we can make it work without errors. But realistically, we are never 100% sure.

Of course, for big features we use testing — but in cases like this, TDD is not always realistic. So if some item does not exist, every time we use $history we have to write:

isset($history['x']) ? $history['x'] : 0; // or
$history['x'] ?? 0;
Enter fullscreen mode Exit fullscreen mode

Can We Use DTOs Here?

$history[] = new AttendanceHistoryDTO(
    absent: false,
    type: 'holiday',
    date: $attend->date,
    item: $holiday,
    attend_item: $attend,
    data: new AttendanceDataDTO(
        sign_in: $attend->sign_in,
        sign_out: $attend->sign_out,
        total_late: $latesSignInMinutes,
        early_leaving: $this->calculator->checkFromFingerprintOut($timeManagement, $earlyLeaving),
        deducted_percent: 0,
        overtime: Carbon::parse($attend->duration)->hour,
        amount_overtime: $this->presentPublicHolidays->getPresentPublicHolidayAmount(
            $overtime_schedule,
            $contract,
            $attend->duration
        ),
        late_deducted_percent: 0,
        early_leaving_deducted_percent: 0,
    )
)
Enter fullscreen mode Exit fullscreen mode

Now, any time we use $history, we don't need to worry about:

isset($history['x']) ? $history['x'] : 0;
Enter fullscreen mode Exit fullscreen mode

Because the default values live inside the DTO itself.


Handling Missing Data in Real Use

Now look at this real usage example:

isset($content?->data?->total_late) ? $content?->data?->total_late : 0;
Enter fullscreen mode Exit fullscreen mode

Why am I still doing this even with DTOs?

Because when you use DTOs:

  • If you write something that does not exist, your editor will warn you.
  • PHP will throw an exception in development and testing — and that's good.
  • But throwing exceptions in production is not always a good idea 😅

The Right Approach: Focus on Business Needs

There are many correct ways to use DTOs.

But always focus on your business needs first. Understand the problem, then choose the right tool.

Personally, I prefer to:

Write the code badly → make it normal → then ask: what is the best solution?

What do you think? 😃

Top comments (0)