-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Form] Respect order of submitted data when dealing with sorted collection models #21595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Actually, the data is submitted in the order which the submitted data is given, not considering how the form is built, The problem with collections is coming from the |
Sorry I've edited my post as I've miss read the listener example. However changing the order of submission depending on the form tree build on initialization rather than using the submitted data order is a big BC break... |
I would definitely make this an opt-in feature, because order should only matter, if you bind your data to collection-like structures ( Consider a form providing edit capabilities for personal data Its about intention of your model. If your model is intented to expose an ordered collection of things, you want your data binder to be able to bind things in order they were submitted. |
I'm not worried about underlying data, but about form dynamic mechanisms happening on submission. Some may (can) rely on this order although they shouldn't (ref #11241 (comment)). You said this solves:
but one the contrary, your examples seem to make impossible to reorder the model data based on the submitted order. Data is already sortable in the view (i.e via JS). Preventing it with listeners is a lot of overhead and impacts performances. For this reason I think the title of this issue is very confusing. The only use case I see, is when you handle an array of data in the model and you don't want the order is the view to be messed up with. So why not as an opt-in? But IMHO, those listeners can live in user land. |
The order in which the FormInterface submits its children is clearly defined (FIFO). But i dont see how this relates here.
I said i want it as an opt-in feature
Here i dont agree. The 2 use cases i presented are very common and the feature is very hard to get right and consistent without having deep knowledge of the form component.
I dont understand what you mean with that. Also note that the classes i posted are not meant to be anything near PR ready and just some quick drop-ins i use in my projects. I think we are basically on the same domain about the feature itself and how it should/could be done, just not about who should provide it (userland or framework). |
I am closing here as this should be possible if #7828 is implemented |
Hey, sorry to dig out that old issue @xabbuh, but I'm facing the problem again, but with custom (string) Keys on the model data, so #7828 still does not make resorting of model data possible, hence this issue should not have been closed. I dug a little deeper into the code and tried to understand the problem. Here is what I found: Say we have a
To check, if that was the problem, I tried to reorder the collection's form children in So from my understanding, the problem can either be solved inside the data mapper (or with a new one for an opt-in sortable version of Any comments on that? Am I on the right track? |
I solved my problem with an improved solution derived from the <?php
declare(strict_types=1);
namespace App\Form\Extension;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Event\PreSubmitEvent;
use Symfony\Component\Form\Event\SubmitEvent;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class CollectionTypeSortableExtension extends AbstractTypeExtension
{
/** @inheritdoc */
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if (true !== $options['sortable']) {
return;
}
// first, reorder the CollectionType's children by the order of the request
$builder->addEventListener(
eventName: FormEvents::PRE_SUBMIT,
listener: function (PreSubmitEvent $event): void {
$form = $event->getForm();
$data = $event->getData();
if (null === $data || count($form->all()) < 1) {
return;
}
if (!is_array($data)) {
throw new TransformationFailedException('Expected array data');
}
// add the CollectionType's children, that have data within the request in the request order and maintain a list of not yet added children
$unmappedIndexes = new ArrayCollection(array_keys($form->all()));
foreach (array_keys($data) as $childName) {
if (is_int($childName)) {
throw new TransformationFailedException('Integer-indexed data is not allowed, as that could lead to really hard to debug side-effects.');
}
if ($form->has($childName)) {
$currentChild = $form->get($childName);
$form->remove($childName);
$form->add($currentChild);
$unmappedIndexes->removeElement($childName);
}
}
unset($currentChild);
unset($childName);
// also add the remaining children, so that they are not lost
foreach ($unmappedIndexes as $unmappedIndex) {
$currentChild = $form->get($unmappedIndex);
$form->remove($unmappedIndex);
$form->add($currentChild);
}
unset($currentChild);
unset($unmappedIndex);
},
priority: -100
);
// and later, reorder the mapped data (ordered by model state) by the order of the previously reordered CollectionType's children
$builder->addEventListener(
eventName: FormEvents::SUBMIT,
listener: function (SubmitEvent $event): void {
$form = $event->getForm();
$data = $event->getData();
if (null === $data) {
return;
}
// we support ArrayCollection and array for the data
if ($data instanceof ArrayCollection) {
$dataType = 'ArrayCollection';
}
elseif (is_array($data)) {
$dataType = 'array';
$data = new ArrayCollection($data);
}
else {
throw new TransformationFailedException('Expected ArrayCollection or array data');
}
// add the datasets, that have a corresponding form CollectionType's child in the CollectionType's children order
// and maintain a list of not yet added datasets
$unmappedIndexes = new ArrayCollection($data->getKeys());
$orderedData = new ArrayCollection();
foreach ($form->all() as $childName => $child) {
$orderedData->set($childName, $data->get($childName) ?? $child->getData());
$unmappedIndexes->removeElement($childName);
}
unset($child);
// also add the remaining datasets, so that they are not lost, just in case as there should not be any at all
foreach ($unmappedIndexes as $unmappedIndex) {
$orderedData->set($unmappedIndex, $data->get($unmappedIndex));
}
unset($unmappedIndexes);
unset($unmappedIndex);
$event->setData('array' === $dataType ? $orderedData->toArray() : $orderedData);
},
priority: -100
);
}
/** @inheritdoc */
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['sortable'] = $options['sortable'];
}
/** @inheritdoc */
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults(['sortable' => false])
->setAllowedTypes('sortable', 'bool')
;
}
/** @inheritdoc */
public static function getExtendedTypes(): iterable
{
return [CollectionType::class];
}
} Have in mind, that this only works, when the model data already comes with string indexed data. Adding those indexes within a data transformer comes too late. So when for example the model data is not a DTO, but maybe a Doctrine Entity and the indexes cannot be added within the collection's getter, they must be added earlier, maybe within a PRE_SET_DATA listener. Most of my forms use DTOs, so the indexes can be added inside their constructors, but in one legacy case I ran into that model transformer pitfall. For that reason I would love to see the core |
Use cases (few select ones):
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:
The text was updated successfully, but these errors were encountered: