Symfony 7 MapRequestPayload Valdation с частным конструктором

Я создал небольшой демонстрационный проект с Symfony 7, чтобы оценить возможности MapRequestPayload, и столкнулся с проблемой: кажется, я что-то упускаю при использовании именованных конструкторов.

Мой контроллер:

<?php

declare(strict_types=1);

namespace App\Controller;

use App\DependencyInjection\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;

final readonly class HelloWordController
{
    #[Route('/')]
    public function helloWord(#[MapRequestPayload('json')] Request $request): Response
    {
        return new JsonResponse(['email' => $request->email->asString()]);
    }
}

Мой запрос DTO:

<?php

declare(strict_types=1);

namespace App\DependencyInjection;

use App\DependencyInjection\Email;
use Symfony\Component\Validator\Constraints as Assert;

final class Request
{

    #[Assert\Valid] public Email $email;
    public function __construct(
        string $email,
    ){
        $this->email = EmailV6::fromString($email);
    }
}

Мой объект значения:

<?php

declare(strict_types=1);

namespace App\DependencyInjection;
use App\DependencyInjection\IsEmail;

final class Email
{
    private string $email;
    private function __construct(string $email)
    {
        $this->email = $email;
    }

    public function asString(): string
    {
        return $this->email;
    }

    public static function fromString(#[IsEmail] string $email): self
    {
        return new self($email);
    }
}

Ограничение:

<?php

declare(strict_types=1);

namespace App\DependencyInjection;

use Attribute;
use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;

#[Attribute]
final class IsEmail extends Constraint
{
    #[HasNamedArguments]
    public function __construct(
        array       $groups = null,
        mixed       $payload = null,
        public bool $isNullable = false,
    )
    {
        parent::__construct([], $groups, $payload);
    }
}

И Валидатор:

<?php

declare(strict_types=1);

namespace App\DependencyInjection;

use InvalidArgumentException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

final class IsEmailValidator extends ConstraintValidator
{
    public function validate(mixed $value, Constraint $constraint): void
    {
        if (!$constraint instanceof IsEmail) {
            throw new UnexpectedTypeException($constraint, IsEmail::class);
        }

        if ($value === null && $constraint->isNullable) {
            return;
        }
        
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(
                sprintf(
                    '"%s" is not a valid email address',
                    $value
                )
            );
        }
    }
}

Моя проблема в том, что если я попробую сделать это таким образом, ограничение для электронной почты никогда не будет проверяться.

Если я сделаю конструктор общедоступным и помещу туда все необходимое, это будет работать, но не соответствует нашим требованиям:

<?php

declare(strict_types=1);

namespace App\DependencyInjection;
use App\DependencyInjection\IsEmail;

final readonly class Email
{
    public function __construct(#[IsEmail] public string $email)
    {}
}

Может кто-нибудь сказать мне, пожалуйста, что я упустил, чтобы запустить это без публичного конструктора?

Обычно вы устанавливаете ограничение непосредственно на свойство: final class Email{ #[IsEmail] public string $email; private function __construct(string $email){ /*...*/ } Вы пробовали это?

Arleigh Hix 15.03.2024 19:28
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
0
1
402
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это не невозможно, потому что Symfony в MapRequestPayload использует сериализатор/нормализатор Symfony и пытается десериализовать данные полезной нагрузки в объект. Подробнее см. \Symfony\Component\Serializer\Normalizer\ObjectNormalizer.

  1. По умолчанию сериализатор пытается рекурсивно десериализовать данные.
  2. Если у объекта есть конструктор, Symfony попытается создать объект через конструктор. Если у объекта нет конструктора, Symfony попытается получить общедоступные свойства.

Но у вас объект Request, где email представлен как в конструкторе, так и в свойстве. В результате Symfony не знает, что имеет высокий приоритет (необходимо видеть код, что Symfony делает в этом случае).

Лучше в этом случае - создать собственный нормализатор для объекта Email и зарегистрировать этот нормализатор в системе.

namespace Acme;

use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

class EmailNormalizer implements DenormalizerInterface
{
    public function getSupportedTypes(?string $format): array
    {
        return [
            Email::class => true,
        ];
    }

    public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): ?Email
    {
        return $data ? Email::fromString($data) : null;
    }

    public function supportsDenormalization(mixed $data, string $type, ?string $format = null): bool
    {
        return \is_a($data, Email::class, true);
    }
}

После этого Symfony всегда вызывает этот денормализатор для денормализации объекта Email.

Все в порядке. Это может быть путь, по которому стоит идти. Я обсужу этот подход с командой. Спасибо!

Klarkash Ton 13.03.2024 16:40

Другие вопросы по теме