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.
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.
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.
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.
UserRegistrationDTO in app/DTOs that accepts email (string), name (string), and age (int).fromArray(array $data) method to your DTO that performs manual casting.UserRegistrationDTO.?string) rather than omitting it, to keep the contract clear.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.
Master Testing DDD components in Laravel. Learn to mock external services, isolate domain logic, and write reliable PHPUnit tests for your Action classes.
Read moreLearn to build a Modular Monolith by structuring your Laravel directory by domain, enforcing encapsulation, and defining public interfaces for module communication.
Utilizing Data Transfer Objects (DTOs)
Custom Middleware Development
Database Connection Pooling
Handling Large Data Exports
Security Header Configuration
Database Sharding Concepts
Real-time Data Synchronization
Database Deadlock Prevention
Managing Third-Party API Integrations