Design Patterns And Data Architecture

DTO, Request, And Response Models

DTO means data transfer object. In PHP application architecture, DTOs, request models, and response models are simple objects that carry data across a boundary. They are useful when arrays become ambiguous, when framework request objects are leaking too far into application code, or when API responses need a deliberate contract instead of accidentally exposing ORM models.

The phrase DTO is sometimes used for any object with public properties. That is too broad to be useful. A good DTO has a boundary reason. It carries validated input from a controller to an action. It carries provider payload data from an adapter to a use case. It carries read-model data from a query to a presenter. It makes shape and meaning explicit.

DTOs do not replace domain objects. A RegisterUserInput DTO can carry submitted registration data. A User entity owns user identity and behavior. A UserResponse model can shape public JSON. These objects may contain similar fields, but they have different jobs and different change pressures.

Why Arrays Become A Problem

Arrays are flexible, but that flexibility becomes expensive at application boundaries. A function accepting array $data does not tell callers which keys exist, which values are optional, whether strings are trimmed, whether dates are parsed, or whether IDs are trusted.

A DTO gives the shape a name. Constructor parameters, readonly properties, and PHPDoc can make the expected fields visible. Static analysis tools can help catch mistakes. Tests can build valid objects without remembering magic keys.

Arrays are still fine for local temporary data or low-level decoded JSON before validation. The important move is to stop letting raw arrays travel through several layers as if their meaning were obvious.

A Small Input DTO

This example models validated registration input.

PHP example
<?php

declare(strict_types=1);

final readonly class RegisterUserInput
{
    public function __construct(
        public string $email,
        public string $plainPassword,
        public bool $acceptsMarketing,
    ) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Email is invalid.');
        }

        if (strlen($plainPassword) < 8) {
            throw new InvalidArgumentException('Password is too short.');
        }
    }
}

$input = new RegisterUserInput('ada@example.com', 'correct horse', true);

echo $input->email . PHP_EOL;

// Prints:
// ada@example.com

The DTO makes input visible. The action receiving it does not need to know whether the data came from HTML form fields, JSON, CLI arguments, or a test fixture.

For larger systems, validation may happen in a separate validator or factory rather than in the DTO constructor. The key is that the action receives an object whose meaning is already clear.

Request Models

A request model is an application-level representation of incoming data. It is not necessarily the framework's HTTP request object. A framework request contains headers, cookies, files, route attributes, server data, and raw input. A request model contains the specific application input needed for one operation.

For example, CreateInvoiceRequest might contain customer ID, line items, due date, and notes. It should not contain the whole Symfony or Laravel request. That keeps the use case callable from tests, CLI commands, and queue jobs.

Build request models at the boundary. The controller, form handler, serializer, or input factory can parse raw input, validate it, normalize values, and create the model. The application service then receives explicit data.

Response Models

A response model shapes outgoing data. It can be used for HTML presentation, JSON APIs, CLI output, or provider responses. It prevents accidental exposure of internal fields and makes output contracts easier to review.

Returning ORM entities directly from APIs is risky. Entities may include internal IDs, flags, timestamps, lazy-loaded relationships, or sensitive fields. A response model says exactly what the client receives.

A UserProfileResponse may include display name and avatar URL but omit password hash, internal notes, deleted flag, and security metadata. A separate AdminUserResponse may include different fields. Reusing one object for every audience can leak data or make public contracts unstable.

DTOs And Validation

There are two common styles. In one style, a DTO constructor enforces basic invariants. In another, a validator produces the DTO only after validation passes. Both can work.

Constructor validation is convenient for small value-like DTOs. Separate validation is often better when error messages must map to form fields, when validation uses services, or when invalid input needs to be collected rather than failing at the first problem.

Do not put heavy provider calls, database queries, or authorization checks in DTO constructors. A DTO should remain cheap to build and easy to reason about. Cross-record rules belong in validators, policies, actions, or domain services.

Immutability And Readonly

DTOs are often immutable. PHP readonly classes or readonly properties make accidental mutation harder. This is useful because DTOs represent a boundary snapshot: the submitted input, the response shape, or the command payload at a point in time.

Immutability is not mandatory. Some form objects collect errors or normalized values during processing. If mutation is allowed, make that lifecycle clear. Avoid passing a mutable DTO through many layers where any layer might change it unexpectedly.

For simple DTOs, public readonly properties can be clearer than getters. For objects with derived values or compatibility constraints, methods may still be useful.

DTOs Versus Value Objects

A value object represents a concept with behavior and invariants, such as EmailAddress, Money, or DateRange. A DTO primarily transfers data. It may contain value objects, but it usually does not own rich domain behavior.

For example, RegisterUserInput may contain an EmailAddress value object. The email object validates and normalizes email rules. The DTO groups email with password and marketing preference for a registration use case.

Do not overload DTOs with domain behavior just because they are convenient. If behavior is stable and meaningful, consider a value object or entity.

Mapping Between Models

DTOs often require mapping. Raw request data maps to a request model. Domain objects map to response models. Provider responses map to application DTOs. Mapping is where mistakes can happen: wrong field names, missing null handling, timezone changes, money precision errors, and accidental exposure.

