Хз как иначе это решить, динамически зарегестрировать сервис не выйдет как и передать параметром.
Старый подход Laravel API validation with Symfony
Загрузка Metadata class
Финальный код в комментариях.
<?php
declare(strict_types=1);
/*
* Created by BonBonSlick
*/
namespace App\Domain\Core\Request;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class AuthRequest extends AbstractValidationRequest {
private ?string $email;
private ?string $password;
public function __construct(Request $request, TranslatorInterface $translator) {
parent::__construct($request, $translator);
$this->email = (string)$request->get('email');
$this->password = (string)$request->get('password');
}
public static function loadValidatorMetadata(ClassMetadataInterface $classMetadata): void {
$passMin = 4;
$passMax = 32;
$emailMin = 5;
$emailMax = 32;
$classMetadata->addPropertyConstraints(
'email',
[
new NotBlank(
[
'message' => parent::simpleTrans('not.blank', 'email'),
]
),
new Length(
[
'min' => $emailMin,
'max' => $emailMax,
'minMessage' => parent::minMaxTrans('email', $emailMin, true),
'maxMessage' => parent::minMaxTrans('email', $emailMax),
]
),
]
)->addPropertyConstraints(
'password',
[
new NotBlank(
[
'message' => parent::simpleTrans('not.blank', 'password'),
]
),
new Length(
[
'min' => $passMin,
'max' => $passMax,
'minMessage' => parent::minMaxTrans('password', $passMin, true),
'maxMessage' => parent::minMaxTrans('password', $passMax),
]
),
]
)
;
}
public function email(): string {
return $this->email;
}
public function password(): string {
return $this->password;
}
}
<?php
declare(strict_types=1);
/*
* Created by BonBonSlick
*/
namespace App\Domain\Core\Request;
use App\Infrastructure\ArgumentResolver\ValidationRequestInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Translation\Exception\InvalidArgumentException as TranslationInvalidArgumentException;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
abstract class AbstractValidationRequest implements ValidationRequestInterface {
protected static TranslatorInterface $translator;
protected static string $messagesFileName = 'validation';
protected static string $inputNamesFileName = 'input_names';
protected static string $attributesFileName = 'validation_attributes';
protected static string $locale;
protected Request $request;
public function __construct(Request $request, TranslatorInterface $translator) {
$this->request = $request;
self::$translator = $translator;
// @todo - should be taken from DB or other settings?
self::$locale = $request->getLocale() ?? $request->getDefaultLocale();
}
abstract protected static function loadValidatorMetadata(ClassMetadataInterface $metadata): void;
/**
* translates message with attribute
*
* @param string $messageId - messages eg "This field is required'
* @param string $inputNameId - input field eg "password"
* @param string|null $attributeId - fields in messages eg "size", "resolution", "bytes"
* @param string|null $messagesFileName - file for messages
* @param string|null $inputNamesFileName - file for input names
* @param string|null $attributesFileName - file for attributes
* @param string $localeCode
*
* @throws TranslationInvalidArgumentException
*
* @return string
*
*/
protected static function simpleTrans(
string $messageId,
string $inputNameId,
?string $attributeId = null,
?string $messagesFileName = null,
?string $inputNamesFileName = null,
?string $attributesFileName = null,
string $localeCode = 'en' // TODO - should be returned from system settigns
): string {
return self::$translator->trans(
$messageId,
[
'{{ inputName }}' => self::$translator->trans(
$inputNameId,
[],
$inputNamesFileName ?? self::$inputNamesFileName,
$localeCode
),
'%attribute%' => self::$translator->trans(
$attributeId,
[],
$attributesFileName ?? self::$attributesFileName,
$localeCode
),
],
$messagesFileName ?? self::$messagesFileName,
$localeCode
);
}
/**
* @throws TranslationInvalidArgumentException
*/
protected static function minMaxTrans(
string $inputNameId,
int $minMaxValue,
?bool $isMin = false,
?string $messagesFileName = null,
?string $inputNamesFileName = null,
?string $attributesFileName = null,
string $localeCode = 'en' // TODO - should be returned from system settigns
): string {
$id = $isMin ? 'min' : 'max';
// https://symfony.com/doc/current/translation/message_format.html#pluralization
return self::$translator->trans(
$id,
[
'{{ inputName }}' => self::$translator->trans(
$inputNameId,
[],
$inputNamesFileName ?? self::$inputNamesFileName,
$localeCode
),
sprintf('%%%s%%', $id) => $minMaxValue,
'%value%' => self::$translator->trans(
'character',
['%count%' => 2],
$attributesFileName ?? self::$attributesFileName,
$localeCode
),
],
$messagesFileName ?? self::$messagesFileName,
$localeCode
);
}
}
final class RequestDTOResolver implements ArgumentValueResolverInterface {
private ValidatorInterface $validator;
private ClassMetadataInterface $classMetadata;
private TranslatorInterface $translator;
public function __construct(
ValidatorInterface $validator,
TranslatorInterface $translator
) {
$this->validator = $validator;
$this->translator = $translator;
}
/**
* https://symfony.com/doc/current/reference/constraints.html
*
* @return \Generator|iterable
*/
public function resolve(Request $request, ArgumentMetadata $argument) {
// creating new instance of custom request DTO
$class = $argument->getType();
$dto = new $class($request, $this->translator);
// throw bad request exception in case of invalid request data
$errors = $this->validator->validate($dto);
if (0 < count($errors)) {
throw new BadRequestHttpException((string)$errors);
}
yield $dto;
}
/**
* @return bool|void
* @throws \ReflectionException
*/
public function supports(Request $request, ArgumentMetadata $argument) {
$reflection = new ReflectionClass($argument->getType());
if ($reflection->implementsInterface(RequestDTOInterface::class)) {
return true;
}
return false;
}
}
final class ValidationExceptionEventSubscriber implements EventSubscriberInterface {
use ApiControllerTrait;
public function __construct(SerializerInterface $serializer) {
$this->serializer = $serializer;
}
public static function getSubscribedEvents(): array {
return [
KernelEvents::EXCEPTION => [
['processValidationException', 11],
],
];
}
public function processValidationException(ExceptionEvent $event): void {
/** @var ValidationException $exception */
$exception = $event->getThrowable();
$isValidationFormException = \get_class($exception) === ValidationException::class;
if (false === $isValidationFormException || false === $event->isMasterRequest()) {
return;
}
$event->setResponse(
$this->createResponse(
$exception->getMessage(),
[
'violations' => $exception->violations(),
],
$this->statusError,
null,
Response::HTTP_NOT_ACCEPTABLE
)
);
}
}