Basic usage of the symfony2 CollectionType form field

UPDATE: This article was written while trying the form system and represents my thoughts at a certain stage, in order to discuss and get feedback. Not all info here is valuable and some is actually mis-leading. If you read this, make sure you read the comments as well, or wait for a coming article with more correct foundations.

Reading these articles in a series (starting with this one) will get you through most of the thought process, and help understand the reasons behind the good practices. Be sure to also read the comments discussion.

In my previous post, it seemed to me and to a few commenters that I needed to make use of the CollectionType inside my Symfony2 form. However the documentation on that specific field is non-existent to the day. This post will show how I used it, which might not be the perfect way, I am not sure. If some people “in the know” read this and think it’s correct, I’d happily write a bit of documentation for everyone to see.

Quick reminder on what I was trying to do

It would be an app related to golfing. Therefore you need to enter golf courses and the standard score (par) for each hole. Not all golf courses are made equal and some will have 9 holes where some other have 18. So the user will first enter basic details of the course including the number of holes and then I need to generate the appropriate form with 9 or 18 pars to be set. Adding all those fields seems the perfect place to use a collection type. Especially as I want them to be part of a “sub”-form in order to make the form rendering more easy.

The form type

At first I was thinking of completely building my form within the controller since there’s not gonna be many re-use of it. However in the end it doesn’t really make more sense to me over implementing a new form type. I called it ParSetType. And the code is as follows:

Update: as asked in the comments, here’s a link to the HoleType code so: the “number” is set as hidden because we set it for the user. The par is the real editable field. The Course is not set in the form because it was already added to the Hole class in ParsetType.

The constructor always takes a Course as an argument because we can’t set the par’s of … nothing and we can’t create the pars before knowing if we need to set 9 or 18 of them.

Now in the buildForm function, I set “holes” as a collection type field, then iterate over the number of holes that this course has and add a specific field HoleType (related to the Hole entity). Here I have a “problem” that I had to manually generate the sub-form’s field names as hole_1, hole_2, … I would rather provide a name format like hole[%s] or anything like this and have the form generate the names itself as I keep adding fields. (This might actually already be possible but I haven’t found how to set it).

Inside the for loop, the first 3 lines are just for me to set some data first. Because I already know some info about the hole that the user should not have to setup himself. The last line is the one that actually populates the collection. So it’s only an ->add(XXX) method to be applied on the sub-form.

Template

Having this set like that allows my template to be very clear easy and straight forward to render:

There’s only one thing interesting here that having set all my hole_$i sub-forms as part of the holes collection, I can render that part precisely as I want and iterate only on the fields included in that collection with {% for hole_form in form.holes %}.

Controller

The controller to create the empty form is pretty straightforward:

But getting the results and saving to database is a bit more tricky. First a look at the code:

The basic steps are:

  1. Retrieve the course from the database based on the hidden form value we had. This has to be done before binding the data to the form because the form needs a Course in its constructor (so here I think the fault is on me and my design).
  2. Create a form and bind it to the data. When the form is created, the course object is already attached to each HoleType.
  3. Get each hole, link it to the entity manager and flush. Everything went ok, send the user to see what he just added.
  4. If it didn’t go ok, show him the form again with errors etc…

Now you’ve noticed there’s one more little step: removing the csrf token from the Collection field. This field does not appear in the HTML source when we iterate over each form.holes in the Twig template. However it is set in the Collection object as one of the subfields. I am not sure what the reason for that is, or what can be done about this, but for now it really doesn’t make sense to me. Especially as it is not used while outputting the form, it does not add any security at all. The one that is output in the HTML of the form is from the global ParSetType form.

Conclusion

That’s how I managed to make use of the CollectionType. I don’t really find it easy because of:

  • The names that I have to set for each sub-form by myself.
  • The CSRF token that is uselessly messing the save process as it’s considered a part of the collection as well. That CSRF is not transparent in my process…

