La gracia de un framework es poderte abstraer de muchos problemas complejos, como por ejemplo dar una manera sencilla de administrar los contenidos. Symfony2 nos provee de una potente herramienta de gestión formularios: su creación, su validación, su populación… y la pieza más importante en relaciones 1 a N: los Form Prototypes.

Cuando realizamos un proyecto, siguiendo un patrón MVC, iniciamos con el modelo de datos (viendo la relación entre los elementos que conforman el todo). Luego nos pondremos a mostrarlos, y finalmente, si es necesario, crear una interfaz de administración. Pero en ocasiones hay ciertas relaciones que no son fácilmente administrables: las relaciones de 1 a N o de N a M elementos.

Symfony2 nos ayuda a través de los FormTypes a crear los distintos elementos que componen los formularios. Con ellos definimos, más allá de las entidades con las que estamos trabajando, cuales vamos a mostrar y que tipos de datos esperamos en los mismos (lo que nos dota de ciertas validaciones, y el tipo de elemento de formulario con que se presentará).

Imaginemos que tenemos una entidad Task, definida en el modelo Doctrine (a través de anotaciones):

// src/AppBundle/Entity/Task.php

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

class Task
{
  // ...
  /** 
   * @ORM\ManyToMany(targetEntity="Tag", cascade={"persist"}) 
   */ 
  protected $tags;

  // ...
  public function __construct()
  {
    $this->tags = new ArrayCollection();
  }
}

Para gestionar los N elementos que tiene un elemento padre, que se suelen almacenar como un ArrayCollection, podríamos ir generando un nuevo formulario por cada uno de los elementos hijos, y luego tener que asociarlos al padre… pero en estos tiempos modernos ya podemos fiarnos de realizar parte del trabajo en frontend y sabemos que es más eficiente si en el mismo formulario del elemento padre tenemos pequeños formularios “embebidos” para los N elementos.  De hecho, muchas veces los elementos hijos son cosas tan sencillas como Tags (que son poco más que una palabra) o una serie de documentos y no merece la pena crear un nuevo formulario. Aunque, eso si, estos se definirán con sus propios FormTypes.

Además, se puede añadir funcionalidad para añadir o eliminar elementos hijos de una manera “automágica”, es tan fácil como indicarlo en el FormType padre.  Lo genial de esta gestión automatizada de Symfony, es que cada elemento nuevo que aparezca en el formulario de vuelta será un elemento añadido a al colección, y cada elemento que desaparezca será eliminado, no hará falta programar nada.

En el ejemplo, vemos que en el FormBuilder de TaskType lo definimos como un CollectionType de elementos de tipo TagType.

// src/AppBundle/Form/Type/TaskType.php

// ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
  // ...
  $builder->add('tags', CollectionType::class, array(
    'entry_type'   => TagType::class,
    'allow_add'    => true,
    'allow_delete' => true,
  ));
}

Para que esta funcionalidad sea completa necesitamos poner un poco de javascript en el frontal que haga que los elementos aparezcan y desaparezcan del HTML. Se explica a fondo en el manual de Symfony, pero por resumir:

  • para añadir (allow_add) crearemos una función en el back que genera el prototype y lo dejamos en el campo data-proyotype de algún elemento padre al listado de elementos hijos, luego desde javascript tomaremos esa cadena de texto y, previa sustitución de una cadena ( __NAME__ por defecto) por la posición en el listado de elementos lo añadiremos al html del formulario, listo para ser enviado.
  • para eliminar (allow_delete) será tan fácil como eliminar del html el elemento, y así cuando el formulario sea enviado, el elemento no existirá.

Personalizar Prototypes en Symfony 2

Los prototipos nos son más que formularios basados en el FormType de la entidad que estamos modelando, por tanto para ser autogenerados funcionan igual que un formulario, pero en vez de pasarle una entidad real le pasamos la entidad prototipo que tiene el elemento padre ( form.tags.vars.prototype ).

En los tutoriales se plantea que hay que hacer personalizaciones a nivel personalizaciones de formulario, pero es más sencillo tratarlo como un formulario más y, por ordenar el código, sugiero que se cree cada prototipo como un fichero que luego se incluya, pasándole el prototipo por parámetro. Este fichero además será reutilizable por los propios elementos del formulario.

Y de la misma manera que podemos, en vez de renderizar todo el formulario, crearnos nuestro propia plantilla usando form_row para cada elemento u optar form_label y form_widget para un diseño más al detalle.

Metíendonos en problemas: Form Prototypes anidados

(VIA: StackOverflow)

En puntuales ocasiones puede que tengamos X elementos padres de entidades A, cada uno con N hijos de entidades B, cada uno con M hijos de entidades C. Si siguieramos la lógica usada hasta el momento nos encontraríamos con un pequeño problema, sustituir la cadena __NAME__ en el prototipo de B, que contiene el algún punto el prototipo hijo (C) sustituiría la cadena también allí, con lo que no la encontraríamos al querer generar el nuevo elemento de C usando su prototipo. Pero para eso el FormType  tiene la opción:

'prototype_name' => '__NAME__'

con ella podemos usar otra cadena a sustituir. Lo normal es que tengamos controlados todos los niveles de anidación y sepamos las cadenas que estamos usando, pero por simplificar recomiendo usar una cadena que incluya el nombre de la entidad. Por seguir el ejemplo: llamar a los primeros __ENTITYB__ y a los segundos __ENTITYC__

Reflexiones finales

Tengo que decir que en un principio estaba reticente a ceder parte de mi funcionalidad al frontal (tengo mis batallitas con javascript en el pasado y aún duelen las heridas) pero una vez que descubrí el potencial que tiene y lo mucho que simplifica la tarea tanto al usuario final como al desarrollador, ya no dudo en usarlo.

Sin duda, leer la documentación a fondo de gestión de formularios de Symfony2 es un mundo de buenas sensaciones, saber sacarle todo el jugo  te permite ganar mucho terreno y centrarte en la lógica de negocio. Cuanto más permitimos que Symfony gestione la creación del formulario, más sencilla será la gestión y validación de los datos que maneje.


Pablo Rodríguez Monedero

Técnico de Tuercas y Tornillos de Torsión Temporal

@yondemon