-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
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
[Form] Collection indices changed by adders are not reassigned #7468
Comments
I had the same issue, I think the problem is when you set by_reference to false the indexes get lost when PropertyAccess/PropertyPath calls addTag and the errors are attached to the previous indexes. To solve this I used the setTags method instead addTag. |
Same issue here! |
Nevermind figured out how to use setTag but here's another issue. If the collection form has an element form completely blank setTags will get an array with null values and you will not be able to persist until you remove the nulls. |
In my case, I use setTags and getTags only. It is not mandatory, PropertyAccess looks for adder and remover, if it doesn't find them, uses the setter. The other issue you mention I think it is the same for any field if you don't have validators in the fields. |
The only viable solution I can think of right now is to require public function addTag($tag, $index = null)
{
if (null !== $index) {
$this->tags[$index] = $tag;
} else {
$this->tags[] = $tag;
}
} Do you think that is an acceptable solution? The problem is that currently the data of the collection field is modified after the $form->submit():
In step 3, the collection indices are changed through the adders, but not reassigned to the collection field, whose scope only ranges from 2.1-2.3. |
Wondering when this will be fix. This make impossible to use the collection field according to the docs. Make the add and remove methods useless since in most of the cases you will want to add and remove elements and the index will not be consecutive. The solution proposed by bschussek migh work very well. |
Same issue here |
I too have seen this problem many times. Without a little tweak Symfony's embedded collections are pretty much useless. Too bad I've seen this problem on many live sites using Symfony (lack of proper testing??)... To "fix" this issue, I just reindex the request collection data array before validating the data. I usually add a helper method similar to this to my base controller:
Then do the reindexing before the handleRequest call, like in this service handler example:
This seems to do the job for me! |
I just thought about this again. I'm not sure whether adding an Another problem is that of race conditions. What if the underlying collection is changed between loading and submitting the form? Then the wrong elements of the collection will be changed. Thus, it would be better to remove the coupling between the indexes of the collection and of the collection field and index by identifying values instead: $builder->add('tags', 'collection', array(
'index_by' => 'id',
)); Then we can deterministically update the right objects on the server, map violation paths and distinguish existing from new elements. |
See also #7828 which is proposing the same feature. |
I second this solution - I stumbled upon this issue in multiple scenarios and came to this same conclusion - field/index decoupling. |
Stupid question... how do you add new items (implementation based on the symfony cookbook form collection example referenced above) when you have set 'index_by' => 'id' to your collection? What would be the next index? Or can you add new items somehow without the index? How would this help if you are adding new collection rows and then you remove a row and then again add new ones? Consider the following scenario:
Now rows 1 and 3 still have their original indexes 0 and 2. Based on the cookbook example js, row 4 would get index 2 and thus it will overwrite the actual row 3 with the same index. And the validation errors won't be shown. The validator assigns errors to row with index 1 but there's still no row with index 1. There's now original row 1 with index 0 and new row 4 with index 2. So at the moment to deal with adding items I'm reindexing the rows before validation, so there won't be any missing indexes. Now in many cases doing the reindexing is not very safe when updating... at least if your collection items have associations to other entities. If I understand this correctly, your suggestion to using indexBy would help with updates. But how on earth can you add new items in that case? What am I missing? :) |
IMO the index should stay like it's now. What's purposed as an index_by should instead be key_columns... which should automatically be written as a hidden fields inside a collection |
Nobody ever said that the cookbook example JS was a perfect JS code for handling collection (IIRC, authors of this snippet even recognized it had some weak parts when submitting it to the doc) |
Stof, you're right... nobody made that claim and we can see that it's not perfect. However the original question still remains open. How do people handle these kinds of forms easily and reliably? Does anyone have a good solution that could be added to the cookbook example? |
I bypass the issue on the front-end side. In the example, in function
I use
This way the indexes remain in the DOM, but the values are disabled and effectively detached from the main object on form submit. |
Yay! Just waited 2+ hours on this. |
We often see issues around this. We resolved the index collision problem in dynamic forms (which can occur if you follow the cookbook recipe), by using randomly generated indexes in JS. So to prevent data corruption for occasional cases we end up breaking collection validation in every case. However, vague error messages seemed like a better compromise to us than broken data. We've also had race condition issues mentioned by @webmozart. In the end we resolved that by using We'd love to see a resolution to this, but I realise it's very difficult. |
+1 for this issue! Spend 3 hours. |
I have come round this issue on the client side by modifying the Javascript/Jquery code given in the Symfony Documentation. Instead of numbering the new elements by counting the sub-elements, I am looking at the last element's id and extracting its index with a regular expression. When adding an element, I am incrementing the last index by 1. That way, I never use the same index. Here is my code :
|
@Zoopme you could use twig filters "keys -> last". |
I wasn't aware of the keys filter until now, but I ended up using something like the code below instead:
I have a javascript that binds dynamic form handling to each dynamicForm element. In the javascript I use and modify the next-index data parameter. The next-index is increased on every addition and never decreased. I've managed to get this working automatically for any number of nested forms. |
So, to summarize this issue: if you have a collection on a form, and if you want to use |
@Tjeerd Sadly I don't think we have the choice for now since the mapping is handled by the |
Solved this by extending my forms with this: use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
/**
* Class AbstractEntityType
* @package BvStandard\ServiceBundle\Form
*/
abstract class AbstractEntityType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmit'));
}
/**
* @param FormEvent $event
*/
public function onPreSubmit(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
$object = $event->getForm()->getData();
// Loop on form fields
foreach ($event->getForm()->all() as $field) {
/** @var FormInterface $field */
$fieldName = $field->getName();
if ($field->getConfig()->getType()->getName() == 'collection') {
if (isset($data[$fieldName]) && $object) {
$collection = $data[$fieldName];
$data[$fieldName] = array();
$k = 0;
// Reorder submitted data
foreach ($object->{'get'.ucfirst($fieldName)}() as $k => $item) {
// Search
foreach ($collection as $i => $c) {
$cId = is_array($c) && isset($c['id']) ? $c['id'] : $c;
if ($item->getId() == $cId) {
$data[$fieldName][$k] = $c;
unset($collection[$i]);
break;
}
}
}
// Added items
foreach ($collection as $c) {
$data[$fieldName][++$k] = $c;
}
}
}
}
$event->setData($data);
}
} Of course, you have to add |
Thanks! |
@Seb33300 Thank you for the solution. Does this approach also work for nested collections? |
If by nested collections, you mean something like: ->add('fieldName', CollectionType::class, array(
'entry_type' => OtherFormType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
)) With |
Yes, that's what I meant. Thanks! |
@Seb33300 @placid2000 Though you should rather think about moving the code into a form type extension instead of having to extend some base class. |
I have the exact same issue with Symfony version 2.3.13-DEV. @Tjeerd what is involved if I do not use |
@Seb33300 thanks for your solution. But this does actually remove an item and create a new one when updating, so its changing the entity ID. Something to be aware of. |
@kimwue If you want to update your entity without removing it (without changing the id), your form have to submit the id of each entities you are updating. If no id is submitted, a new entity is created. |
Confirmed @artorozenga |
I can't use the |
@Glideh as far as I know you need to write your own validator (or logic on Controller) to check if there are non-unique items. UniqueEntity checks only against DB. |
I see, I'll do that, so it is the expected behavior. |
@Seb33300 Thanks for this bit of code. I made one very minor modification to handle zero based indexing: I changed $data[$fieldName][++$k] = $c; to $data[$fieldName][$k++] = $c; I then followed the doc http://symfony.com/doc/current/form/create_form_type_extension.html I extended the form type FormType, so all forms now automatically use this extension. Nice solution! |
@dsands Doing You should set |
@Seb33300 It seemed to be working, but looking at the code again I see what you mean. Thanks again! |
…oc'ed it quite a bit - it fixes (in our project) symfony/symfony#7468. Thanks to Jürgen Modic for the tip on this!
In case it helps, we ran into this issue on our KnpUniversity tutorial about form collections. The fix for our situation was actually quite simple (inspired by the solution by @Seb33300, but simpler because it just fixes our use-case): #7468 (comment) The solution is here: knpuniversity/symfony@ab02c47#diff-458cc0b549cdd91692b368dd8a0885bc along with a detailed explanation. In our JavaScript (http://knpuniversity.com/screencast/collections/add-new-collection-prototype#the-prototype-javascript) we always increment on new items... so if you delete an item before it's submitted, you have a hole (e.g. 1, 2, 4). Re-indexing the submitted data to be 1, 2, 3 seems to fix things just fine. I hope that helps someone out :) |
@weaverryan You need to be careful with such a solution as you may run into cases where this means that no new object will be created, but the object to be removed from the collection is instead updated with the data of the newly added object. |
@weaverryan Your solution is not working if you use the UniqueEntity constraint, I think this is due to the fact that the collection index is rebuild. |
There are many issues related to this topic. I am closing here so we can keep #7828 as the main feature request. |
I am coding a project along the lines of the cookbook entry on embedded collections as stated on: http://symfony.com/doc/current/cookbook/form/form_collections.html.
I am considering the following use case:
Now, if I would leave all these tags blank (with a notblank validation on the tag name attribute), the returned form would be expected to visualize the error statements on instances 0 and 2. However, it turns out that the feedback is only displayed for instance 0.
If I change the returned form with firebug and set the instances on the name input field of instance 2 to '1', the feedback is displayed on the correct instance. This behavior indicates a mismatch between the indexing of the embedded form instances and the validators.
The text was updated successfully, but these errors were encountered: