Finally Through: symfony2 forms and CollectionType, make it dynamic!

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:

  1. Form composition in symfony2
  2. First shot at the collection field
  3. Something is broken?

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!

48 thoughts on “Finally Through: symfony2 forms and CollectionType, make it dynamic!

  1. 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.

  2. 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.

  3. Pingback: Prototype attribute on Symfony2 Forms | Craft It Online!

  4. 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?

  5. 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!

  6. 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?

  7. 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?

    • 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.

  8. Awesome, just what I needed, thanks! :) This might be very useful for the Symfony CMF project too, consider joining the mailing list and contributing!

  9. 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!

  10. 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 :)

  11. 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.

  12. @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!

  13. 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.

  14. Pingback: Formulaires dynamiques avec Symfony2 | KeiruaProd

  15. 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???

  16. 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…

  17. 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”.

  18. 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.

  19. 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.

  20. 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.

  21. Hi!

    Thanks for writing these! Always nice when people who have spent lots of effort understanding these kinds of things take time to put up a ladder behind them.

    I’m working on a project where one of the challenges is to categorize ideas (more things than just ideas will be categorized using the same type of category-system actually, but I’m digressing). The thing is: The user can select several categories, not just one, and thus check-boxes will be used. Also, a category can have subcategories.

    When a user chooses a category for his or her idea, and selects the checkbox representing this category, then a new collection of check-boxes will pop up representing the subcategories of this category (if any). This is illustrated here: http://postimg.org/image/piakoiwsl/. There are complications beyond this as well, but I think I have gotten across the jist of what we want to do.

    I have read the symfony book, and things that have to do with this in the cookbook, but being kind of noob at present (and not as of yet having experience with sending requests for data to the server using javascript) I still don’t feel that I have quite the idea of how I should go about this.

    I understand that I should make use of the CollectionType class, but how would you go about making this work? I’m especially wondering:

    * How can I make it so that extra collections of check-buttons that are added with help from ‘data-prototype’ has the data representing the subcategories for the given category? (Is it possible to do a call to the database to fill this new form?)
    * Is there a standard javascript or jquery snippet that I paste to make the CollectionType-class work? And if so, does this help me do calls to the database/server, or do I have to make this work separately?
    * Is it possible to have the selection of a checkbox be the event that triggers the adding of a new form (and the de-selection of this check-box be an event that triggers the removal of the form that previously was added)?

    I know that I’m asking a lot, but if any of you could answer this, or point me in the right direction, I would be very grateful.

    Also, if you Sébastien, or anyone else, have a general idea about how this could be done, but would have a hard time explaining it in a short comment, I would be very open for discussing this over Skype or google hangout (in addition to helping out with a project that could benefit humanity, and getting my indefinite gratification, I could pay a decent amount of money per time spent talking).

    Again, thanks for your posts :)

  22. I wanna display only some fields of collection while adding. how can be this done with this method.

Comments are closed.