Introduction

Such a pleasure to work on a project that makes use of Data Transfer Objects (DTOs), especially when it's your project. I can't stress enough how important they are in PHP development, and how they make your app easier to understand and reason about. If you're working on a project that uses shared state, I highly recommend you start taking advantage of them as soon as possible.

If you have never used DTOs before or have little experience with them, you have come to the right place! This article will guide you through the basics of DTOs, and give you a better understanding of why they are useful in PHP applications and how they can help us to write more reliable code.

The main goal of this short article is to show you some of the places in PHP apps where you can apply the concept of DTO. By the end of the article, you'll understand why using a DTO approach can make your apps easier to maintain in the future.

I don't include declare(strict_types=1); in code examples because I'm assuming everybody is using strict types in modern PHP development. If you don't, there is no point even reading this article.

What is a DTO?

Data Transfer Object (DTO) is an object that represents the data that needs to be transferred between different parts of the program. Think of it like a type-safe version of an object or an object that encapsulates the information you want to transfer between different components or modules of your app.

In other words, DTO is a representation of data that is being passed around your program. It's similar to a POPO class that contains values that need to be passed from one module to another.

The POPO stands for Plain Old PHP Object and is a basic concept of Object-oriented programming in PHP. The POPO class:

  • Doesn't extend from another class;
  • Doesn't implement any interfaces;
  • Has no dependencies or capabilities beyond what's defined in the class.

DTO is very similar, it is like a subspecies of POPO. It should not contain any business logic and shouldn't be used for any calculation or other operations that require using complex logic. DTO should be used only to transfer data between different parts of your app.

Let's say the JSON request came in from the client (browser) that we need to validate and save the result in the database. The client and the server do not have an agreement on what variables the client should use and how the request should look like. Especially in PHP, where a client request is usually a JSON string or global array, which doesn't have any types defined.

Server returns DTO

When you receive a JSON object in typed languages like Golang, you normally need to define a struct in your code to define the structure. For example, if you receive a user JSON like this:

{
    "name": "Anna",
    "age": 26
}

In Golang, you have to define a struct:

type User struct {
	Name string `json:"name"`
	Age  uint    `json:"age"`
}

With this struct in place, you can validate JSON and use it whenever you need throughout your codebase.

I'm sure all of us experienced this problem more than once, where our server logic breaks because we use a string instead of the int. Or we can't grab the data from the request because it uses the array instead of the object, and we don't know how to convert it back to the data we need. That's where DTO can help us out!

Overusing associative array

Unfortunately, many devs are using associative arrays instead of typed data classes with predefined fields. I'm guilty of that myself. And the main reason for that is that it's faster to create an array than a class with empty fields in it. But eventually, this approach is inefficient and hard to debug.

Take a look at this PHP array:

$users = [
    ['name' => 'Serhii', 'age' => 32],
    ['name' => 'Anna', 'age' => 26],
];

You can clearly see that it's an array of users, but there is a problem with this code because we can do something like this:

$users = [
    ['name' => 'Serhii', 'age' => 32],
    ['name' => 'Anna', 'age' => 26],
    ['name' => 'Ksenia', 'age' => '27'], // <-- age is string
];

In the best-case scenario, you'll get a type error somewhere in your code. In the worst case, you'll get a bug that is hard to find and fix later.

Now, consider code like this:

$users = [
    new UserDto(name: 'Serhii', age: 32),
    new UserDto(name: 'Anna', age: 26),
];

Now you have a typed class definition. No matter what kind of user you get from the server, you'll have the right type to work with. That will make your code more robust, and easier to read and debug. That's the beauty of Data Transfer Objects.

Extract the request into a DTO

To better digest what I mean here, let's take a look at the JSON request that comes to our backend. Depending on what framework you are using, this request can look different, but let's use a plain PHP example to make it simple for everybody.

$json = file_get_contents('php://input');

$data = json_decode($json, associative: true);

var_dump($data);

If we take a look at the request in dev tools, we can see the var dump of a decoded JSON object:

JSON request using var_dump

The issue with this object is that it isn't well-defined, meaning there is no schema for the type of data we receive. If we had converted it into a proper class and used it throughout our app, it would have made our life a lot easier. We can encapsulate this data as a DTO and use it wherever we need in our code without having to worry about the internal structure or how to convert it back into the original data structure.

$json = file_get_contents('php://input');
$data = json_decode($json, associative: true);

$user = new UserDto($data['name'], $data['age']);

With this logic in place, we have type validation for free because our class will throw a TypeError if some data will be not the right type.

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

When to use it

One of the questions people often ask, is when should I use DTO, and where should I just skip it? The answer to this question is hiding in DTO, Transfer. Whenever you need to transfer data from one part of the application to another, use DTO. If you receive data from the request but only going to use it in one class, I would just use a simple array.

I would suggest trying it for yourself and seeing if it works for you, you can't go wrong with such a simple pattern. This is precisely what we need in languages like PHP. This is what TypeScript fixes in JavaScript, it gives everything its type. And when you receive data from the server, you tell what structure the response is going to have.

type Service = {
    id: number
    categoryId: number
    title: string
    description: string
}

function fetchServices(): void {
    loading = true

    axios.get<Service[]>('/api/services')
        .then(resp => services = resp.data)
        .catch(err => handleError(err))
        .finally(() => loading = false)
}

This is why TypeScript is so loved among the developers. You know what to expect from the function or variable. It's like reading code and reading the documentation at the same time.

Conclusion

DTO is useful when you want to move information around your application without worrying too much about the internal structure of the object you are working with. Instead of using associative arrays, we can use DTOs and it will make our apps easier to maintain and extend in the future.

Here are some links to resources that I've studied before writing this article:

Posts and Articles:

Videos:

Keywords: design, pattern, popo, backend