I have a big entity and a big form. When updating my entity, I only render parts of my form, through ajax calls. On client side, I\'m using Jquery and html5 FormData
The Symfony CheckboxType, at least in the current version 3.3 (seems like since 2.3, see update below), accepts an input value of null
, interpreted as "not checked" (as you can see in lines 3-5 of the snippet in Roubi's really helpful answer).
So in your client-side AJAX-PATCH-controller you set the value of of your (dirty) unchecked checkbox-field in your application/merge-patch+json
payload to null
and everything is fine. No form extensions overwriting CheckboxType's behavior needed at all.
Problem is: I think, you cannot set values of HTTP-POST-payload to null
, so this only works with JSON (or other compatible) payload within the request body.
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:
Submitting the checkbox with null
value
{"checkbox": null}
will overwrite the checkbox to false:
{"checkbox": false, "dummyfield": "presetvalue"}
and submitting the checkbox with its original value
{"checkbox": "1"}
will set the checkbox to true (was also true before)
{"checkbox": true, "dummyfield": "presetvalue"}
and no submitted value for the checkbox
{"dummyfield": "requestvalue"}
will leave the checkbox in its initial true-state and only overwrites the dummyfield:
{"checkbox": true, "dummyfield": "requestvalue"}
This is how a PATCH request should work, no extra hidden inputs needed. Just prepare your JSON-payload on the client-side properly and you are fine.
For expanded ChoiceType (or child types of it like EntityType), which renders checkboxes or radiobuttons and expects a simple list of the checked checkboxes/radiobuttons values within the submitted payload, this simple solution doesn't work. I implemented a form extension, adding an event listener for PRE_SUBMIT on those fields, setting the non submitted checkboxes/radiobuttons to null. This event listener must be called after the closure-listener of CheckboxType, transferring the simple list ["1", "3"]
to a hash with checkbox-values as keys and values. A priority of -1
workes for me. So ["1" => "1", "3" => "3"]
coming out of the closure 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: have a look at this comment within the submit-method in /Symfony/Component/Form/Form.php (it is there since 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: Radiogroups must be handled the same way as Checkboxgroups, so my listener handles both. Selects and multi selects work correctly out of the box.
Here goes a working solution, that could be improved.
To force unchecked checkboxes to appear in the request, thanks to Felix Kling's comment on this question, I've 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 behaviour, that returns true
for whatever string and false
only for null value. Making a change in the last line, we now return false if the value doesn't match the value defined for true ("1" by default). So 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 made my own DataTransformer, as well as a custom AjaxCheckboxType
Unfortunately, it seems that Symfony uses both DataTransformers (mine and the original one), one after the other, so it didn't work. In the docs they extend TextType
not CheckboxType
, that must explain the problems I encountered.
I ended up copying and pasting the whole CheckboxType class
in my own AjaxCheckboxType, only changing the DataTransformer's call in order to use mine.
A much nicer solution would be to totally override the DataTransformer, but I don't know how.
You are absolutely right, Symfony will ignore it if method is PATCH because of this line in Request Handler:
$form->submit($data, 'PATCH' !== $method);
Now, I would generally suggest that you use a PUT
request if that is an option, but if it isn't then second argument to FormInterface::submit($submittedData, $clearMissing = true)
is what you're after.
The "proper" way would probably be to make your own implementation of Symfony\Component\Form\RequestHandlerInterface which would force $clearMissing
to be true
.
Other, way is a lot easier but might 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 second parameter since true
is the default value