-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Description
Q | A |
---|---|
Bug report? | no |
Feature request? | yes |
BC Break report? | no |
RFC? | n/a |
Symfony version | all |
Use cases (few select ones):
- expanded/multiple ChoiceType (checkbox list) sortable via JS
- CollectionType sortable via JS without reassigning indexes (to avoid breaking entity identity/content mismatch)
Can this be solved with custom FormTypeExtension? Yes, but its somewhat bloated and requires deep knowledge of the form component to get it working correctly and consistently, definitely far beyond "entry" level experience for such common use cases.
Was this issue brought up before? Yes, but declined for various reason. One of the major arguments was, that browsers are not required to maintain a specific order of submitted form values. This isnt true: https://www.w3.org/TR/html5/forms.html#constructing-form-data-set requires tree order.
Furthermore the data format that the form component is expecting (in compound forms) are PHP arrays (FormInterface::submit(null|string|array $submittedData, ...)
, null
treated as empty array) and PHP arrays are ordered hash maps and therefore exhibit an explicit ordering of its key-value pairs.
ref #10575, #4492, #8315, #8987
Here are some classes i use in my projects to achieve this:
// this one has the problem that it always clears the default data and completed replaces it with default data
class RestoreInputOrderTypeExtension extends AbstractTypeExtension {
public function buildForm(FormBuilderInterface $builder, array $options) {
if(!$options['restore_input_order'] || !$options['compound']) {
return;
}
$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $fe) {
$data = $fe->getData();
if($data === null) {
return;
}
if(!is_array($data)) {
throw new TransformationFailedException('expected array', 1);
}
$form = $fe->getForm();
foreach(array_keys($data) as $name) {
if($form->has($name)) {
$child = $form->get($name);
$form->remove($name);
$form->add($child);
}
}
}, -100);
$builder->addEventListener(FormEvents::SUBMIT, function(FormEvent $fe) {
$data = $fe->getData();
if($data === null) {
return;
}
if(!is_array($data)) {
throw new TransformationFailedException('expected array', 1);
}
$orderedData = array();
foreach(array_keys($fe->getForm()->all()) as $name) {
$orderedData[$name] = $data[$name];
}
$fe->setData($orderedData);
}, -100);
}
/**
* @see \Symfony\Component\Form\AbstractTypeExtension::setDefaultOptions()
*/
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'restore_input_order' => false,
));
}
/**
* @see \Symfony\Component\Form\FormTypeExtensionInterface::getExtendedType()
*/
public function getExtendedType() {
return 'form';
}
}
class RestoreCheckboxInputOrderListener implements EventSubscriberInterface {
private $inputData;
private $choiceList;
public function __construct(ChoiceListInterface $choiceList) {
$this->choiceList = $choiceList;
}
public function preSubmit(FormEvent $event) {
$this->inputData = $event->getData();
}
public function submit(FormEvent $event) {
$data = array();
foreach($this->choiceList->getChoicesForValues($this->inputData) as $inputChoice) {
foreach($event->getData() as $i => $choice) {
if($choice === $inputChoice) {
$data[$i] = $choice;
}
}
}
$event->setData($data);
}
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SUBMIT => array('preSubmit', 100),
FormEvents::SUBMIT => 'submit'
);
}
}