- Запускаем docker-контейнеры командой
docker-compose up -d
- Логинимся в контейнер
php
командойdocker exec -it php sh
. Дальнейшие команды выполняются из контейнера. - Устанавливаем зависимости командой
composer install
- Устанавливаем пакет
symfony/validator
- Добавляем класс
App\Controller\Input\InputDTO
<?php namespace App\Controller\Input; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; class InputDTO { #[Assert\NotBlank] public string $notEmptyString; #[Assert\NotNull] public string $notNullString; #[Assert\Type('integer')] public int $int; public static function fillFromRequest(Request $request): self { $result = new self(); $result->notEmptyString = $request->request->get('notEmptyString'); $result->notNullString = $request->request->get('notNullString'); $result->int = $request->request->get('int'); return $result; } }
- Добавляем класс
App\Controller\Controller
<?php namespace App\Controller; use App\Controller\Input\InputDTO; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Validator\Validator\ValidatorInterface; #[AsController] #[Route('/api/v1/validator-test', methods: ['POST'])] class Controller { public function __construct( private readonly ValidatorInterface $validator ) { } public function __invoke(Request $request): Response { $dto = InputDTO::fillFromRequest($request); $errors = $this->validator->validate($dto); if (count($errors) > 0) { return new Response((string)$errors, Response::HTTP_BAD_REQUEST); } return new Response('Success'); } }
- Добавляем файл
config/routes/attributes.yaml
controllers:
resource:
path: ../../src/Controller/
namespace: App\Controller
type: attribute
- Отправляем запрос
Validator simple test
из Postman-коллекции, получаем ответ с кодом 200 - Отправляем запрос
Validator simple test
с пустым параметромnotEmptyString
, получаем ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator simple test
с пустым параметромnotNullString
, получаем ответ с кодом 200 - Отправляем запрос
Validator simple test
без параметраnotNullString
, получаем ответ с кодом 500 - Отправляем запрос
Validator simple test
с параметромint
, равным строке, не являющейся числом, получаем ответ с кодом 500 - Исправляем класс
App\Controller\Input\InputDTO
<?php namespace App\Controller\Input; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; class InputDTO { #[Assert\NotBlank] public string $notEmptyString; #[Assert\NotNull] public ?string $notNullString; #[Assert\Type('integer')] public $int; public static function fillFromRequest(Request $request): self { $result = new self(); $result->notEmptyString = $request->request->get('notEmptyString'); $result->notNullString = $request->request->get('notNullString'); $result->int = $request->request->get('int'); return $result; } }
- Отправляем запрос
Validator simple test
без параметраnotNullString
, получаем ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator simple test
с параметромint
, равным строке, не являющейся числом, получаем ответ с кодом 400 и текстом ошибки
- Исправляем класс
App\Controller\Input\InputDTO
<?php namespace App\Controller\Input; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; class InputDTO { #[Assert\Choice(choices: ['correct', 'valid'])] public string $choice; public static function fillFromRequest(Request $request): self { $result = new self(); $result->choice = $request->request->get('choice'); return $result; } }
- Отправляем запрос
Validator choice test
, видим ответ с кодом 200 - Отправляем запрос
Validator choice test
с параметромchoice = bad
, видим ответ с кодом 400 и текстом ошибки
- Исправляем класс
App\Controller\Input\InputDTO
<?php namespace App\Controller\Input; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; class InputDTO { #[Assert\Choice(choices: ['correct', 'valid'], multiple: true)] #[Assert\Type('array')] public array $choices; public static function fillFromRequest(Request $request): self { $result = new self(); $result->choices = $request->request->all('choices'); return $result; } }
- Отправляем запрос
Validator array test
, видим ответ с кодом 200 - Отправляем запрос
Validator array test
со вторым параметромchoices[] = bad
, видим ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator array test
со единственным параметромchoices = valid
, видим ответ с кодом 400, но ошибку выдаёт не компонент валидации
- Добавляем перечисление
App\Enums\ValueEnum
<?php namespace App\Enums; enum ValueEnum: string { case Valid = 'valid'; case Correct = 'correct'; }
- Исправляем класс
App\Controller\Input\InputDTO
<?php namespace App\Controller\Input; use App\Enums\ValueEnum; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; class InputDTO { #[Assert\Choice(callback: [ValueEnum::class, 'cases'], multiple: true)] #[Assert\Type('array')] public array $choices; public static function fillFromRequest(Request $request): self { $result = new self(); $result->choices = array_map( static fn(string $choice): ValueEnum => ValueEnum::from($choice), $request->request->all('choices') ); return $result; } }
- Отправляем запрос
Validator array test
, видим ответ с кодом 200 - Отправляем запрос
Validator array test
со вторым параметромchoices[] = bad
, видим ответ с кодом 500 - Исправляем класс
App\Controller\Input\InputDTO
<?php namespace App\Controller\Input; use App\Enums\ValueEnum; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; class InputDTO { #[Assert\Choice(callback: [ValueEnum::class, 'cases'], multiple: true)] #[Assert\Type('array')] public array $choices; public static function fillFromRequest(Request $request): self { $result = new self(); $result->choices = array_map( static fn(string $choice): ?ValueEnum => ValueEnum::tryFrom($choice), $request->request->all('choices') ); return $result; } }
- Устанавливаем пакет
symfony/expression-language
- Добавляем класс
App\Enums\TypeEnum
<?php namespace App\Enums; enum TypeEnum: string { case Absolute = 'absolute'; case Relative = 'relative'; public static function values(): array { return array_map(static fn(TypeEnum $value): string => $value->value, self::cases()); } }
- Исправляем класс
App\Controller\Input\InputDTO
<?php namespace App\Controller\Input; use App\Enums\TypeEnum; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; #[Assert\Expression( "(this.type === 'absolute' and this.value > -1000 and this.value < 1000) or (this.type === 'relative' and this.value >= 0 and this.value <= 100)" )] class InputDTO { public int $value; #[Assert\Choice(callback: [TypeEnum::class, 'values'])] public string $type; public static function fillFromRequest(Request $request): self { $result = new self(); $result->type = $request->request->get('type'); $result->value = $request->request->get('value'); return $result; } }
- Отправляем запрос
Validator conditional test
, видим ответ с кодом 200 - Отправляем запрос
Validator conditional test
с параметромvalue = -2000
, видим ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator conditional test
с параметрамиtype = relative
иvalue = -20
, видим ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator conditional test
с параметрамиtype = relative
иvalue = 99
, видим ответ с кодом 200 - Отправляем запрос
Validator conditional test
с параметрамиtype = other
, видим ответ с кодом 400 и текстом ошибки
- Добавляем перечисление
App\Enums\NameEnum
<?php namespace App\Enums; enum NameEnum: int { case Large = 20; case Medium = 15; case Small = 10; public static function values(): array { return array_map(static fn(NameEnum $value): string => $value->value, self::cases()); } public static function names(): array { return array_map(static fn(NameEnum $value): string => $value->name, self::cases()); } public static function getValueByName(string $name): ?int { return array_combine(self::names(), self::cases())[$name] ?? null; } }
- Исправляем перечисление
App\Enums\TypeEnum
<?php namespace App\Enums; enum TypeEnum: string { case Absolute = 'absolute'; case Relative = 'relative'; case Name = 'name'; public static function values(): array { return array_map(static fn(TypeEnum $value): string => $value->value, self::cases()); } }
- Исправляем класс
App\Controller\Input\InputDTO
<?php namespace App\Controller\Input; use App\Enums\TypeEnum; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; class InputDTO { #[Assert\When( expression: "this.type === 'absolute'", constraints: [ new Assert\GreaterThan(-1000), new Assert\LessThan(1000), ] )] #[Assert\When( expression: "this.type === 'relative'", constraints: [ new Assert\GreaterThanOrEqual(0), new Assert\LessThanOrEqual(100), ] )] #[Assert\When( expression: "this.type === 'name'", constraints: [ new Assert\NotNull(), new Assert\Choice(callback: [NameEnum::class, 'names']), ] )] public mixed $value; #[Assert\Choice(callback: [TypeEnum::class, 'values'])] public string $type; public static function fillFromRequest(Request $request): self { $result = new self(); $result->type = $request->request->get('type'); $result->value = $request->request->get('value'); return $result; } }
- Отправляем запрос
Validator conditional test
, видим ответ с кодом 200 - Отправляем запрос
Validator conditional test
с параметромvalue = -2000
, видим ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator conditional test
с параметрамиtype = relative
иvalue = -20
, видим ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator conditional test
с параметрамиtype = relative
иvalue = 99
, видим ответ с кодом 200 - Отправляем запрос
Validator conditional test
с параметрамиtype = other
, видим ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator conditional test
с параметрамиtype = name
иvalue = other
, видим ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator conditional test
с параметрамиtype = name
иvalue = Large
, видим ответ с кодом 200
- Добавляем класс
App\Constraint\RangeConstraint
<?php namespace App\Constraint; use Attribute; use Symfony\Component\Validator\Constraints\Compound; use Symfony\Component\Validator\Constraints as Assert; #[Attribute] class RangeConstraint extends Compound { protected function getConstraints(array $options): array { return [ new Assert\NotNull(), new Assert\GreaterThanOrEqual($options['payload']['min']), new Assert\LessThan($options['payload']['max']), ]; } }
- Исправляем класс
App\Controller\Input\InputDTO
<?php namespace App\Controller\Input; use App\Constraint\RangeConstraint; use App\Enums\NameEnum; use App\Enums\TypeEnum; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; class InputDTO { #[Assert\When( expression: "this.type === 'absolute'", constraints: [ new Assert\GreaterThan(-1000), new Assert\LessThan(1000), ] )] #[Assert\When( expression: "this.type === 'relative'", constraints: [ new RangeConstraint(['payload' => ['min' => 0, 'max' => 100]]), ] )] #[Assert\When( expression: "this.type === 'name'", constraints: [ new Assert\NotNull(), new Assert\Choice(callback: [NameEnum::class, 'names']), ] )] public mixed $value; #[Assert\Choice(callback: [TypeEnum::class, 'values'])] public string $type; public static function fillFromRequest(Request $request): self { $result = new self(); $result->type = $request->request->get('type'); $result->value = $request->request->get('value'); return $result; } }
- Отправляем запрос
Validator conditional test
с параметрамиtype = relative
иvalue = 5
, видим ответ с кодом 200 - Отправляем запрос
Validator conditional test
с параметрамиtype = relative
иvalue = -5
, видим ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator conditional test
с параметрамиtype = relative
иvalue = 101
, видим ответ с кодом 400 и текстом ошибки
- Добавляем класс
App\Constraint\CustomRangeConstraint
<?php namespace App\Constraint; use Attribute; use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; #[Attribute] class CustomRangeConstraint extends Constraint { #[HasNamedArguments] public function __construct( public readonly int $min, public readonly int $max, array $groups = null, $payload = null ) { parent::__construct([], $groups, $payload); } }
- Добавляем класс
App\Constraint\CustomConstraintValidator
<?php namespace App\Constraint; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; class CustomRangeConstraintValidator extends ConstraintValidator { public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof CustomRangeConstraint) { throw new UnexpectedTypeException($constraint, CustomRangeConstraint::class); } if (!is_numeric($value)) { throw new UnexpectedValueException($value, 'int'); } if ($value >= $constraint->min && $value <= $constraint->max) { return; } $this->context->buildViolation('Value should be in range [{{ min }}, {{ max }}].') ->setParameter('{{ min }}', $constraint->min) ->setParameter('{{ max }}', $constraint->max) ->addViolation(); } }
- Исправляем класс
App\Controller\Input\InputDTO
<?php namespace App\Controller\Input; use App\Constraint\CustomRangeConstraint; use App\Constraint\RangeConstraint; use App\Enums\NameEnum; use App\Enums\TypeEnum; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; class InputDTO { #[Assert\When( expression: "this.type === 'absolute'", constraints: [ new Assert\GreaterThan(-1000), new Assert\LessThan(1000), ] )] #[Assert\When( expression: "this.type === 'relative'", constraints: [ new CustomRangeConstraint(min: 0, max: 100), ] )] #[Assert\When( expression: "this.type === 'name'", constraints: [ new Assert\NotNull(), new Assert\Choice(callback: [NameEnum::class, 'names']), ] )] public mixed $value; #[Assert\Choice(callback: [TypeEnum::class, 'values'])] public string $type; public static function fillFromRequest(Request $request): self { $result = new self(); $result->type = $request->request->get('type'); $result->value = $request->request->get('value'); return $result; } }
- Отправляем запрос
Validator conditional test
с параметрамиtype = relative
иvalue = 5
, видим ответ с кодом 200 - Отправляем запрос
Validator conditional test
с параметрамиtype = relative
иvalue = -5
, видим ответ с кодом 400 и текстом ошибки - Отправляем запрос
Validator conditional test
с параметрамиtype = relative
иvalue = 101
, видим ответ с кодом 400 и текстом ошибки