One of the easiest steps when moving an application to a Domain Driven Design codebase is to create data transfer objects or DTOs for short.
A barebones DTO is class with properties. But it is not a nice way to handle a DTO that is composed.
// AddressDTO.php
readonly class AddressDTO {
public function __construct(
public ?string $street = null,
public ?string $streetNumber = null,
public ?string $city = null,
public ?string $postalCode = null,
public ?string $country = null,
){}
}
// PersonDTO.php
readonly class PersonDTO {
public function __construct(
public ?string $name = null,
public ?AddressDTO $address = null,
){}
}
// some controller
$person = new PersonDTO(
name: $request->get('name'),
address: new AddressDTO(
street: $request->get('street'),
// ...
),
);
And here is where the first way the two packages differ. The Symfony Validator component has no transformer functionality build-in. The Laravel-data package provides from methods for this purpose.
// AddressDTO.php
use Spatie\LaravelData\DTO;
class AddressDTO extends DTO {
public function __construct(
public ?string $street = null,
public ?string $streetNumber = null,
public ?string $city = null,
public ?string $postalCode = null,
public ?string $country = null,
){}
}
// PesonDTO.php
use Spatie\LaravelData\DTO;
class PersonDTO extends DTO {
public function __construct(
public ?string $name = null,
public ?AddressDTO $address = null,
){}
}
// some controller
$pserson = PersonDTO::from([
'name' => $request->get('name'),
'address' => [
'street' => $request->get('street')
],
]);
Eagle-eyed people might have spotted that the DTOs in this example have lost their readonly
property. This is because the DTO
class is not readonly
.
While mutable DTOs are not a problem, the option to have immutable DTOs makes the code more robust.
Why do I compare an object focused package with a validation focused package?
One of the actions that will happen on a DTO is validation. And because validation is business logic that code should happen in a domain.
I don't want to reinvent the wheel, and that is why I choose the Symfony validator component.
Laravel-data comes with validate methods out of the box.
According to the definition a DTO is not meant to have validation. The validation can happen before the data is converted to the DTO, or it can happen after the DTO is transformed.
I think a DTO is a good place to store the validation rules. And both packages provide this with property attributes.
class AddressDTO extends DTO {
public function __construct(
#[Required]
public ?string $street = null,
#[Required]
public ?string $streetNumber = null,
#[Required]
public ?string $city = null,
#[Required]
public ?string $postalCode = null,
#[Required]
public ?string $country = null,
){}
}
The biggest difference is that the Laravel-data DTO
validation is powered by the Laravel validator and the Symfony validator component has no Symfony dependency.
Another benefit of the Symfony validator component is that the rules can be grouped. This allows us to use the same DTO but with different business requirements.
// AddressDTO.php
use Symfony\Component\Validator\Constraints as Assert;
readonly class AddressDTO {
public function __construct(
#[Assert\NotBlank(groups: ['CreateAddress'])]
public ?string $street = null,
#[Assert\NotBlank(groups: ['CreateAddress'])]
public ?string $streetNumber = null,
#[Assert\NotBlank(groups: ['CreateAddress'])]
public ?string $city = null,
#[Assert\NotBlank(groups: ['CreateAddress', 'ChangePostalCode'])]
public ?string $postalCode = null,
#[Assert\NotBlank(groups: ['CreateAddress'])]
public ?string $country = null,
){}
}
// in domain code
$validator->validate($addressDTO, groups: ['ChangePostalCode']);
Conclusion
I think it has become obvious that using the Laravel-data DTO
class is not a good candidate when you want to move to a DDD codebase. The strengths of the package lie somewhere else.
Creating a DTO explicitly might be the way to go, even if when is a bit more work (If you don't use AI).
A flaw that both packages have is that it is not that easy to match the error messages with the input fields, because a DTO is used to do the validation.
Top comments (0)