Utilizing Data Transfer Objects (DTOs) for Type-Safe Laravel
Stop passing associative arrays through your application. Learn to use DTOs to enforce type safety and data integrity across your Laravel architecture.
Previously in this course, we explored implementing action classes to encapsulate our business logic. While actions keep our controllers thin, passing loose associative arrays between them creates a "hidden contract" problem where types are guessed and keys are easily forgotten.
This lesson adds Data Transfer Objects (DTOs) to our architectural toolkit. By replacing unstructured arrays with strict, immutable objects, we gain compile-time confidence and runtime Data Integrity.
The Problem with Associative Arrays
In a high-traffic SaaS environment, an associative array is a liability. Consider a CreateSubscription action:
PHP#6A9955">// The "Array Hell" approach public function handle(array $data) { return Subscription::create([ 'user_id' => $data['user_id'], #6A9955">// What if this is missing? 'plan_id' => $data['plan_id'], #6A9955">// Is this an int or a string? ]); }
When you pass an array, you lose static analysis capabilities. You don't know if user_id is present, if it's the right type, or if the key was typoed as userid. By utilizing a DTO, we formalize the schema.
Creating Type-Safe DTOs
A DTO is a simple object whose only job is to carry data. In modern PHP, we use readonly properties and constructor promotion to keep them concise and immutable.
PHPnamespace App\DTOs; readonly class CreateSubscriptionDTO { public function __construct( public int $userId, public string $planId, public ?string $promoCode = null, ) {} public static function fromRequest(\Illuminate\Http\Request $request): self { return new self( userId: (int) $request->user()->id, planId: $request->validated('plan_id'), promoCode: $request->validated('promo_code'), ); } }
By defining this class, we ensure that any service receiving this DTO knows exactly what data is available. This is the foundation of Type Safety in our domain layer.
Validating Data Flow Across Services
To ensure our DTOs are always valid, we perform validation before instantiation. We use Laravel's FormRequest to handle the heavy lifting of input validation, then hydrate the DTO.
In our project, we are currently re-architecting our billing module. Instead of passing the request object into our ProcessPayment action, we now require a PaymentDTO.
PHP#6A9955">// Inside a Controller public function store(PaymentRequest $request) { $dto = PaymentDTO::fromRequest($request); #6A9955">// The action now has a strict contract return $this->paymentAction->execute($dto); }
This pattern ensures that by the time our domain logic runs, the data is already cleaned, cast, and validated.
Hands-on Exercise
- Define a DTO: Create a
UserRegistrationDTOinapp/DTOsthat acceptsemail(string),name(string), andage(int). - Implement Hydration: Add a static
fromArray(array $data)method to your DTO that performs manual casting. - Refactor an Action: Take an existing action that accepts an array and update the signature to accept your new
UserRegistrationDTO.
Common Pitfalls
- Over-engineering: Do not turn your DTOs into mini-models. They should not contain business logic or database interactions. If you find yourself adding methods to query the database, you've crossed the line into Entity territory.
- Performance overhead: While creating objects has a negligible memory cost, avoid creating thousands of DTOs inside a tight loop if you are processing massive datasets.
- Incomplete Hydration: Always ensure your DTOs are fully populated. If a property can be null, explicitly mark it as nullable (
?string) rather than omitting it, to keep the contract clear.
Recap
We've moved from passing "blind" arrays to using strict, immutable DTOs. This change shifts our error surface from runtime (where things break in production) to development time (where IDEs and static analysis tools like PHPStan can catch our mistakes). We have enforced a strict data contract that will make our SaaS platform significantly easier to refactor as we scale.
Up next: Service Layer Pattern — we'll learn how to orchestrate these DTOs across multiple domains using dedicated service classes.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.