Symfony2: Embeber formularios con relaciones

Hoy toca un post bastante largo, gran parte va a ser código y la parte de texto será para explicar cada decisión que tomamos. En esta ocasión vamos a ver cómo embeber formularios con diferentes relaciones que podemos tener y cómo configurar las entities para que se persistan correctamente.

Antes de meternos con los ejemplos vamos a ver la teoría que debemos saber sobre formularios, Doctrine, validación y cómo se comportan.

Formularios

El segundo parámetro cuando se añade un campo a un formulario es un Type

¿Qué significa esto? Es una tontería, pero hay que tenerlo claro, cuando hacemos esto:

text y date son Types que vienen por defecto en el componente Form y están registrados de forma que podemos obtenerlos por el nombre, es decir, es lo mismo que si hiciéramos:

De esta forma embeber un formulario dentro de otro es inmediato:

El parámetro by_reference

En las opciones disponibles cuando añadimos un campo tenemos el parámetro by_reference que como bien explica la documentación, indica si ese campo se modifica por referencia o no, por defecto se modifica por referencia (true). Con el ejemplo de la documentación se verá mejor que significa esto:

En el ejemplo vemos que se está creando un formulario con un campo title y el otro campo es un formulario embebido que tiene dos campos, name y email. Según configuremos el parámetro by_reference se comportará de una forma u otra.

Si el valor es true, que es el valor por defecto, cuando se envíe el formulario (se ejecute el método bind) se ejecutará lo siguiente:

Los valores del autor se modifican por referencia y no hay una llamada explícita a setAuthor. En cambio si el valor by_reference es false cuando se envíe el formulario se ejecutará algo así:

Así se fuerza a llamar al setter.

CollectionType

Ya hemos visto cómo se embebe un formulario dentro de otro, con CollectionType podemos embeber una colección de formularios:

Este ejemplo mostraría varios inputs de tipo email.

CollectionType tiene varios parámetros específicos:

  • type: Es el tipo de formulario de cada elemento de la colección. Puede ser como en el ejemplo ‘email‘ o ‘date‘, ‘text‘, etc. y también puede ser una instancia de un Type como por ejemplo new AddressType().
  • allow_add: Este parámetro por defecto es false y cuando está a true lo que permite es que cuando se le llama al método bind si hay más items que al principio los añade a la colección.
  • allow_delete: Por defecto es false y cuando está a true permite que eliminar elementos de una colección.
  • prototype: Por defecto está a true y lo que hace este parámetro es que cuando se renderiza la colección en la vista añade un atributo data-prototype al contenedor de la colección cuyo valor es una plantilla que nos permitirá añadir items usando Javascript más fácilmente. En la documentación hay un ejemplo.
  • prototype_name (a partir de la 2.1): Cuando se usa el parámetro prototype que acabamos de ver e insertamos un elemento mediante Javascript debemos sustituir el string __name__ (por defecto) de la plantilla por un número único para cada item. Este parámetro permite definir la cadena a sustituir.
  • by_reference: Lo hemos visto arriba y aunque este parámetro no es específico de una colección, hay que destacar que cuando se está embebiendo una colección si existen los método addX y removeX en el objeto que se está manipulando, se llamará a esos métodos en lugar de setX.

Validación

Valid constraint

Este constraint nos permitirá validar los objetos embebidos.

En este caso $address es una instancia de otra Entity y a la hora de validar un Author también se tendrá en cuenta las validaciones de $address.

Doctrine

Lo vimos en el post anterior.

Embeber formularios con relaciones

Ya tenemos todo lo necesario, en los siguientes ejemplos veremos cada una de las relaciones, mostraremos el mínimo código posible para que no hacerlo aún más largo.

OneToOne

Dado el siguiente diagrama:

 Symfony2: Embeber formularios con relaciones

Tenemos una clase User con un campo username y que tiene una relación OneToOne con Profile:

La clase Profile tiene un atributo name y la relación con User:

Y finalmente la clase UserProfileType que tiene embebido un formulario ProfileType (sólo contiene un campo name):

