DDD: сравнение Laravel-data и Symfony Validator Object как DTO
Один из самых простых шагов при переходе приложения к кодовой базе Domain Driven Design — создание объектов передачи данных, или DTO для краткости.
Базовый DTO — это класс с свойствами. Но это неудобный способ работы с составными DTO.
// 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,
){}
}
// какой-то контроллер
$person = new PersonDTO(
name: $request->get('name'),
address: new AddressDTO(
street: $request->get('street'),
// ...
),
);
А теперь глянем, чем эти два пакета отличаются. Компонент Symfony Validator не имеет встроенной функциональности трансформации. Пакет Laravel-data предоставляет для этого методы from.
// 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,
){}
}
// какой-то контроллер
$pserson = PersonDTO::from([
'name' => $request->get('name'),
'address' => [
'street' => $request->get('street')
],
]);
Внимательные читатели заметили, что DTO в этом примере потеряли свойство readonly. Это потому, что класс DTO не является readonly. Хотя изменяемые DTO не проблема, возможность иметь неизменяемые DTO делает код более надежным.
Зачем сравнивать?
Почему я сравниваю пакет, ориентированный на объекты, с пакетом, ориентированным на валидацию?
Одно из действий с DTO — валидация. Поскольку валидация — это бизнес-логика, этот код должен происходить в домене. Не хочу изобретать велосипед, поэтому выбираю компонент Symfony validator.
Laravel-data поставляется с методами валидации из коробки.
Согласно определению, DTO не предназначен для валидации. Валидация может происходить до преобразования данных в DTO или после трансформации.
DTO — хорошее место для хранения правил валидации. Оба пакета предоставляют это через атрибуты свойств.
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,
){}
}
Главное отличие: валидация DTO в Laravel-data использует валидатор Laravel, а компонент Symfony validator не зависит от Symfony.
Еще одно преимущество компонента Symfony validator — правила можно группировать. Это позволяет использовать один DTO с разными бизнес-требованиями.
// 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,
){}
}
// в коде домена
$validator->validate($addressDTO, groups: ['ChangePostalCode']);
Заключение
Стало очевидно, что использование класса DTO из Laravel-data не подходит для перехода к DDD-кодовой базе. Сильные стороны пакета в другом.
Создание DTO явно может быть правильным путем, даже если это немного больше работы (если не использовать ИИ).
Недостаток обоих пакетов: не так просто сопоставить сообщения об ошибках с полями ввода, поскольку валидация происходит в DTO.

