It’s been quite a road to understand the symfony2 CollectionType, and to make it work the way I wanted!
This article will be brief (I’ll try at least) and will not explain a lot because this reflexion started 3 articles ago so all the information on why things are made like this, why is my code like that etc… can be understood through those articles:
Part of the solutions are in the comments too.
Why make a collection?
First things first: When do you want to use the collection field? Any time you need to add an unknown number of fields to a form. This means anytime you need to add an unknown number of related entities to your “core” entity in my case. Good examples are:
- Holes on a golf course (9? or 18?)
- Tags on a blog article or on a product
- Actors on a movie
- Toppings on a pizza
- …
The use for collection field is when you want to set all those things and the “core” entity at the same time. You don’t want your user to first chose a pepperoni pizza, then validate, go to the next screen add toppings. You want him to create a pepperoni pizza with toppings right away.
Here I made a small ProductBundle where I save products that have tags (like ‘shoes’, ‘blue’, ‘winter’, ‘fashionable’, ‘ugly’, …) The complete bundle code is here.
How to make a collection
In the backend
Read the previous article: Something is broken?
In the frontend
If we’re adding this many fields without knowing how many there will be, we need a dynamic frontend. We need to use some javascript to add and remove some fields as we see fit when filling that form.
Remember we used the prototype = true option here because this will allow us to have a prototype version for the tag fields that we can add to our html and then submit. Also remember that until now we did add 3 empty tags in the controller in order to have a non-empty collection. The controller now becomes this as we don’t need to have any tags at first.
Adding fields
In my case, the name for TagType was: khepin_productbundle_producttype_tags (auto generated and I didn’t change it, probably not the best). This means that when I output my form, I have a div with this as an id. And it’s this div that has a ‘data-prototype’ attribute containing the html of the tag fields with a generic ‘$$name$$’ in them that needs to be replaced before inserting into the final html.
We add an ‘Add Tag’ link which when clicked will add a tag field to our html page. In twig new.html.twig:
I am using jQuery for the javascript that will add the fields. The script is pretty simple:
The add function:
- Finds the div holding our fields for the collection
- Gets the data prototype
- Replaces all instances of $$name$$ in the prototype with a number (current number of elements in the collection. Since they start numbering at 0, we always have one more).
- Adds the resulting html to the div holding the collection
The rest of the script only subscribes the link’s click to activate the “add” function, and prevents the link from actually doing anything.
Now you can add an infinite number of tags to your Product and save all of them at the same time!
Thanks for this, I was beating my head trying to find how to add subforms dynamically
BTW, it is better to use delegate instead of live on jquery
$(".record_actions").delegate("a.jslink", "click", function(event){
event.preventDefault();
add();
});
True, though I didn’t know about delegate, I remembered that “live” is not very recommended for some reason.
This is what you mean, the problem is that i see the prototype=true on github and not on your code here or viceversa, very confusing. Please don’t assume too many things about your readers. Also it would be very interesting to see the actual mechanics of how this snippet put into a ‘data-prototype’ field is generated or how it is binded…
<div id="khepin_productbundle_producttype_tags"
data-prototype="
$$name$$
Name
">
I will try to post something on http://www.craftitonline.com but my problem was different.
Indeed, I didn’t realize that.
The default options in the CollectionType class are as follow:
‘allow_add’ => false,
‘allow_delete’ => false,
‘prototype’ => true,
‘type’ => ‘text’,
‘options’ => array(),
So the ‘prototype’ => true is not absolutely needed as it is default. Setting it to false however brings the correct behavior that the data-prototype is not included in the html.
Regarding how it is rendered, you should probably have a look at the Twig files for form widgets and find where the ‘prototype’ option is checked.
http://www.craftitonline.com/2011/08/prototype-attribute-on-symfony2-forms/
Pingback: Prototype attribute on Symfony2 Forms | Craft It Online!
This is great and I’m very appreciative you’ve figured all this out. But now my my real question is how do you customize the widget/row templates for the collection items?
I managed to piece together the info on doing this.
If you want to override a form template just in a single template (one off) then you start by adding this to your template:
{% form_theme form _self %}
Then check the default form blocks in Symfony: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
and override the “collection_widget” block by adding it to your template.
Hi, I have found a problem with editing (although it could be the version of Doctrine I have, or my configuration):
when editing a Product doctrine throws this:
Catchable Fatal Error: Argument 1 passed to …. must be an instance of Doctrine\Common\Collections\ArrayCollection, instance of Doctrine\ORM\PersistentCollection given
to fix the problem I suggest letting Doctrine do the “casting” :
change
public function setTags(\Doctrine\Common\Collections\ArrayCollection $tags){
$this->tags = $tags;
foreach ($tags as $tag){
$tag->setProduct($this);
}
}
to:
public function setTags($tags){
$this->tags = $tags;
foreach ($tags as $tag){
$tag->setProduct($this);
}
}
that will enable the code to work for both new and editing objects (it did in my case
Instead of ArrayCollection or nothing, use \Doctrine\Common\Collections\Collection.
Both ArrayCollection and PersistenCollection extend \Doctrine\Common\Collections\Collection, so it works fine and it’s clearer this way, when a type is defined!
Hey,
this looks really interesting! I need the same functionality but I don’t use 3 text fields for this 3 tags, I want to use just one text field and seperate the tags with comma or semicolon. How does this work?
Well then you can either use some javascript to catch what the user is typing, split the string on each “;” and populate the fields of your form with it (using hidden fields so the user doesn’t see it happening).
Or you can just set a special field and then deal with this in symfony maybe through a factory.
Hey,
okay, but I want a non-javascript solution, or a solution that works with javascript turned off. Maybe split them on the server side and create a Tag entity object for each tag and add them all to the Article entity.
But how does it work if the tag is already in the database? I don’t want to get the tag called “google” three times in my database. Does this work with your solution?
data transformer?
How about a better question? How do you template the prototype?
I think like for all twig widgets. There’s a Twig file defining all the defaults widgets that you can the override / customize to your needs.
Awesome, just what I needed, thanks!
This might be very useful for the Symfony CMF project too, consider joining the mailing list and contributing!
In case anyone else is struggling with the removal of entities in the collection type (the removal of Tags you decouple from an Product in your post): you need to add “orphanRemoval=true” on the OneToMany side like this:
/**
* @ORM\OneToMany(targetEntity="Tag", mappedBy="product", cascade={"all"},orphanRemoval=true )
* @var type
*/
private $tags;
Also note I changed cascade from “persist” to “all” so that all tags are removed when a product is removed. Perhaps in this example you don’t want to remove the associated entities, but I used your tutorial to create my collection field where deletion was required.
It took me a hour to sort this out, so hope I help someone else with the same problem.
Btw. nice post, helped me out a lot!
Hey, great posts and thank you for sharing them. I stumbled across this after pulling my hair out for hours looking for something in Symfony2 like the collection type but had not found it. Works perfectly & still got hair
Yeah I’m thinking of getting a job at “Head’n Shoulders”!!!
not that fast , i am looking to something like double or triple nesting of collections, like Product=>tags=>tagImportance
Useful article thanks. How would you go about adding Tags to the Product for a Tag that already exists. In my case, I need to add an Administrator to a Company by typing in their username/email. However, what happens is that when saving the company, Doctrine attempts to persist the administrator even though it already exists.
It seems that the examples only apply when you want to create a new Tag/Administrator, not select existing ones.
Maybe I need some hook or post-load event on the form whereby I can do a “findByEmail”?
In the symfony form those are different things, something that already exists would be an entity field and something that doesn’t exist would be the full form to create that entity.
Two possible solutions:
1. While saving the form you check if this is an existing tag and perform what is needed to retrieve the entity and save this.
2. You could have 2 collections and in some way have you javascript check what needs to be added. Check the IoFormBundle for example that will give you autocomplete entity fields. You could then check that if what is typed did not match to an entity then you add the field to a different “new entities” collection.
What about deleting some records?
@ricbra – it took me at least an hour or two on the same problem – if only I saw your comment. For when this comment gets index by Google: allow_delete and orphanRemoval=true are the needed keywords!
Any ideas how use this tips to create form with dynamically attachments?
Would You like to create custom article about that?
I definitely won’t have time to write an article about this myself. Unless the case comes up that I actually need to do this. Not sure indeed how this would work for file attachments as I have not used them yet.
Ok, I’ve done this.
I had to change your code a little, but everything looks fine.
Pingback: Formulaires dynamiques avec Symfony2 | KeiruaProd
Hi,
When I do all the tutorial, I get this error:
Integrity constraint violation: 1048 Column ‘noticia_id’ cannot be null
noticia_id is the foreign key, which is null, but only with the widgets added through Javascript.
Why???
Check in the cookbook on symfony’s website. I consolidated these articles in a more consistent manner there.
I checked all the tutorial and I think my code is correct (obviously not). But when it occurs this association?
You method that “sets” the collection on the parent object has to take care of the association happening both sides. This was on the previous article http://sf.khepin.com/2011/08/inconsistent-behavior-of-symfony2s-form-framework/
or
https://gist.github.com/1156036
Very interesting article. Please, tell me if i’m wrong but this isn’t the best solution: relation between Products and Tags is ManyToMany.
What happens if 30 products have tag “blue”? You end up with 30 “blue” rows in your tags db table, each one associated with a single product which is a significant waste.
Could it be possible to implement the same thing on a ManyToMany relation?
Yeah, with this implementation you would end up with 30 tags called “blue”. It is definitely possible to make this happen on a many to many relation to avoid this, just requires more work and to chose a way to implemented from the user’s perspective. You could use auto complete tags, or let the user write anything and then group things on the server side, but then you have to take care of “Blue” vs “blue” or many other such cases etc…
I’ve found an error that happens only when adding new tags (both in add page and in edit page).
Trying to debug a little, i’ve found that new tags are passed to setTags method of Khepin\ProductBundle\Entity\Product as array, not as Tag objects.. so calling $tag->setProduct($this) causes the error “Call to a member function setProduct() on a non-object”.
In this article: http://sf.khepin.com/2011/08/inconsistent-behavior-of-symfony2s-form-framework/ I was explaining that you need to add a setTags method. However this will change in 2.1 I think. Then the forms will always call “addTag”
Thank you!! I’ve been trying for a while to figure out how this works, and it works fantastically. Never needed collections until today, and this pushed me in the right direction.
Now to figure out how to make collections of a collection work correctly.
Wow! I’d be much more worried about how to make something like that usable and understandable for the user than to have it actually work
It’s not as silly as it sounds, but it needs to be all on one page.. so it’s a bit of a pain to make it look good (thankfully we have a designer).
I’ve got it working alright, here’s what I needed to do:
1) Template
2) Template has many groups
3) Groups have many tasks
So you click “create a new template”, and you can click “create a new group” and then tasks under each group. Luckily I’m doing this under symfony2 where I can read official documentation + various blogs that turn up via Google searches when I get stuck.
did you figure out the templating and double nesting? that is exactly what i am after now
it would be great if Sebastian works on an example like the docu
http://symfony.com/doc/master/reference/forms/types/collection.html
http://symfony.com/doc/master/cookbook/form/form_collections.html
but with more than one level, that is Posts->tags->ranks or like you said
templates->groups->users
any pointers would suite me great, I am desperate!
Hello khepin and thanks for your excellent tutorial. I would like to ask what the difference is between addTags and setTags methods in Product.php entity. When generating setters/getters for entities and there is a onetomany relation, the addTags methods seems to be the one created by default. But is it really used, or does it all depend on the setTags methods?? Thanks in advance
If you generate the getters and setters automatically, the framework creates a method called “addTags” that only adds one tag to the collection.
However the form framework doesn’t use that method as it always calls methods in the form of “setXxx”.
I remember @webmozart saying that he changed this behavior in the last versions of the form framework though so it might be fine to go with “addTags” now. Haven’t tried.
Hi,
Im really pleased to read this article. I use this one to do a form to update photos.
But now, i want to take off the link “Add a photo” and force 9 photos for exemple. How can i do that, i have to write my 9 inputs ? I don’t know how to do that.
Thanks for your answer.
Sylvain.
Check out one of the previous articles of this series: http://sf.khepin.com/2011/08/basic-usage-of-the-symfony2-collectiontype-form-field/
This is what was done too…
Thx a lot mate !