A DTO is a simple object designed to carry data between layers or systems. Think of it as a glorified envelope for your data: it’s lightweight, neatly organised, and has one job—to move data without any behaviour or extra baggage.

If your database is the kitchen and your API is the dining room, a DTO is the server delivering your meal without dropping the plate.

The "Why" of DTOs

Why should you care? Here are a few reasons:

  1. Separation of Concerns: DTOs keep your business logic, database models, and API responses clean and distinct.

  2. Readability: A DTO lets you see at a glance what data is being passed around.

  3. Validation: You can validate data at the DTO level, preventing sneaky invalid values from creeping into your code.

  4. Flexibility: DTOs make it easier to tweak data representations without breaking your core application logic.

What’s the alternative? Maybe it’s a jumble of associative arrays? Nobody has time for $data['user']['preferences']['notifications']['email'] every time you want to know if someone wants to hear from you.

The Grumpy Dev is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.

The "How" of DTOs

Let’s get practical with some PHP examples.

A Simple UserDTO

class UserDTO
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly int $age
    ) {}
}

// Usage
$userDto = new UserDTO(
    'Taylor Swift', 
    '[email protected]', 
    33
);

// Accessing properties
echo $userDto->name; // Taylor Swift

Here, UserDTO encapsulates user data in a structured way. It’s easy to see what’s going on—no digging through nested arrays or guessing what keys exist.

Transforming API Responses to DTOs

DTOs often act as intermediaries between your database models and other layers, like APIs or views. Let’s say you’ve got an API response, you can add a static function to create the DTO, like so:

class UserDTO
{
    public function __construct(
        public string $name,
        public string $email,
        public int $age
    ) {}

    /**
     * Creates a UserDTO from an API response.
     */
    public static function fromResponse(array $response): self
    {
        return new self(
            name: $response['full_name'],
            email: $response['email_address'],
            age: (int) $response['age']
        );
    }
}

// Simulated API response
$apiResponse = [
    'id' => 42,
    'full_name' => 'Jane Doe',
    'email_address' => '[email protected]',
    'age' => '28',
    'other_field' => 'not needed'
];

// Transform API response to DTO
$userDto = UserDTO::fromResponse($apiResponse);

// Access DTO data
echo $userDto->name;  // Outputs: Jane Doe
echo $userDto->email; // Outputs: [email protected]
echo $userDto->age;   // Outputs: 28

Explanation

  1. Constructor: Defines the expected structure of the UserDTO with fields like name, email, and age.

  2. fromResponse Method: Maps API response keys to DTO properties. This is the translator between the raw API response and your application's internal structure.

  3. API Response Simulation: Represents typical API output, possibly containing extra fields you don't need in your DTO.

  4. Field Translation: Converts API-specific field names (e.g., full_name to name) and handles data type mismatches (age as a string to age as an integer).

This approach ensures your application doesn’t have to deal with the quirks or inconsistencies of raw API responses, keeping your code cleaner and more maintainable.

The "Gotchas"

DTOs are great, but they’re not magic. Here are a few things to keep in mind:

  1. Overhead: Don’t go DTO-crazy and start wrapping every variable in sight. Use them where they make sense.

  2. Validation: DTOs aren’t a substitute for robust validation—they’re just one tool in the toolbox.

  3. Too Much Abstraction: If your DTOs are more complex than the data they’re wrapping, you might need to rethink your approach.

Conclusion

DTOs are like the gift wrap for your data—clean, presentable, and easy to handle. Whether you’re transforming models, structuring API responses, or just trying to avoid the horrors of deeply nested arrays, DTOs have your back.

So the next time you’re passing data around, consider using a DTO. Your future self (and your teammates) will thank you.

Want some examples? Check out my code repository!