vittorio
vittorio

Reputation: 181

Prevent Php/Symfony from type-casting 'true' value

I have a Symfony (4.3) Form and some validation rules.

In my App\Entity\Objectif class :

/**
* @ORM\Column(type="float", options={"default" : 0})
* @Assert\Type("float")
*/
private $budget;

In my App\Form\ObjectifType class :

public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
    ->add('budget')
    ->add('userQuantity')
    /* some other parameters */
;
}

In my App\Controller\ObjectifController class :

public function generateMenu(Request $request)
{
    $form = $this->createForm(ObjectifType::class);
    $data = json_decode($request->getContent(),true);
    $form->submit($data);

    if ($form->isValid()) {
    /* do some stuff with data */
    } else {
      return $this->json('some error message');
    }
}

My Symfony application is an API, so I receive data formatted in Json from the frontend.

My goal is to ensure that value sended by end-user as $budget is of float type.

Problem : the validation process does not work for value 'true'.

If end-user sends a string as $budget, the process works and the validation fails as it should.

If end-user sends the value 'true' as $budget, that value gets implicitly type-casted as a '1' and so the validation succeed, which souldn't happen.

How do I force PHP or Symfony to not implicitly type-cast 'true' to '1' in that situation ?

Thank you


TESTS (optionnal reading)

For testing purpose, I put a Callbak validator (symfony doc) in my App\Entity\Objectif class, whose only purpose is to output the type of $budget property when form is being validated :

// App\Entity\Objectif.php

/**
* @Assert\Callback
*/
public function validate(ExecutionContextInterface $context, $payload)
{

dump('Actual value is : ' . $this->budget);

if (!is_float($this->budget)) {
    dump('Value is NOT FLOAT');
    $context->buildViolation('This is not a float type.')
            ->atPath('budget')
            ->addViolation();
    exit;
  } else {
    dump('Value is FLOAT'); 
    exit;
  }
}

If I send 'true' as the 'budget' key with my API testing software (Insomnia) :

{
    "budget": true
}

I always get those outputs :

Objectif.php on line 193:
"Actual value is : 1"
Objectif.php on line 202:
"Value is FLOAT"

I suspect it is a Symfony problem, because when i use PHP CLI :

php > var_dump(is_float(true));
bool(false)

I get correct result.

By the way, 'false' value get autocasted to 'null', which doesn't bother regarding my validation purpose, but I don't find if necesary.

Upvotes: 0

Views: 840

Answers (2)

vittorio
vittorio

Reputation: 181

So, after some research, I found out that submit() method of classes that implements FormInterface do cast every scalar value (i.e. integer, float, string or boolean) to string (and "false" values to "null") :

// Symfony\Component\Form\Form.php
// l. 531 (Symfony 4.3)

if (false === $submittedData) {
          $submittedData = null;
} elseif (is_scalar($submittedData)) {
          $submittedData = (string) $submittedData;
}

This has nothing to do with the validation process (which was, in my case, correctly set up).

This explains why I get "true" value casted to "1".

Someone know why symfony's code is designed that way ? I don't get it.

I tried to get rid of submit() method in my controller by using the more conventionnal handleRequest() method. But it does not change anything at all, since handleRequest internally calls submit() (see handleRequest() in HttpFoundationRequestHandler class). So "true" is still casted to "1".

So I ended up using Chris's solution (cf. above). It works perfectly. All credits goes to him :

public function generateMenu(
    Request $request,
    ValidatorInterface $validator
)
{
    $data = json_decode(
        strip_tags($request->getContent()),
        true);

    $dto = ObjectifDto::fromRequestData($data);
    $errors = $validator->validate($dto);

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

    // everything is fine. keep going
}

I think it is a good habit to not use Symfony's Form validation process, at least when dealing with API and JSON.

I feel like the 'raw' (so to speak) Validator, combined with DataTransfertObject, gives much more control. Most importantly, it does not autocast your values for whatever reasons (which would totally screw your validation process)...

Upvotes: 1

Chris
Chris

Reputation: 799

I can't tell you where and why the Form Component changes true to 1 without further investigation but you could use a DataTransferObject and validate against that before submitting the form.

class ObjectifDto
{
    /**
     * @Assert\Type("float")
     * @var float
     */
    public $budget;

    public static function fromRequestData($data): self
    {
        $objectifDto= new self();
        $objectifDto->budget = $data['budget'] ?? 0;

        return $objectifDto;
    }
}

In the Controller:

public function generateMenu(Request $request, ValidatorInterface $validator)
{
    $data = json_decode($request->getContent(),true);
    $dto = ObjectifDto::fromRequestData($data);
    $errors = $validator->validate($dto);

    //return errors or go on with the form or persist manually
}

Upvotes: 1

Related Questions