Data Transfer Objects in PHP

DTOs (Data Transfer Objects) are common building blocks in object oriented programming. They are already present in some frameworks in various programming languages, with very different implementations and use cases. However, in PHP, we almost always use Data Transfer Objects to keep and transport data between application processes and layers.

Martin Fowler first introduced the pattern in his book Patterns of Enterprise Application Architecture, so if you’re curious about the details, don’t be afraid to dive in.:)

DTOs are most frequently and helpfully used in PHP to maintain data structure in requests and move data from the controllers to the service layer of the application code, or, more generally, the layer responsible for all the business-driven logic that occurs in the application. When the code design is more domain driven, DTOs are even more beneficial. But that is an other subject, and I’ll try to explain it in a different blog article.

However, here are some practical examples of how you can use this pattern in your MVC-structured web application written in PHP.

Without Data Transfer Object

Assume we have an endpoint in charge of storing patient data in the database. Typically, we will have a controller class that looks like this:

class StorePatientController
{
    public function __invoke(
        StorePatientRequest $request
    ) {
        // You get only validated data from the request
        $validated = $request->validated();
        
        // Now you have an array like this
        $data = [
            'first_name' => $validated['first_name'],
            'last_name' => $validated['last_name'],
            'address' => $validated['last_name'],
            'health_card_id' => $validated['last_name'],
            'family_physician' => $validated['family_physician'],
            'clinic' => $validated['clinic'],
            'phone' => $validated['phone'],
            'email' => $validated['email'],
            // ..........
        ];

        // Now you process the data to the business layer
        $result = new StorePatientService($data);

        // And you end it with a response
        return response()->json([
            'message' => 'Your patient some succesfully created'
        ], 201);
    }
}

For those unfamiliar with the Laravel framework, I am using some Laravel-specific concepts in the request class and the response json helper, but you will get the point.

Typically, the Request class in Laravel will validate the data posted, and the next step is to process that data to the business logic layer, which will need to do some logic with the data, possibly storing some additional meta data or user tracking data, and storing the patient data to the database.

As we can see from the preceding example, the validation results in a massive array. Arrays can be difficult to maintain and work with at times, not to mention their mutable nature, which can create a real hell in a large code base with a lot of abstractions. We can use the built-in Request class for that (if we use Laravel), but I’m not sure if that’s a good idea because the Request class is responsible for a slew of other methods and functionalities, and we’ll be transferring a strictly application-related dependency to the service layer, which is unquestionably a domain-related layer. Also, consider whether there will be any inconsistencies or difficulties in writing unit tests if you follow that practice. Unit tests are most common and practical in the business logic layer. Simply follow the rule “If it’s difficult to test, you’re doing something wrong.” We require something to which we can always refer; we will use it as a rule for how our data must pass the request life cycle, and we will be certain that it will never change during this process.

With Data Transfer Object

Data Transfer Objects could come in handy here. We’ll write a class to convert this array into an immutable object that can be safely processed further.

class StorePatientDto
{
    public function __construct(
        public readonly string $firstName,
        public readonly string $lastName,
        public readonly string $address,
        public readonly ?string $healthCardId,
        // ..........
    ) {}
}

The controller will now do something like this:

class StorePatientController
{
    public function __invoke(
        StorePatientRequest $request
    ) {
        // You get only validated data from the request
        $validated = $request->validated();
        
        // Now you create an object like this
        $data = new StorePatientDto(
            firstName: $validated['first_name'],
            lastName: $validated['last_name'],
            address: $validated['address'],
            healthCardId: $validated['health_card_id'],
            // ..........
        );

        // Now you process the data to the business layer
        $result = new StorePatientService($data);

        // And you end it with a response
        return response()->json([
            'message' => 'Your patient some succesfully created'
        ], 201);
    }
}

This is now more likely in terms of how data is transferred in the future. However, if you want to add more cleanness to the controller, you can make your DTO class transform the array into an object, but keep in mind that you limit your DTO class to only accept arrays. This, however, should look like this:

class StorePatientDto
{
	public readonly string $firstName;
	
	public readonly string $lastName;

    public readonly string $address;

    public readonly string $healthCardId;

    // ..........
	
    public function __construct(public readonly array $patientData)
    {
        $this->firstName = Arr::get($patientData, 'first_name');
        $this->lastName =  Arr::get($patientData, 'last_name');
        $this->address =  Arr::get($patientData, 'address');
        $this->healthCardId =  Arr::get($patientData, 'healthCardId');
        // ..........
    }
}

Now we’ll put it like this:

class StorePatientController
{
    public function __invoke(
        StorePatientRequest $request
    ) {
        // You get only validated data from the request
        $validated = $request->validated();
        
        // Now you create an object like this
        $data = new StorePatientDto($validated);

        // Now you process the data to the business layer
        $result = new StorePatientService($data);

        // And you end it with a response
        return response()->json([
            'message' => 'Your patient some succesfully created'
        ], 201);
    }
}

It looks good. 🙂 You will now have immutable data objects in your service layer, and you can be confident that their structure and data will not change during the process. It’s even cleaner now that PHP 8.1 has readonly class properties, which eliminates the need for getters and setters and makes the class declaration more readable.

Final note

Please keep in mind that while some of the code examples use Laravel-specific features such as the Arr class helper or the Request class, the concept of Data Transfer Objects remains agnostic. Also, for the sake of separation and cleanliness, there will be no other necessary boilerplate code in the examples, such as namespaces, class usages, or anything similar. I tried to limit my attention to the concept and its potential applications.

This is just one example of the concept of Data Transfer Objects and one possible application in PHP; if you want to learn more about it, you can do so by searching for it on the internet; I’m sure you’ll find many more interesting use cases and variations to the concept. Also consider Martin Fowler’s post on the concrete applications of the Local DTO concept, which is probably even more similar to the concept I explained in my post.