Colecciones de formularios en Symfony

Este post tiene el código necesario para implementar el tipo colección (collection). Lo que se busca es embeber uno o más formularios dentro de otro formulario.

Tenemos dos entidades relacionadas Factura e Item.
Una Factura contiene Items.
Los Items pertenecen a Facturas.

En el formulario de la Factura:

// ../src/Acme/TestBundle/Form/FacturaType.php
//..
$builder
        //..otros campos
        ->add('items', 'collection', array(
            'type' => new ItemType(),
            'by_reference' => false,
            'allow_add' => true, //Permite agregar nuevas colecciones
            'allow_delete' => true, //Permite eliminar colecciones
            'label' => 'Items',
            'options' => array('label' => false)
        ))
;

En la entidad Factura:

// ../src/Acme/TestBundle/Entity/Factura.php
//..
/**
 * @var Items
 *
 * @ORM\OneToMany(targetEntity="Acme\TestBundle\Entity\Item", mappedBy="factura", cascade={"persist", "remove"})
 */
private $items;

/**
 * Add item
 *
 * @param \Acme\TestBundle\Entity\Item $item
 *
 * @return Factura
 */
public function addItem(\Acme\TestBundle\Entity\Item $item)
{
    $item->setFactura($this);
    $this->items[] = $item;

    return $this;
}

En la entidad Item:

// ../src/Acme/TestBundle/Entity/Item.php
//..
/**
 * @var Factura
 *
 * @ORM\ManyToOne(targetEntity="Acme\TestBundle\Entity\Items", inversedBy="items")
 */
private $factura;

En el template donde se muestra el formulario (new):

<hr>
<div>Items: </div>
<ul class="items form-group" data-prototype="{{ form_widget(form.items.vars.prototype)|e('html_attr') }}">
    <li class="list-group-item">
        {{ form_errors(form.items) }}
        {{ form_widget(form.items) }}
    </li>
</ul>
<hr>

En el template donde se muestra el formulario (new):

(Las clases CSS aplicadas a los tags HTML, puede que no sean de bootstrap y se pueden eliminar)

<script>
 $(document).ready(function () {
     var addLink = $('<a href="#" class="right btn btn-link btn-icon add-icon">Agregar</a>');
     var newLinkLi = $('<li class="add-icon-li"></li>').append(addLink);

     var collectionHolder = $('ul.items');

     collectionHolder.append(newLinkLi);

     collectionHolder.data('index', collectionHolder.find(':input').length);

     addLink.on('click', function (e) {
         e.preventDefault();
         addTagForm(collectionHolder, newLinkLi);
     });

     //Agrego link para borrar
     collectionHolder.find('li').each(function () {
         if (!$(this).hasClass('add-icon-li')) {
             addTagFormDeleteLink($(this));
         }
     });

     function addTagForm(collectionHolder, newLinkLi) {
         var prototype = collectionHolder.data('prototype');
         var index = collectionHolder.data('index');
         var newForm = prototype.replace(/__name__/g, index);
         collectionHolder.data('index', index + 1);
         var newFormLi = $('<li class="list-group-item"></li>').append(newForm);
         newLinkLi.before(newFormLi);
         addTagFormDeleteLink(newFormLi);
     }

     function addTagFormDeleteLink(tagFormLi) {
         var removeFormA = $('<a href="#" class="btn btn-link btn-icon delete-icon">Borrar</a>');
         tagFormLi.append(removeFormA);
         removeFormA.on('click', function (e) {
             e.preventDefault();
             if (collectionHolder.find(':input').length > 2) {
                 tagFormLi.remove();
             }
         });
     }
 });
 </script>