En este caso no hemos cambiado el parámetro by_reference a false porque cuando creamos el usuario, creamos también un perfil y llamamos a este método explícitamente:

Por otra parte si se usa by_reference a false el objeto se clona y Doctrine lo interpreta como que ha cambiado.

OneToMany

En esta ocasión tenemos el siguiente diagrama:

 Symfony2: Embeber formularios con relaciones

Tenemos la misma clase User a la que le agregamos una colección de direcciones:

¿Por qué hemos usado setAddresses y no addAddress y removeAddress? Por un tema relacionado con los errores, al final de este ejemplo lo vemos.

La clase Address tiene un atributo street, una relación con City y otra con User:

Vamos a ver primero la clase AddressType:

Hemos aprovechado los selects dependientes que creamos y este formulario tendrá un campo street y 3 selects dinámicos.

Finalmente la clase UserAddressesType:

Estamos embebiendo una colección como vimos al principio del artículo. El parámetro by_reference está a false porque en esta ocasión se van a poder añadir (allow_add) y eliminar (allow_delete) direcciones dinámicamente (en el controlador tendremos que añadir código para borrar),  por lo que tenemos que llamar a los métodos addAddress, removeAddress o setAddresses para que éstos añadan la relación con el usuario. Finalmente hay un atributo prototype_data que no está aún por defecto en los formularios y lo he agregado a través de una extensión del componente Form, en la versión 2.3 puede que esté por defecto.

¿Para qué necesitamos prototype_data? Como hemos visto al principio el campo prototype renderiza una plantilla, pero en algunas ocasiones queremos que esa plantilla tenga datos por defecto y para esto precisamente sirve este campo prototype_data. En nuestro caso lo necesitamos por los eventos del formulario, los campos de ciudad, provincia y país se añaden en el evento PRE_SET_DATAPRE_BIND del formulario por lo que es necesario pasarle algún valor para que se rendericen.

Sobre la vista, en este artículo de la documentación de Symfony está explicada la parte de Javascript para añadir en este caso más direcciones o se puede mirar directamente el código en la demo (hay código duplicado para que se pueda ver directamente en la parte de inferior de cada ejemplo). Hay un artículo en la documentación donde se puede leer cómo personalizar la renderización de los formularios, es posible que hagamos un artículo sencillo sobre esto.

Finalmente el problema que hay con addAddress y removeAddress tiene que ver con donde se muestran los errores. Cuando estamos añadiendo direcciones en la vista, el índice con el que se añaden en la colección debe de ser único, por esto es normal acabar teniendo los siguientes name en la vista (teniendo en cuenta que también se pueden borrar):

Cuando tenemos el parámetro by_reference a false lo que hace el componente de formularios es llamar a los método addX, removeX si existen y si no a setX. El problema viene cuando llamamos a addX porque itera sobre los elementos de la colección para ir añadiéndolos y los índices se pierden, entonces cuando vamos a mostrar los errores que están sobre los índices originales no los encuentra y los muestra como errores globales. En cambio con setX se mantienen estos índices ya que estamos pasando la colección entera.

ManyToMany

 Symfony2: Embeber formularios con relaciones

Este tipo de relaciones normalmente no tiene sentido embeberlas, ya que en el caso de estar el formulario de un usuario, lo normal sería elegir a qué grupos de los existentes va a pertenecer ese usuario y será simplemente un select o checkboxes. De todas formas si quisiéramos crear en ese formulario los grupos, la forma sería parecida a OneToMany.

OneToOne y OneToMany

Para finalizar vamos a ver los dos casos en un mismo formulario:

 Symfony2: Embeber formularios con relaciones

Simplemente creamos un nuevo UserType:

El type simplemente es una mezcla de los campos de los otros dos.

Conclusión

Lo importante en todo esto es saber cómo funciona todo lo que hemos visto, a partir de esta base se pueden construir cosas más complejas.

Vamos a ver todos estos ejemplos funcionando en la siguiente demo en la que no se persiste nada por comodidad, aunque la línea que llama a flush está comentada y con sólo descomentarla persistiría.

Ver Demo