Keep mapping code visible and tested. A mapper or factory method is often clearer than building arrays inside controllers. For public APIs, response model tests should verify field names, omitted sensitive fields, date formats, null behavior, and enum values.

Mapping is not only mechanical. It is a contract decision. If an internal property is renamed, the public response model may intentionally keep the old field name for compatibility.

Versioned Payloads And Compatibility

DTOs that cross process boundaries need compatibility discipline. An internal input object used only inside one request can change with the code that calls it. A queued command payload, webhook payload, public API response, or stored event may be read by older or newer code. Those DTOs should be treated as contracts, not convenient bags of properties.

Prefer additive changes for public or asynchronous payloads. Adding an optional field is usually safer than renaming an existing field. Removing a field, changing a type, changing date format, or changing enum values can break consumers even when the PHP code compiles. Keep version names, migration notes, or compatibility tests near DTOs that leave the process.

Generated serializers and reflection-based mappers do not remove the need for review. They can reduce boilerplate, but they may also expose public properties you did not mean to publish. Always verify the serialized shape at the boundary that matters: JSON response, queue message, webhook body, or provider request.

Handle secrets deliberately. Passwords, tokens, recovery codes, provider payloads, and internal notes should not appear in debug dumps, logs, public responses, queued messages, or exception text just because they were convenient DTO fields.

Review DTOs as part of the application boundary, not as harmless plumbing.

Common Failure Modes

One failure mode is the array-shaped DTO. A DTO with one public array $data property has not solved the shape problem. It only moved the array into an object.

Another failure mode is the universal DTO. One UserDto used for registration input, database transport, admin output, public API output, and event payload will either expose too much or serve nobody well. Split DTOs by boundary.

A third failure mode is putting framework objects inside DTOs. A request model that stores Request $request has not separated the application from HTTP.

A fourth failure mode is treating DTOs as security. A response DTO helps prevent exposure, but tests and review still need to verify sensitive fields are omitted.

Review Criteria

When reviewing a DTO, ask which boundary it serves. Check that its name includes enough context: input, request, response, payload, event, or result. Check that fields are typed and meaningful. Check that optional values are deliberate.

Check whether validation belongs in the constructor, a factory, or a validator. Check that DTOs do not contain service dependencies. Check mappings and tests at public boundaries.

What To Check

Before moving on, make sure you can:

  • explain DTOs as boundary data carriers
  • distinguish DTOs, request models, response models, entities, and value objects
  • stop raw arrays from crossing several application layers
  • keep framework request objects at the edge
  • shape API responses deliberately instead of returning ORM entities
  • choose constructor validation or external validation deliberately
  • test mapping and sensitive-field omission

After this lesson, you should be able to introduce DTOs where they clarify data shape, avoid them where they add ceremony without boundary value, and review response models as part of a public contract.

Practice

Task: Design An Input DTO

A RegisterUser action needs email, plain password, marketing preference, and referral code from either an HTML form or JSON API.

Design an input DTO.

Requirements

Name the class, fields, validation location, and how controllers should create it.

Check your work

The action should not receive the raw framework request.

Show solution

Use RegisterUserInput with fields such as EmailAddress $email, string $plainPassword, bool $acceptsMarketing, and ?string $referralCode. Controllers for HTML and JSON can parse their respective inputs and call a shared factory or validator that returns the DTO.

Validation can live in a form/request validator when the application needs field-level error messages. Basic value invariants can live in value objects such as EmailAddress. The action receives only RegisterUserInput, so it does not depend on HTTP form fields or JSON parsing.

Task: Review A Response Model

An API endpoint returns the ORM User object directly. The object includes password_hash, admin_notes, deleted_at, and lazy-loaded relationships.

Review the design and propose a response model.

Requirements

Name the response model, include safe fields, and explain how tests should verify the public contract.

Check your work

The response should not expose internal ORM fields accidentally.

Show solution

Returning the ORM object directly is unsafe because internal fields and relationships can leak. Create UserProfileResponse for the public endpoint with fields such as id, displayName, avatarUrl, and joinedAt if those are part of the public contract.

Omit password_hash, admin_notes, deleted_at, internal flags, and relationships not intentionally exposed. If admins need more data, create a separate AdminUserResponse with its own authorization and tests.

Tests should assert exact JSON field names, date format, null handling, and absence of sensitive fields. They should not only assert that the endpoint returns 200.

Task: Test Model Boundaries

A CreateInvoiceRequest DTO is built from JSON input and mapped to a CreateInvoice action. The action returns an InvoiceCreatedResponse for the API.

Plan tests for request and response model boundaries.

Requirements

Include malformed JSON, missing fields, wrong types, valid input, domain result mapping, and sensitive-field omission.

Check your work

The tests should cover boundary shape, not only domain success.

Show solution

Request tests should cover malformed JSON, missing customer ID, wrong line-item type, invalid money format, and a valid payload that creates CreateInvoiceRequest. These tests prove parsing and validation before the action runs.

Action tests can use CreateInvoiceRequest directly and focus on business behavior. They should not need malformed JSON fixtures.

Response tests should map a successful domain result into InvoiceCreatedResponse and assert field names, status, date format, amount format, and omitted internal fields such as database primary keys not meant for clients, internal notes, or accounting flags.