DEV Community

david duymelinck
david duymelinck

Posted on

DDD: comparing Laravel-data and Symfony validator object as DTO

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'),
                              // ...
                          ),
         );    
Enter fullscreen mode Exit fullscreen mode

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')
   ],
]);
Enter fullscreen mode Exit fullscreen mode

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,
    ){}
}
Enter fullscreen mode Exit fullscreen mode

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']);
Enter fullscreen mode Exit fullscreen mode

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)

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy