Home > Software engineering >  Symfony 5.4 - how to filter/sanitize/validate request parameter in Rest API action
Symfony 5.4 - how to filter/sanitize/validate request parameter in Rest API action

Time:11-13

I am fairly new to Symfony 5.4 and recently created my first API using that version

For my specific API endpoint one of the parameters is an array of IDs.

I need to validate this array in the following way:

  • make sure that this IS an array;
  • make sure that IDs in the array actually refer to database records;

I implemented it in a straightforward way where I check the array before persisting the entity using typecasting and existing Repository:

$parentPropertyIds = (array)$request->request->get('parent_property_ids');
if ($parentPropertyIds) {
   $parentCount = $doctrine->getRepository(Property::class)->countByIds($parentPropertyIds);

   if ($parentCount !== count($parentPropertyIds)) {
       return $this->json([
            'status'  => 'error',
            'message' => 'parent_property_id_invalid'
       ], 422);
   }

   foreach ($parentPropertyIds as $parentPropertyId) {
      $parentProperty = $doctrine->getRepository(Property::class)->find($parentPropertyId);
      $property->addParent($parentProperty);
   }
}

However, this makes my controller action become too "body-positive" and also feels like something that could be implemented in a more elegant way.

I was unable to find anything in Symfony 5.4 docs.

At the moment I am wondering if:

  • there is a way to filter/sanitize request parameter available in Symfony;
  • there is an elegant built-in way to apply custom validator constraint to a request param (similar to well-documented entity field validation);

Full endpoint code:

/**
     * @Route("/property", name="property_new", methods={"POST"})
     */
    public function create(ManagerRegistry $doctrine, Request $request, ValidatorInterface $validator): Response
    {
        $entityManager = $doctrine->getManager();

        $property = new Property();
        $property->setName($request->request->get('name'));
        $property->setCanBeShared((bool)$request->request->get('can_be_shared'));

        $parentPropertyIds = (array)$request->request->get('parent_property_ids');
        if ($parentPropertyIds) {
            $parentCount = $doctrine
                ->getRepository(Property::class)
                ->countByIds($parentPropertyIds);

            if ($parentCount !== count($parentPropertyIds)) {
                return $this->json([
                    'status'  => 'error',
                    'message' => 'parent_property_id_invalid'
                ], 422);
            }

            foreach ($parentPropertyIds as $parentPropertyId) {
                $parentProperty = $doctrine->getRepository(Property::class)->find($parentPropertyId);
                $property->addParent($parentProperty);
            }
        }

        $errors = $validator->validate($property);

        if (count($errors) > 0) {
            $messages = [];
            foreach ($errors as $violation) {
                $messages[$violation->getPropertyPath()][] = $violation->getMessage();
            }
            return $this->json([
                'status'   => 'error',
                'messages' => $messages
            ], 422);
        }

        $entityManager->persist($property);
        $entityManager->flush();

        return $this->json([
            'status' => 'ok',
            'id'     => $property->getId()
        ]);
    }

CodePudding user response:

You could use a combination of Data Transfer Object (DTO) with Validation service. There is a number of predefined constraints or you could create a custom one.

For expamle, how to use simple constraint as an annotation:

class PropertyDTO {
  /**
   * @Assert\NotBlank
   */
  public string $name = "";
  public bool $shared = false;
}

Then assign data to DTO:

$propertyData = new PropertyDTO();
$propertyData->name = $request->request->get('name');
...

In some cases it is a good idea to define a constructor in the DTO, then get all data from the request and pass it to DTO at once:

$data = $request->getContent(); // or $request->getArray(); depends on your content type
$propertyData = new PropertyDTO($data);

Then validate it:

$errors = $validator->validate($propertyData);

if (count($errors) > 0) {
    /*
     * Uses a __toString method on the $errors variable which is a
     * ConstraintViolationList object. This gives us a nice string
     * for debugging.
     */
    $errorsString = (string) $errors;

    return $this->json([
                'status'  => 'error',
                'message' => 'parent_property_id_invalid'
            ], 422);
}

//...
  • Related