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:
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):
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.