← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

Data Transfer Object (DTO)

Code Quality PHP 8.0+ Intermediate
debt(d5/e3/b5/t5)
d5 Detectability Operational debt — how invisible misuse is to your safety net

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.

e3 Effort Remediation debt — work required to fix once spotted

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.

b5 Burden Structural debt — long-term weight of choosing wrong

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.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

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.

About DEBT scoring →

Also Known As

DTO data object transfer object

TL;DR

A simple object that carries data between layers or systems with no business logic — reducing coupling between layers and making data contracts explicit.

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

DTOs and Value Objects are the same thing — Value Objects have domain meaning and validate their own state; DTOs are layer-crossing containers with no validation logic.

Why It Matters

Without DTOs, controllers pass raw arrays or HTTP request objects into domain services, coupling domain logic to HTTP infrastructure and making it untestable in isolation.

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

✗ Vulnerable
// 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'),
        ]);
    }
}
✓ Fixed
// 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]);
    }
}

Added 15 Mar 2026
Edited 25 Mar 2026
Views 84
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 1 ping M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 0 pings S 0 pings M 1 ping T 4 pings W 1 ping T 0 pings F 0 pings S 0 pings S 0 pings M 1 ping T 0 pings W 3 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Amazonbot 17 Perplexity 12 Google 9 Scrapy 7 Ahrefs 5 Unknown AI 3 ChatGPT 2 Claude 2 Bing 2 SEMrush 2 Majestic 1 Qwen 1
crawler 60 crawler_json 2 pre-tracking 1
DEV INTEL Tools & Severity
🟢 Low ⚙ Fix effort: Low
⚡ Quick Fix
Use readonly constructor promotion for DTOs — public function __construct(public readonly string $email, public readonly int $age) {} — zero boilerplate, immutable by default
📦 Applies To
PHP 8.0+ web cli queue-worker
🔗 Prerequisites
🔍 Detection Hints
Mutable DTO with public setters; passing associative arrays instead of typed DTOs across layer boundaries
Auto-detectable: ✗ No phpstan psalm
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: Medium Context: Class


✓ schema.org compliant