Data Transfer Object (DTO)
debt(d5/e3/b5/t5)
Closest to 'specialist tool catches it' (d5). PHPStan and Psalm (cited in detection_hints.tools) can detect type mismatches and missing readonly modifiers, but cannot automatically distinguish a proper immutable DTO from a mutable data class with setters, or detect when arrays are used instead of DTOs across boundaries. Requires static analysis configuration and architectural rules.
Closest to 'simple parameterised fix' (e3). The quick_fix shows a single-line constructor promotion pattern to create proper DTOs. Converting an existing array-based approach to typed DTOs typically requires creating new DTO classes and updating method signatures in one component, but doesn't require cross-cutting architectural changes.
Closest to 'persistent productivity tax' (b5). DTOs apply across web, cli, and queue-worker contexts per applies_to. Once adopted, every layer boundary crossing needs a corresponding DTO, creating ongoing maintenance overhead. However, they don't define the system's shape entirely — they're additive structure that supports layered architecture rather than dictating it.
Closest to 'notable trap' (t5). The misconception explicitly states developers confuse DTOs with Value Objects, believing they're the same thing when they serve different purposes. Common mistakes include adding business logic to DTOs and reusing the same DTO for input/output — these are documented gotchas that intermediate developers learn through experience, not catastrophic but consistently encountered.
Also Known As
TL;DR
Explanation
DTOs carry data without behaviour — they are glorified structs. They decouple layers: a controller accepts a CreateUserDTO from the HTTP layer, passes it to the domain without exposing HTTP-specific types, and the domain returns a UserDTO to the controller without exposing internal entities. In PHP, DTOs are implemented as readonly classes (PHP 8.2), value objects, or simple classes with typed properties and constructor promotion.
Common Misconception
Why It Matters
Common Mistakes
- Adding business logic to a DTO — it should be a data container; logic belongs in the domain.
- Using the same DTO for input and output — input DTOs carry unvalidated data; output DTOs carry validated results.
- Not using readonly properties for DTOs — a mutable DTO can be changed after construction, losing the data-snapshot guarantee.
- Using arrays instead of DTOs — arrays lose type information and require constant isset() checks.
Avoid When
- The DTO simply mirrors an entity one-to-one with no transformation — it adds a class for no benefit.
- Using DTOs as mutable bags that accumulate logic over time — keep them dumb data carriers.
- Passing DTOs across process boundaries without versioning — unversioned DTOs break when fields change.
When To Use
- Transferring data across layer boundaries (controller → service → repository) without leaking domain objects.
- API request and response shapes that differ from internal domain models.
- Type-safe alternatives to associative arrays when passing structured data between methods.
- Decoupling serialisation concerns from domain logic — DTOs can be serialised freely without affecting entities.
Code Examples
// No DTO — domain service receives HTTP request:
class UserService {
public function create(Request $request): User { // Coupled to HTTP!
return User::create([
'name' => $request->input('name'),
'email' => $request->input('email'),
]);
}
}
// DTO decouples HTTP from domain:
readonly class CreateUserDTO {
public function __construct(
public string $name,
public string $email,
) {}
}
class UserService {
public function create(CreateUserDTO $dto): User {
return User::create(['name' => $dto->name, 'email' => $dto->email]);
}
}