In my previous post on interactive generators I explained how simple it is to create doctrine entities and persist them through the Symfony generated CRUD controllers. These interactive generators create the controllers and forms required for creating, writing, updating and deleting the entities they are tied to. The Symofny 2 documentation is very good at explaining how forms are used in a variety of ways. In this post I am going to show you how to use embedded forms for collections.

Collections will be common to your entities; you will encounter collections  when ever you have a one-to-many or many-to-many relationship. For example,

namespace SS\Bundle\DemoBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
 
/**
 * SS\Bundle\DemoBundle\Entity\Person
 *
 * @ORM\Table(name="person")
 * @ORM\Entity()
 */
class Person
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
 
   /**
    * @var string $name
    * @ORM\Column(name="name", type:"string") 
    */
   private $name;
 
 
   /**
    * @ORM\OneToMany(targetEntity="Animal", mappedBy="owner",cascade={"persist", "remove"})
    */
    private $animals;
 
   ...
}

Inside your Form you must specify the type of the field and notify the form that it is a collection of this particular type.

namespace SS\Bundle\DemoBundle\Form;
 
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
 
class PersonType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('name')
            ->add('animals', 'collection', array(
                    'type' => new AnimalType(),
                    'allow_add' => true,
                    'allow_delete' => true,
                    'prototype' => true,
                    'by_reference' => false,
                    ))
        ;
    }
 
     public function getDefaultOptions(array $options){
        return array('data_class' => 'SS\Bundle\DemoBundle\Entity\Person');
    }
 
    public function getName()
    {
        return 'ss_bundle_demobundle_persontype';
    }
}

The next part had me stumped for a couple of hours, the issue was that I was frantically looking for somewhere within the Form Framework itself to magically add and remove new forms as I needed them. I guess I had been spoiled by other bundles that had taken care of this I had never considered how it was being done. The answer is so obvious that it is unreal, and I was forgetting what the framework gave me. The form and all of the previous setup provided the backend validation and the persisting of the objects, to add new “Animal” items to the form I had to use javascript. Its always the simple things that get me and take the most time! First I created my form in my twig template,

<form action="{{ path('person_create') }}" method="post" {{ form_enctype(form) }}>
     {{form_label(form.name)}}
     {{form_widget(form.name)}}
 
     {% macro prototype(animal) %}
         <tr>
                 <td>{{form_widget(animal.name)}}</td>
                 <td>{{form_widget(animal.age)}} </td>
                 <td><ul><li><a href="#">Add</a></li><li><a href="#">Remove</a></li></ul>
         </tr>
     {% endmacro %}
 
     <table>
         <caption></caption>
         <thead><tr>Animals</tr></thead>
         <tbody>
             {% for animal in form.animals %}
                {{_self.prototype(answer)}}
             {% endfor %}
         </tbody>
     </table>
 
    {{ form_rest(form) }}
    <p><button type="submit">Create</button></p>
</form>

Lets break this down a little, the first section we render the persons name along with the label associated with it. By default this label will just have the column heading, in this case Name.

    {{form_label(form.name)}}
    {{form_widget(form.name)}}

The next snippet of code uses a twig macro. Macros act like twig functions you can call them later and render the same block using _self and the name of the macro i.e _self.prototype(animal) #}. This reduces code duplication, after all why reinvent the wheel.

    {% macro prototype(animal) %}
        <tr>
                <td>{{form_widget(animal.name)}}</td>
                <td>{{form_widget(animal.age)}} </td>
                <td><ul><li><a href="#">Add</a></li><li><a href="#">Remove</a></li></ul>
        </tr>
    {% endmacro %}

We then render the table with each animal associated with this particular person. Notice here we are using our prototype macro to render each animal.

    <table>
        <caption></caption>
        <thead><tr>Animals</tr></thead>
        <tbody>
            {% for animal in form.animals %}
             {{_self.prototype(answer)}}
            {% endfor %}
        </tbody>
    </table>

Finally we render the rest of the elements of the form this covers hidden fields such as CSRF fields and other form fields you ahem not rendered and the forms submit button.

   {{ form_rest(form) }}
    <p><button type="submit">Create</button></p>

The final section of code required is the javascript that will append new animal forms as you create a new animal that is to be associated with the person.

function add(){
    var index = $('table tbody tr').length;
    var row = $('script[type="text/html"]').text().replace(/\$\$name\$\$/g, index);
 
    $('table tbody').append(row);   
}
 
if ($('table tbody tr').length === 0) {
    add();
}
 
$('table tbody :checkbox').live('click', function(event) {
    $('table tbody :checkbox').attr('checked', false);
    $(this).attr('checked', true);
});
 
$('table tbody a').live('click', function(event) {
  event.preventDefault();
    if ($(this).text() === "Add") {
        add();
    }
 
    if ($(this).text() === "Remove") {
        $(this).closest('tr').remove();
    }
 
    //If the user has just removed the last animal from the list add a new empty form in.
    if ($('table tbody tr').length === 0) {
        add();
    }
});

Most of the previous code is self explanitory however credit has to be given to the AcmePizzaDemoBundle. I have augmented the javascript to include,

   //If the user has just removed the last animal 
   //from the list add a new empty form in.
   if ($('table tbody tr').length === 0){
      add();
   }

This prevents the last embedded form from being removed. Alternatively you could have an add button that is independent from your rows, the choice is really yours. Thats all for now hope this helps someone.

Here is another very interesting and related article that highlights how to handles the submission of collections looking specifically at the AcmePizzaBundle.

I am not convinced yet this is he best approach but I haven’t thought of or seen any other way yet.

Check out some of my other Symfony 2.0 posts,