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;
}
}
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);
}
Anyone will say: Why am I using a DTO? I can just do this:
$user = $this->userService->createUser($request->all()); // $request->validate()
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:
HolidayShiftPublicHolidayErrandsLeavesOvertimeRequest
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;
}
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;
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,
)
)
Now, any time we use $history, we don't need to worry about:
isset($history['x']) ? $history['x'] : 0;
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;
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?

Top comments (0)