Skip to main content

DTOs: Typed Data Between Layers

Premium
7 min read

In the previous lesson, we moved validation into a Form Request. Now, the $request->validated() method returns an array of validated data. But what happens next? That array gets passed to a Service or Action class.

The problem with arrays is that they have no structure. You don't know what keys are inside without checking. If you misspell a key, you won't get an error — just a null.

This is where DTOs (Data Transfer Objects) come in. A DTO is a simple class whose only job is to carry data between layers — from the Form Request to the Service, or from the Service to the Controller.

As with everything in this course: DTOs are optional. For simple CRUD, an array is perfectly fine. But as your data flow gets more complex, DTOs help you stay organized.


Plain PHP DTO

The simplest DTO is just a class with typed public properties. No framework, no package — just PHP.

php artisan make:class DTOs/CreateUserDTO

app/DTOs/CreateUserDTO.php:

namespace App\DTOs;
 
class CreateUserDTO
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $password,
public readonly array $roles = [],
public readonly ?string $start_at = null,
) {}
 
public static function fromRequest(array $validated): self
{
return new self(
name: $validated['name'],
email: $validated['email'],
password: $validated['password'],
roles: $validated['roles'] ?? [],
start_at: $validated['start_at'] ?? null,
);
}
}

Notice we use readonly properties — once a DTO is created, its data shouldn't change. This makes the data flow predictable.

Now, in the Controller, instead of passing a raw array to the Service:

public function store(StoreUserRequest $request, UserService $userService)
{
$dto = CreateUserDTO::fromRequest($request->validated());
$user = $userService->create($dto);
 
// ...
}

And the Service accepts the DTO instead of an array:

app/Services/UserService.php:

use App\DTOs\CreateUserDTO;
 
class UserService
{
public function create(CreateUserDTO $dto): User
{
$user = User::create([
'name' => $dto->name,
'email' => $dto->email,
'password' => $dto->password,
'start_at' => $dto->start_at,
]);
 
$user->roles()->sync($dto->roles);
 
return $user;
}
}

Now you get autocomplete in your IDE, type checking from PHP, and it's immediately clear what data the Service expects.

The Full Lesson is Only for Premium Members

Want to access all of our courses? (34 h 11 min)

You also get:

58 courses
Premium tutorials
Access to repositories
Private Discord
Get Premium for $129/year or $29/month

Already a member? Login here

Comments & Discussion

No comments yet…

We'd Love Your Feedback

Tell us what you like or what we can improve

Feel free to share anything you like or dislike about this page or the platform in general.