Symfony3: how to upgrade boolean to false using the PATCH method

I have a big essence and a big form. When updating my entity, I process only parts of my form through ajax calls. On the client side, I use jquery and html5 FormData strong> so that I can submit files in my form. To make sure that fields that are not displayed will not be set to zero in the process, I use the PATCH method .

So, when this field is absent in the request, it remains as in Symfony.

But when the field I update is a boolean (visualized aa flag) that was set to true and I want to set it to false, it is not passed in the request, so my update is ignored .

Is there an easy way to make raw flags appear in a request?

EDIT
I found a way to make unchecked flags appear in the request, thanks to Felix Kling's comment on this question :

$("input:checkbox:not(:checked)").each(function() { formData.set($(this).attr('name'), formData.has($(this).attr('id')) ? '1' : '0'); }); 

Unfortunately, this did not solve my problem , due to the behavior of Symfony:
- When using PUT, if a logical field appears in the request, it sets to true, regardless of its value (even if it is "0" or "false").
- When using the PATCH method, fields that are not displayed in the request are ignored.

Can this be resolved with a DataTransformer? (I never used it)

+5
source share
3 answers

Here is a working solution that can be improved .

In order not to show the checkboxes in the request, thanks to the comment by Felix Kling on this question , I added this js before my ajax request:

 $("input:checkbox:not(:checked)").each(function() { formData.set($(this).attr('name'), formData.has($(this).attr('id')) ? '1' : '0'); }); 

Then, on the Symfony side, I had to override the BooleanToStringTransformer behavior, which returns true for any string and false only for a null value. By making changes to the last line, we now return false if the value does not match the value set to true (the default is "1"). Therefore, if the value returned by the form is "0", we get false , as expected.

 public function reverseTransform($value) { if (null === $value) { return false; } if (!is_string($value)) { throw new TransformationFailedException('Expected a string.'); } return ($this->trueValue === $value); // initially: "return true;" } 

Following the docs , I created my own DataTransformer as well as a custom AjaxCheckboxType

Unfortunately, it seems that Symfony uses both DataTransformers (mine and original) one by one, so it doesn't work. In the docs, they extend TextType not TextType , which should explain the problems I am having.

I ended up copying and pasting the entire CheckboxType class into my own AjaxCheckboxType, only changing the DataTransformer call to use mine.

A nicer solution would be to completely override the DataTransformer, but I don't know how to do this.

+1
source

You are absolutely right, Symfony will ignore it if the PATCH method due to this line in the request handler :

 $form->submit($data, 'PATCH' !== $method); 

Now I usually suggest you use a PUT request if this is an option, but if it is not the second argument to FormInterface::submit($submittedData, $clearMissing = true) , this is what you need.

The "correct" way is probably to make your own implementation of Symfony\Component\Form\RequestHandlerInterface , which will make $clearMissing be true .

Another way is much simpler, but may not work for all use cases: use $form->submit() directly.

If you have the following code:

 $form->handleRequest($request); 

You can do:

 $form->submit($request->get($form->getName()), true); 

You can also omit the second parameter, since true is the default value.

+4
source

Symfony handles this out of the box, properly prepare your PATCH payload :)

Symfony CheckboxType, at least in the current version 3.3 (it seems, starting from version 2.3, see the update below), accepts an input value of null , interpreted as “not verified” (as you can see in lines 3-5 of the fragment in Ruby it’s really useful answer ).

So, on your client AJAX-PATCH controller, you set the value of your (dirty) flag in the application/merge-patch+json field to null and everything is fine. No form extensions overwrite CheckboxType behavior at all.

Problem: I think you cannot set the HTTP-POST payload values ​​to null , so this only works with the JSON payload (or other compatible) in the request body.

Simple demonstration

To demonstrate this, you can use this simplified test controller:

 /** * @Route("/test/patch.json", name="test_patch") * @Method({"PATCH"}) */ public function patchAction(\Symfony\Component\HttpFoundation\Request $request) { $form = $this->createFormBuilder(['checkbox' => true, 'dummyfield' => 'presetvalue'], ['csrf_protection' => false]) ->setAction($this->generateUrl($request->get('_route'))) ->setMethod('PATCH') ->add('checkbox', \Symfony\Component\Form\Extension\Core\Type\CheckboxType::class) ->add('dummyfield', \Symfony\Component\Form\Extension\Core\Type\TextType::class) ->getForm() ; $form->submit(json_decode($request->getContent(), true), false); return new \Symfony\Component\HttpFoundation\JsonResponse($form->getData()); } 

For PATCH requests with Content-Type: application/merge-patch+json or in this case also any valid JSON payload, the following will happen:

Sending a flag with a null value

 {"checkbox": null} 

will replace the checkbox with false:

 {"checkbox": false, "dummyfield": "presetvalue"} 

and check the box with its original value

 {"checkbox": "1"} 

set the flag to true (also true earlier)

 {"checkbox": true, "dummyfield": "presetvalue"} 

and not passed value for flag

 {"dummyfield": "requestvalue"} 

will leave the checkbox in its original true state and will only overwrite the dummy field:

 {"checkbox": true, "dummyfield": "requestvalue"} 

This is how the PATCH request should work; no additional hidden input is required. Just prepare your client side JSON payload correctly and you're fine.

OK, but what about the extended ChoiceType / EntityType type?

For an extended ChoiceType (or its child types, such as EntityType) that displays flags or radio buttons and expects a simple list of checked radio flag / values ​​in the presented payload, this simple solution does not work. I implemented the form extension by adding an event listener for PRE_SUBMIT to these fields, setting the non-broadcast / radioobuttons checkboxes to null. This event listener should be called after closing the CheckboxType listener, transferring a simple list ["1", "3"] to the hash with value flags in the form of keys and values. Priority -1 works for me. So, ["1" => "1", "3" => "3"] , exiting the close, gets ["1" => "1", "2" => null, "3" => "3"] after my listener. The listener of my PatchableChoiceTypeExtension looks basically like this:

 $builder->addEventListener( \Symfony\Component\Form\FormEvents::PRE_SUBMIT, function (\Symfony\Component\Form\FormEvent $event) { if ('PATCH' === $event->getForm()->getRoot()->getConfig()->getMethod() && $event->getForm()->getConfig()->getOption('expanded', false) ) { $data = $event->getData(); foreach ($event->getForm()->all() as $type) { if (!array_key_exists($type->getName(), $data)) { $data[$type->getName()] = null; } } ksort($data); $event->setData($data); } }, -1 ); 

Update: Check out this comment in the submit method in /Symfony/Component/Form/Form.php (it is with Symfony 2.3):

 // Treat false as NULL to support binding false to checkboxes. // Don't convert NULL to a string here in order to determine later // whether an empty value has been submitted or whether no value has // been submitted at all. This is important for processing checkboxes // and radio buttons with empty values. 

Update 2017-09-12: Radigroups should be processed in the same way as Checkboxgroups, so my listener processes both. Selects and multi-selects the job correctly from the box.

+1
source

Source: https://habr.com/ru/post/1242245/


All Articles