Building Maintainable PHP Applications: Data Transfer Objects
This article is part of the Building Maintainable PHP Applications series
Most PHP developers are used to working with arrays when they are at the start or years into their programming career because they seem to be very simple to use.
Arrays however fall short when things get complex. It may seem like they are fine, but they do lack types and visibility.
There’s no way to know what the data inside of the array looks like or what each of the keys contain, what type the values are etc.
Let’s take a quick example of a very common controller that you may see in a Laravel application:
1<?php 2 3class ShippingController 4{ 5 public function __construct(ShippingService $shippingService) 6 { 7 $this->shippingService = $shippingService; 8 } 9 10 public function store(StoreShippingRequest $request): RedirectResponse11 {12 $this->shippingService->create($request->validated());13 14 return redirect()->route('shippings.index');15 }16}
As you can see in the example, the service class is taking a list of validated fields.
The first thing as a Laravel developer, you may go to the request class and see all of the fields, learn about what fields and what their types are.
Here’s an example of the form request:
1<?php 2 3class StoreShippingRequest extends FormRequest 4{ 5 public function authorize(): bool 6 { 7 return true; 8 } 9 10 public function rules(): array11 {12 return [13 'type' => ['required', 'string'],14 'price' => ['nullable', 'numeric'],15 'status' => ['required', Rule::in(['active', 'inactive'])],16 ];17 }18}
This somewhat helps with the main visibility issues, because you can see all of the validation rules that are already added there but what if not all validation rules are applied? You would have to try to figure out the request params either visiting the page, looking at the whole code or experiment with the API endpoint which takes time.
This becomes a problem where in order to see all of the values and their types, you would have to debug the array that is being returned by the `$request→validated()` method call.
This is one of the main reasons why data transfer objects are a great option to use instead of arrays.
Data transfer objects (DTOs) are objects (or classes) which carry data and they do not have any methods that represent behavior, validate data, returns any response or execute specific actions.
Here’s an example of a data transfer object (DTO) based on the example above:
1<?php 2 3final readonly class ShippingDataTransferObject 4{ 5 public function __construct( 6 public string $type, 7 public ShippingStatus $status, 8 public ?int $price 9 ) {}10 11 public static function fromArray(array $data): self12 {13 return self(14 $data['type'],15 ShippingStatus::tryFrom($data['status']),16 $data['price'] ?? null,17 );18 }19}
As you can see, the class contains the properties and their types, and a method or multiple methods which the class can be instantiated from.
Here are a few examples of methods that you may see:
from an array
from JSON
from XML
from an
Illuminate/Http/Request
classfrom an Eloquent model
and many more
which their main purpose is to map the data into the properties by instantiating the object.
Here’s an example of the DTO usage instead of the array:
1<?php 2 3class ShippingController 4{ 5 public function __construct(ShippingService $shippingService) 6 { 7 $this->shippingService = $shippingService; 8 } 9 10 public function store(StoreShippingRequest $request): RedirectResponse11 {12 $this->shippingService->create(ShippingDataTransferObject::fromArray($request->validated());13 14 return redirect()->route('shippings.index');15 }16}
As you can see, we just call the static method fromArray()
and pass the array so that the service method takes it and passes it along.
The main benefits that DTOs bring are:
it brings structure to the data
It shows what data is being transferred and its type
It improves the intellisense support without any additional IDE or editor plugins
The most common usage of data transfer objects may be when you transfer data from the controller to a service class but it can also be used as:
an event being dispatched and used in the event listener
a command being passed to a command handler
data coming from a different delivery mechanism such as a console command or a message queue (and a web request which I already mentioned)
All of the above are options of what I would call input DTO, but you can also use DTOs for data that is coming from a 3rd party API in a form of a response which I would classify as an output DTO.
It may seem like DTOs are unnecessary at first because it will require you to create additional files but they do help in more complex projects that need to be maintained for years, especially for new people on the team to see what the code does and what data is being used.
The main reason why people may find DTOs complex is that either they are just not used to creating additional classes, thinking that it will complicate the project but on the other hand, most of the examples (including this one in this blog post) is very simple.
DTOs shine in situations when you have 5 or more values to work with in a single request / API endpoint as an example. This doesn’t mean that DTOs are too much or unnecessary complexity when you have 2 values to work with.
Having a consistent codebase is very important, makes your and your team’s life easier to work with so it’s better to use them everywhere if you choose to use them.
Oh and one more thing, you don’t need to use a package for data transfer objects. I know that some people do prefer to use a package for data transfer objects that does validation, sets headers, returns responses, reduces the code that needs to be written and so on, but that’s not really a data transfer object at all by the original definition.
Arrays may seem like a good idea when you use them but they have many flaws and are not as useful besides them being the easiest approach to use when working with data.