33 thoughts on “Basic usage of the symfony2 CollectionType form field

  1. I understood the for loop for adding to the collection, but I don’t understand what ‘type’ => new HoleType() did… You didn’t even include anything in the constructor when you initially set that. I’m confused by that. :)

    Would love to subscribe to your whole blog via email, but for now I’ll settle on comments.

    • Ooohhhh China doesn’t want me to comment and reply to you this morning! I’m in a fight with the Great FireWall!

      Anyway. You are very right not to understand that ‘type’ => new HoleType() so I updated my gist to remove it.

      The collection type can take many options, and I tried giving those different values while trying to make it work.
      It takes the following:
      - ‘type’
      - ‘prototype’
      - ‘allow_add’
      - ‘allow_remove’

      So far I have been unable to actually understand how those work! So my use of ‘type’ here was just a remain of those experiments.

      Thanks for the hint

  2. Hi,

    Say you didn’t know how many holes would be on the course and the user could dynamically create new holes, or have none, in the form (0 – 18). It’s easy enough to manage that in the template, but my question is, how does your form handle the situations where user can give no holes on the course. I realize this doesn’t work in the context of a golf course, but its a real situation data objects that can have empty collections.

    • I was thinking about this one too. I think what you could probably do is add all 18 (or 9, depending on what the course allows for) holes in the collection, and then use javascript or jquery to simple remove them from the DOM until someone clicks an “add hole” link. When you ran getData on each of your collection it’d only pull back the ones that were actually added back to the DOM.

      This may be more of workaround than anything though. I’d like to hear from Khepin or others if there’s a better way to go about this.

      • Probably kind of something like that, though the best is to be able to add not only 18 or 9 but n forms.

        Have not tried yet!

    • This is what I think for the case where you want to add n number of “things” linked to the original. I have NOT solved NOR tried to solve this issue so far as it has not showed up in my use cases.
      In symfony 1.4 there used to be a great plugin for that where all was magical and transparent. In symfony2 the current documentation says that somewhere along the lines we’ll see how to deal with dynamically adding sub-forms through ajax, but there is nothing right now.

      The case exists where you truly have no idea how many there will be. One use case I had a while ago (but while using sf1.4). Is for payments of a contract. The payments could be made some times in one shot, some times every 3 months, some times every month and with unknown contract length. Also some contracts were just mocks in the system and had no payments. So it virtually could go anywhere from 0 to infinite number of payments linked to a contract.

      My opinion is that this is usually better done through ajax: add one payment form at a time, fill all of them and send.

      In sf2 right now I have no idea how you deal with this. My guess is it’s linked with the “type” and “prototype” options of the CollectionType, that would basically say to the collection “you’ll receive a bunch of forms that look like this”.

      I’ll try if I get some time.

      • Sorry for not following up my comment on your last post, I though I’d get replies via email but seems I forgot to check the box below haha

        Anyway, I’ve been using CollectionType for one project and manage to make it work for me. As I understand, the options work like this:
        - ‘type’ : Well, you indicate the type (Form) that the collection will contain, so it knows what to expect. In this case, new HoleType() was ok. You can also use a string that represent the predefined types (‘number’, etc).
        - ‘prototype’ : Indicates wether to output a “prototype” field in case you allow to dynamically add items to the collection (ie: add via javascript the html for a new Hole). This will usually output the html with variables (such as $$name$$, that you can replace accordingly) for the new field inside a tag, by default. You can customize that as you would customize any other widget.
        - ‘allow_add’ : Well, if you want to handle new items in the collection.
        - ‘allow_remove’ : If you want to handle removal of items in the collection.

        CollectionType handles extra items or removal of items in a pretty transparent way. The prototype field is useful to generate the html for extra items, as it’s like an html template for each new item.

        So that’s pretty much it for what I understand. No official documentation about it, so can’t be 100% sure, but it’s working for me :)

        • hi Diego, come down to Lima man, to work or let’s talk sometime. You got the best answer. Now i am trying to solve a different problem. It has some ajax but I don’t think that is related to collections. But I am trying to load rather the options for different selections into the same select element. Do you have a blog?

          • Hey, in fact I’m in Guayaquil (Ecuador), not that far haha anyway, I don’t think I got your point. If you could explain it a little more in detail, maybe I could be of some help.
            I do have a blog, although it’s not related to webdev. Hopefully I’ll start one soon.
            Follow me on twitter @dluces so we can talk about the problem you’re trying to solve.
            Se vale en español ;)

  3. If you combine both ‘prototype’ and ‘allow_add’, the form generate a data-prototype parameter in the wrapper div of the target “widget” (if you just set allow_add, nothing happens).
    It’s fully handleable with javascript but I don’t understand why it’s made like that in sf2. What if the client has javascript disabled ?
    If someone has the courage to dig into the code to see how forms are rendered …

  4. for the generation of [%s] use a factory class to generate the embedded form names and post the code on github please, I forked your repo but i have not seen more updates since then.

  5. this does not sound right, I would rather persist $course and because of the cascading annotation doctrine would take care of these:

    // Attach each hole to the entity manager
    foreach($form->get('holes') as $hole_form) {
    $hole = $hole_form->getData();
    $em->persist($hole);
    }

    I think there is a way to simplify the code. Your background on sf1.x is doing you great damage in the practical sense.

      • I think getData is a useful method though, basically Form Types are our famous Form Models, we just have to get used to it on how to extend those to our usage.

    • Ok, yes so in defining your entity, in the annotations, you can decide to persist in cascade or not. As right now my entities are not cascaded by Doctrine automatically, I need this little loop.

  6. Pingback: Form composition in symfony2 | Synofony

  7. Pingback: Finally Through: symfony2 forms and CollectionType, make it dynamic! | Synofony

    • Yeah, on the golf app, I have no real need for a dynamic behavior, but since I had started working on this thing, I wanted to push all the way to dynamic things.
      Wanted a clean independent bundle to try that that would be as simple as possible.

  8. Hi,

    I have this entity Tracker that has multiple Params. Now I managed to have an embedded form with the relevant Tracker proporties showing up on the form. Moreover, I managed to have multiple Params embedded in the parent Tracker form.

    The question now, is to have a textbox for one param and a dropdown for another, based on some passed parameters. Does anyone have a clue on how to accomplish this? There is no Ajax going on, just plain and simple.

    Current code in Tracker type is add(‘params’, ‘collection’, array(‘type’ => new ParamType())

    Thanks!

    • My guess is that you should customize ParamType::buildForm method. Depending on the initial data ($options['data']) passed, build the form accordingly.

      Eg:

      if($options['data']['whateverdatayouwanttocheck']=='something') {
      $builder->add('param','text_or_whatever_type');
      } else {
      $builder->add('param','choice_or_whatever_type');
      }

  9. Hi Diego,

    Seems logical and is feasible. Saillant detail was that I am using the FormFactory:

    $this->getFormFactory()->createBuilder(
    new TrackerType(),
    $tracker,
    array(‘data_class’ => ‘Gear\SmmBundle\Entity\Tracker’
    ))
    ->add(‘params’, ‘collection’, array(
    ‘type’ => new ParamType(array(‘choices’ => array(’1′ => ‘test1′, ’2′ => ‘test2′)))
    ));

    • Well the FormFactory shouldn’t be a problem really. I see you’re adding the params collection to the builder returned by the FormFactory, but you’re not passing any initial data to the collection. So basically, it will be an empty collection and so won’t show any params initially.
      Now, how would you determine for what params a text field should be used and for what params the choices field?
      If you only want a couple random params with textboxes and the others with dropdowns, then you may only edit ParamType::buildForm to randomly add text fields or the choices, without checking for initial data, but only randomly.
      Sorry if I got lost here.

  10. Well we could also conclude by stating that symfony forms are not so easy to leverage if you want to do special things like you but it is for sure powerful once you masterise it!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>