When building Laravel applications, we often rely on Form Requests for validation. They’re great for keeping controllers clean and ensuring input data is valid before reaching business logic.
But here’s the problem:
After validation, many developers still use raw arrays or the request object itself to pass data deeper into their application services, repositories, or jobs. This can lead to messy, fragile code that’s hard to maintain.
This is where DTOs (Data Transfer Objects) come in. A DTO acts as a simple, structured object to carry data safely and clearly through your application.
Let’s see how we can use DTOs inside Laravel Form Requests.
đź› User Registration Flow
Imagine you’re building a user registration feature. Your form collects these inputs:
- name
- password
Step 1: Create a Form Request
<?php
declare(strict_types=1);
namespace App\Http\Requests\Auth;
use App\Http\Requests\Auth\Data\RegisterUserData;
use Illuminate\Foundation\Http\FormRequest;
class RegisterUserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'string', 'min:8'],
];
}
public function dto(): RegisterUserData
{
return new RegisterUserData(
$this->input('name'),
$this->input('email'),
$this->input('password'),
);
}
}
Here, we’ve added a dto()
method to return a structured object instead of raw inputs.
Step 2: Define the DTO
<?php
declare(strict_types=1);
namespace App\Http\Requests\Auth\Data;
class RegisterUserData
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $password,
) {}
}
This class represents the validated, immutable registration data.
Step 3: Use the DTO in Your Controller
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\RegisterUserRequest;
use App\Services\Auth\RegisterUserService;
class RegisterController extends Controller
{
public function __construct(
private RegisterUserService $registerUserService
) {}
public function store(RegisterUserRequest $request)
{
$userData = $request->dto();
$user = $this->registerUserService->register($userData);
return response()->json([
'message' => 'User registered successfully!',
'user' => $user,
]);
}
}
Now your controller isn’t dealing with arrays or $request->input()
. Instead, it passes a clean RegisterUserData
DTO to the service.
Step 4: Service Layer Uses the DTO
<?php
declare(strict_types=1);
namespace App\Services\Auth;
use App\Http\Requests\Auth\Data\RegisterUserData;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class RegisterUserService
{
public function register(RegisterUserData $data): User
{
return User::create([
'name' => $data->name,
'email' => $data->email,
'password' => Hash::make($data->password),
]);
}
}
The service doesn’t care about request objects. It only knows it needs a RegisterUserData
object. This makes it reusable in CLI commands, jobs, or tests.
đź’ˇ Another Example: Payment Checkout
Consider a checkout request with fields like:
- amount
- currency
- payment_method
Instead of juggling arrays like $request->only(['amount', 'currency', 'payment_method'])
, you can use a DTO:
$checkoutData = $request->dto();
$this->paymentService->process($checkoutData);
This makes your code much clearer and type-safe.
âś… Benefits of Using DTOs in Form Requests
- Clarity:
$request->dto()
is cleaner than$request->only(['field1', 'field2'])
. - Type Safety: IDE autocompletion + compile-time checks help prevent mistakes.
- Immutability: DTOs don’t change once created.
- Separation of Concerns: Controllers handle requests, services handle logic, DTOs handle data.
🚀 Conclusion
DTOs may seem like a small addition, but they can greatly improve the maintainability and readability of your Laravel projects.
Next time you create a Form Request, don’t just validate inputs, wrap them into a DTO. Your future self (and your teammates) will thank you!
Top comments (2)
There is a difference between
safe
andinput
. Whilesafe
is taking the value from the validator,input
is taking the value from the request. So if the validator has andafter
orpassedValidation
method the output can be not what you expect it to be.If you really want to keep using the
validated
orsafe
method you could overwrite them.I would do that only in case you want all the
FormRequest
classes to return DTOs. Otherwise thedto
method is the way to go.styling side note: add php after the opening ticks of the code blocks, this adds syntax highlighting.
Thank you for the valuable feedback 🙏. You make an excellent point about the distinction between
safe()
(validator output) andinput()
(raw request data), especially in cases whereafter()
orpassedValidation()
modifies the values.Overriding
safe()
to always return a DTO is an elegant approach for projects where consistency across all FormRequests is required. For now, I’ll continue with thedto()
method to maintain flexibility, but I’ll definitely consider adding this alternative as an advanced option in the article.Also, thank you for the styling tip. I’ll update my code blocks with php for syntax highlighting 👍.