← Back to articles
PHP Architecture & Design Patterns

What is a DTO in PHP and Why Do You Need It?

Learn what a Data Transfer Object (DTO) is in PHP, why it matters for type safety and clean architecture, and how to implement it with modern PHP 8 features.

What is a DTO in PHP and Why Do You Need It?
Featured article ↗

What is a DTO in PHP and Why Do You Need It?

When building PHP applications, developers constantly face the challenge of passing data between different layers of an application: controllers, services, repositories, and views. A common anti-pattern is passing raw arrays or stdClass objects, which leads to unclear contracts, hidden bugs, and difficult refactoring. This is where Data Transfer Objects (DTOs) come to the rescue.

What is a DTO?

A DTO (Data Transfer Object) is a simple object whose sole purpose is to carry data between processes or layers of an application. It contains no business logic, no validation rules, and no database queries—just properties and methods to access them.

The term was popularized by Martin Fowler in his book Patterns of Enterprise Application Architecture. In the PHP world, DTOs became widely adopted with the rise of Domain-Driven Design (DDD) and clean architecture.

Why Do You Need DTOs?

Using DTOs instead of arrays or generic objects provides several important benefits:

  • Type safety — DTOs have strictly typed properties, which catches errors early and improves IDE autocompletion.
  • Clear contracts — a DTO explicitly defines what data it carries, unlike a plain array with unknown keys.
  • Immutability — DTOs can be made read-only, preventing accidental modification of data as it travels through the system.
  • Encapsulation — data structure is separated from business logic, keeping your code clean and focused.
  • Easier refactoring — changing a DTO's structure is a single-point change, not a hunt through dozens of array accesses.
  • Self-documentation — the class itself describes the shape of the data.

A Simple DTO Example in PHP

Let's create a basic DTO for a user:

<?php declare(strict_types=1); final class UserDTO { public function __construct( public readonly int $id, public readonly string $name, public readonly string $email, public readonly int $age, ) { } }

Thanks to PHP 8.1+ readonly properties and constructor promotion, this DTO is concise, immutable, and type-safe.

Using a DTO in Practice

Here is how you might use a DTO when returning data from a repository:

<?php final class UserRepository { public function findById(int $id): ?UserDTO { $row = $this->db->fetchOne('SELECT * FROM users WHERE id = ?', [$id]); if ($row === null) { return null; } return new UserDTO( id: (int) $row['id'], name: $row['name'], email: $row['email'], age: (int) $row['age'], ); } }

Now the calling code knows exactly what data it receives:

<?php $user = $repository->findById(42); if ($user !== null) { echo $user->name; // IDE knows this is a string echo $user->email; // IDE knows this is a string }

DTO vs. Value Object vs. Entity

It's important not to confuse DTOs with other patterns:

  • DTO — carries data, no behavior, no identity.
  • Value Object — immutable, defined by its values, may contain behavior (e.g., Money, Email).
  • Entity — has a unique identity and lifecycle, contains business logic.

Mapping Arrays to DTOs

In real applications, you often need to build a DTO from an array, such as a request payload. A common approach is a static factory method:

<?php final class CreateUserDTO { public function __construct( public readonly string $name, public readonly string $email, public readonly int $age, ) { } public static function fromArray(array $data): self { return new self( name: $data['name'], email: $data['email'], age: (int) $data['age'], ); } }

DTOs and Validation

DTOs should not perform validation themselves. Instead, keep validation in a dedicated layer, such as a form request or a validator service. The DTO is created only after the data has been validated.

<?php $data = $request->getParsedBody(); $validator->validate($data, [ 'name' => 'required|string', 'email' => 'required|email', 'age' => 'required|integer|min:18', ]); $dto = CreateUserDTO::fromArray($data);

When DTOs Might Be Overkill

DTOs are not always necessary. In small scripts or prototypes, using arrays may be perfectly fine. Introducing DTOs makes sense when:

  • Your application has multiple layers with clear boundaries.
  • Data is passed across module or service boundaries.
  • You need type safety and long-term maintainability.
  • Multiple consumers rely on the same data structure.

Best Practices for DTOs in PHP

  • Make them immutable — use readonly properties or private properties with getters.
  • Keep them small — a DTO should represent one clear concept.
  • No business logic — move any logic to services or domain objects.
  • Use strict types — declare strict_types=1 in every DTO file.
  • Provide factory methods like fromArray() or fromRequest() for convenient construction.
  • Avoid setters — DTOs should be created once and never modified.

Conclusion

DTOs are a simple but powerful pattern that brings clarity, type safety, and maintainability to PHP applications. They define explicit contracts between layers, reduce bugs caused by loosely typed arrays, and make refactoring significantly easier. While they add a bit of boilerplate, the long-term benefits in medium and large projects far outweigh the cost. Start small: introduce DTOs where data crosses layer boundaries, and you'll quickly feel the difference in code quality.

Try refactoring one of your array-based data flows into a DTO today—your future self will thank you.

Technologies & topics

Article tags

No articles match these filters.

Have a project or an idea to discuss?

Let's talk ↗