phpsymfonyvalidationsymfony6

Validation Constraints: How to assert that a string has exact length 4


Here is my simplified class of my DTO which is filled by the Serializer:

<?php

namespace App\Api\Dto;

use OpenApi\Attributes as OA;
use Symfony\Component\Serializer\Annotation\Groups as SerializerGroups;
use Symfony\Component\Validator\Constraints as Assert;


#[OA\Schema()]
readonly class WorkHour implements DtoInterface
{
    public function __construct(
        #[OA\Property(description: 'House name', type: 'string')]
        #[Assert\Length(
            exactly: 4,
            exactMessage: 'House name is mandatory. Stringlength 4',
            groups: ['create', 'update']
        )]
        #[SerializerGroups(['view', 'create', 'update', 'collection'])]
        public string $sapHouseNumber
        ) {
    }
}

I am writing unit tests and try to fail the validation by sending an empty string for this value with creation. But the json is serialized and my DTO is created without any complaint.

Is the Assert correct? Should it work?

If yes, I have to search for the error elsewhere in my code. What am I missing? Why is my validation not working?

It looks like the class Length is used, but not the class LengthValidator.

php8.2, Smyfony 6.4


Solution

  • The error was, that I hadn't configured MapRequestPayload correctly in my Controller. I will add the essential code here, for all others, who have a similar problem.

    The Constraint was configured correctly. I added in my Controller the following:

            Route(path: '', name: 'create', methods: ['POST'], format: 'json')]
        public function create(#[MapRequestPayload(
            acceptFormat: 'json',
            serializationContext: ['create'],
            validationGroups: ['create'],
            resolver: DtoArgumentResolver::class
        )] WorkHourDto $workHourDto): CreatedView
        {
            $workHourDto = $this->workHourService->create($workHourDto);
    
            return $this->created($workHourDto);
        }
    

    DtoArgumentResolver being the class were I call the serialisation and Vaidation:

    class DtoArgumentResolver implements ValueResolverInterface
    {
        use LoggerTrait;
    
        private SerializerInterface $serializer;
        private ValidatorInterface $validator;
    
        public function __construct(
            SerializerInterface $serializer,
            ValidatorInterface $validator
        ) {
            $this->serializer = $serializer;
            $this->validator = $validator;
        }
    
        public function supports(Request $request, ArgumentMetadata $argument): bool
        {
            if (null === $argument->getType()) {
                return false;
            }
    
            return 1 === preg_match('/App\\\\Api\\\\Dto\\\\[a-zA-Z]*/', $argument->getType());
        }
    
        public function resolve(Request $request, ArgumentMetadata $argument): Generator
        {
            $data = $request->getContent();
            $class = $argument->getType();
    
            if (empty($data)) {
                throw new InvalidArgumentException('Request data is empty');
            }
    
            try {
                /** @var DtoInterface $dto */
                $dto = $this->serializer->deserialize($data, $class, 'json');
            } catch (Exception $e) {
                $message = sprintf('Object of class %s caanot be created with this data: %s', $class, $data);
                throw new ApiException(Response::HTTP_UNPROCESSABLE_ENTITY, $message, $e, [], 1056);
            }
    
            $groups = null;
            if ($request->attributes->has('_validation_group')) {
                $groups = $request->attributes->get('_validation_group')->getGroup();
            }
    
            // we need the id for all PUT methods
            if ('PUT' === $request->getMethod()) {
                $violations = $this->validator->validate(
                    ['dto' => $dto, 'id' => $request->attributes->get('id')],
                    null,
                    $groups
                );
            } else {
                $violations = $this->validator->validate($dto, null, $groups);
            }
    
            if (count($violations) > 0) {
                throw ValidationException::fromViolationCollection($violations);
            }
    
            yield $dto;
        }
    }