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.