Как передать подготовленную и валидированную DTO в сервис, если в контроллере есть шаблонный код сериализации и валидации?

«Как передать подготовленную и валидированную DTO в сервис, если в контроллере есть шаблонный код сериализации и валидации?» — вопрос из категории Архитектура, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Цель — вынести логику валидации и преобразования запроса в DTO из сервиса, оставив в контроллере только координацию. Вот как я это делаю в Symfony-проектах:

1. Использование форм для валидации DTO: Создаю форму, привязанную к DTO. Форма автоматически валидирует данные и заполняет объект DTO.

// src/Controller/UserController.php
use AppDtoUserUpdateDto;
use AppFormUserUpdateType;
use AppServiceUserService;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;

class UserController extends AbstractController
{
    public function update(
        Request $request,
        UserService $userService
    ): Response {
        // 1. Создаём экземпляр DTO
        $userUpdateDto = new UserUpdateDto();

        // 2. Создаём и обрабатываем форму для этого DTO
        $form = $this->createForm(UserUpdateType::class, $userUpdateDto);
        $form->submit($request->toArray()); // Или handleRequest()

        // 3. Валидация. Форма использует constraints из DTO.
        if (!$form->isValid()) {
            // Возвращаем ошибки валидации
            return $this->json(['errors' => $form->getErrors(true)], Response::HTTP_BAD_REQUEST);
        }

        // 4. Передаём чистый, валидированный DTO в сервисный слой
        $updatedUser = $userService->updateUser($userUpdateDto);

        // 5. Сериализуем результат (например, через встроенный JsonResponse)
        return $this->json($updatedUser, Response::HTTP_OK);
    }
}

2. Использование сериализатора и валидатора (для API без форм):

// Альтернативный подход с @ParamConverter или ручной десериализацией
public function update(
    Request $request,
    SerializerInterface $serializer,
    ValidatorInterface $validator,
    UserService $userService
): Response {
    // Десериализуем JSON-тело запроса сразу в DTO
    $userUpdateDto = $serializer->deserialize(
        $request->getContent(),
        UserUpdateDto::class,
        'json'
    );

    // Валидируем DTO
    $errors = $validator->validate($userUpdateDto);
    if (count($errors) > 0) {
        // Обработка ошибок...
    }

    // Передаём в сервис
    $userService->updateUser($userUpdateDto);

    return $this->json(['status' => 'success']);
}

Ключевой принцип: Контроллер отвечает за HTTP-взаимодействие (десериализацию, валидацию входных данных, формирование ответа). Сервис получает уже готовый к использованию, гарантированно валидный объект DTO и работает только с бизнес-логикой.