Я создал небольшой демонстрационный проект с 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)
{}
}
Может кто-нибудь сказать мне, пожалуйста, что я упустил, чтобы запустить это без публичного конструктора?






Это не невозможно, потому что Symfony в MapRequestPayload использует сериализатор/нормализатор Symfony и пытается десериализовать данные полезной нагрузки в объект. Подробнее см. \Symfony\Component\Serializer\Normalizer\ObjectNormalizer.
Но у вас объект 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.
Все в порядке. Это может быть путь, по которому стоит идти. Я обсужу этот подход с командой. Спасибо!
Обычно вы устанавливаете ограничение непосредственно на свойство:
final class Email{ #[IsEmail] public string $email; private function __construct(string $email){ /*...*/ }Вы пробовали это?