Formularios empotrados dinámicos en Symfony
Los formularios embedded de symfony 1.2 son una herramienta muy útil, pero ¿cómo agregarlos dinámicamente desde la vista con AJAX?
Supongamos una aplicación simple. Se trata de tarjetas con una pregunta y su respuesta.
Podemos querer poner una imagen como pista, empotrando un formulario PictureForm:

class CardForm extends BaseCardForm
{
public function configure()
{
unset($this['cardset_id'],$this['usercard_list']);
$this->embedForm('picture', new PictureForm());
}
}
y quedaría así:
Luego esto con suerte lo maquetará otra persona ;)
¿Pero y si nos interesa que pueda haber más de una imagen, en número variable?

Pues en ese caso nos tenemos que arremangar y meter más código. Empezamos por el formulario (CardForm.class.php en mi caso):
class CardForm extends BaseCardForm
{
public function configure()
{
unset($this['cardset_id'],$this['usercard_list']);
//Empotramos al menos un formulario de pictures
$pictures = $this->getObject()->getPictures();
if (!$pictures){
$picture = new Picture();
$picture->setCard($this->getObject());
$pictures = array($picture);
}
//Un formulario vacío hará de contenedor para todas las pictures
$pictures_forms = new SfForm();
$count = 0;
foreach ($pictures as $picture) {
$pic_form = new PictureForm($picture);
//Empotramos cada formulario en el contenedor
$pictures_forms->embedForm($count, $pic_form);
$count ++;
}
//Empotramos el contenedor en el formulario principal
$this->embedForm('pictures', $pictures_forms);
}
}
Este código nos valdrá tanto para objetos nuevos (en ese caso empotramos un solo formulario de pictures para empezar), como para objetos con pictures ya empotradas.
Ahora vamos a hacer en el mismo fichero una función AddPicture() que nos será muy útil:
public function addPicture($num){
$pic = new Picture();
$pic->setCard($this->getObject());
$pic_form = new PictureForm($pic);
//Empotramos la nueva pícture en el contenedor
$this->embeddedForms['pictures']->embedForm($num, $pic_form);
//Volvemos a empotrar el contenedor
$this->embedForm('pictures', $this->embeddedForms['pictures']);
}
Esta función añade un PictureForm a al formulario. Esto nos va a venir bien por dos razones (si esto no se entiende bien ahora, no importa, luego lo veremos con detalle):
1. Al añadir con AJAX una picture, llamaremos a un action que ejecutará esta función para obtener un formulario con el número de pictures empotradas adecuado. Entonces el action renderizará solo la que nos interesa. Y esto será lo que insertaremos en la vista.
2. Al asociar (bind) el formulario, necesitamos que el formulario que va asociarse a los datos que enviemos tenga el número correcto de pictures ya empotradas. Es decir, si vamos a asociar un formulario que ha introducido el usuario con un número determinado de pictures empotradas, el objeto formulario con el que vamos a hacer bind debe tener el mismo número de formularios empotrados. Para eso, sobreescribiremos la función bind para que añada tantas pictures empotradas como nos haga falta.
Vamos por pasos. El primero. En la vista, añadimos esta función de AJAX. Yo uso Jquery, si usas Prototype u otra cosa no te será difícil hacer algo parecido:
<script type="text/javascript">
var pics = <?php print_r($form['pictures']->count())?>;
function addPic(num) {
var r = $.ajax({
type: 'GET',
url: '<?php echo url_for('card/addPicForm')?>'+'<?php echo ($form->getObject()->isNew()?'':'?id='.$form->getObject()->getId()).($form->getObject()->isNew()?'?num=':'&num=')?>'+num,
async: false
}).responseText;
return r;
}
$().ready(function() {
$('button#add_picture').click(function() {
$("#extrapictures").append(addPic(pics));
pics = pics + 1;
});
});
</script>
y tras el formulario un botón para añadir subformularios y un espacio para colocarlos cuando nos los la función AJAX.
<?php echo $form ?> <div id="extrapictures"/> <tr><td><div><button id="add_picture" type="button"><?php echo "Añadir otra imagen"?></button></div></td></tr>
En el action, hacemos la función executeAddPicForm()
public function executeAddPicForm($request)
{
$this->forward404unless($request->isXmlHttpRequest());
$number = intval($request->getParameter("num"));
if($card = CardPeer::retrieveByPk($request->getParameter('id'))){
$form = new CardForm($card);
}else{
$form = new CardForm(null);
}
$form->addPicture($number);
return $this->renderPartial('addPic',array('form' => $form, 'num' => $number));
}
Que básicamente lo que hace crear un formulario y llamar al addPicture() de ese formulario para tener un nuevo formulario con el número de elementos empotrados que nos interesa. Entonces le pasamos a una template addPic ese objeto formulario y el número del subformulario empotrado que queremos representar. La template tiene este código:
<?php echo $form['pictures'][$num]['id']->render()?> <?php echo $form['pictures'][$num]['file']->renderRow();?>
Con esto ya tenemos un botón que añade nuevos subformularios. Pero ¿y a la hora de guardar? Como hemos dicho antes, necesitaremos que bind prepare el formulario. El nuevo código de bind será:
public function bind(array $taintedValues = null, array $taintedFiles = null)
{
foreach($taintedValues['pictures'] as $key=>$newPic)
{
if (!isset($this['pictures'][$key]))
{
$this->addPicture($key);
}
}
parent::bind($taintedValues, $taintedFiles);
}
Y con esto ya podemos usar este formulario como uno cualquiera. No es lo más cómodo del mundo, pero podemos seguir aprovechando sfForm totalmente en lugar de escribir toda la lógica de tratamiento de formulario nosotros mismos.
Solo nos quedará el trabajo de maquetación, descomponiendo campos de formularios para que todo quede bien :)

Hola gracias por el artículo. Es muy bueno. Sin embargo, lo estoy aplicando en un proyecto con symfony 1.2.9, y no acaba de funcionar. El problema sucede cuando intento “modificar” el objeto principal (Card). Al entrar en el formulario de edición y pulsar “guardar”, symfony arroja este error:
SQLSTATE[23502]: Not null violation: 7 ERROR: el valor null para la columna “id” viola la restricción not null
Refiriéndose a que el objeto subordinado (en este caso la imagen), carece del campo ID. Si por contra, cambiamos el campo ID de la imagen, por un sfWidgetFormInput, los datos se guardan correctamente.
El problema parece estar en el hecho de que al hacer un embedForm, el campo clave de la clase subordinada, al ser de tipo Hidden no se incluye en el formulario.
Pienso que la solución puede estar en el bind … ¿ qué opinas ?
Solucionado. Problema mío. El problema radicaba en que estoy utilizando un “Formateador” para el formulario, y se me olvidó indicar que mostrase los campos ocultos …
De todos modos, ahora estoy modificando el formulario para que dinámicamente se puedan eliminar algunas líneas de detalle.
Ya contaré cómo consigo arreglarlo porque de momento, al eliminar las líneas falla.
Hola, Juan. Perdona por no haber contestado antes. Sí, lo de los campos ocultos a mí se me olvida con frecuencia. Respecto a lo de borrar, ya contarás si lo consigues. Se me ocurre una forma análoga a la fución addPic del post, pero tendría que probarla antes de actualizar el post para estar segudo de que funciona. Saludos.
Hola Nacho. Soy nuevo en symfony. y aún no conosco bien la estructura de carpetas y archivos. ni e hecho nada con ajax.
01) En el ejemplo que expusiste no entendí en que archivos debo guardar los textos que mencionaste.
Por ejemplo para el siguiente código:
01
02 var pics = count())?>;
03
04 function addPic(num) {
Eso sería en apps/miaplicación/modules/miModulo/templates/indexSuccess.php
O en apps/miaplicacion/templates/layout.php ¿??
Y en que archivo va:
1
2
En /modules/miModulo/actions/actions.class.php va el siguiente codigo:??
public function executeAddPicForm($request)
{
03 $this->forward404unless($request->isXmlHttpRequest());
04 $number = intval($request->getParameter(“num”));
En cual de las carpetas templates iría lo siguiente??
1 render()?>
2 renderRow();?>
Donde va public function bind(…. ¿??
en CardForm.class.php???
02) otra consulta.. como sería el schema.yml para este caso??
03) que sentencias debo ejecutar antes de empezar a escribir todo esto?
symfony plugin:install sfJqueryReloadedPlugin
symfony propel:build-all
y Editar el archivo setting.yml
all:
.settings:
jquery_web_dir:/sfJqueryPlugin/js/jquery-1.2.6.min.js
se que son varias consultas, pero es que ya son varios días que estoy investigando esto.
yo estoy necesitando formularios empotrados porque quiero hacer un pequenho formulario donde maneje las tablas factura, detalleFactura, concepto. en delphi lo hago rápido pero en php con symfony me está dando dolores de cabeza ya.
La web está muy buena, e interesante.!
Hola Nacho. ya logré que funcione el embebido. pero aparentemente hay un error en el script.
según Netbeans y Firebug de mozilla, hay un error: missing {after propertier list} y senhala la url del script como el que contiene el error.
y con netbeans senhala error de sintaxis en casi todas las lineas del script.
a que se debería eso?
Disculpa que no contestase antes, pablo. Tengo que hacer que esto me avise de los nuevos comentarios, que aún no lo tengo puesto :P
No sé a qué se deberá lo del error. Uso vim normalmente y no me avisa de estas cosas. Con eclipse no me da error en este tipo de código. Sí que me los da en cambio en ficheros de symfony, pero tampoco sé a qué se deberá. Si lo descubres por favor dímelo.
Hola queria saber si me podias dar una mano, porque me perdi en el tutorial que armaste. Llegue hasta la funcion addPicture, despues de ahi no se donde agregar el codigo
Gracias
Lo siguiente va en la vista. ( _form.php )
Una pregunta, he visto muchos post y dudas de formularios empotrados y he seguido el ejmplo con schema de venta, es decir tengo un form de venta al cual le voy agregando productos dinamicamente, todo funciona bien en modo edicion.
Mi schema resumido
venta:
id:
total: {type: decimal}
created_at:
updated_at:
detalleventa:
id: {type: integer, primaryKey:true}
venta: {type: integer, primaryKey: true, foreignTable: venta, foreignReference: id, onDelete:cascade}
producto: {type: varchar(15), foreignTable: producto, foreignReference: id}
Pero cuando la venta es nueva, no tengo manera de obtener el id y siempre el id de venta en la tabla de detalleventa me lo guarda como 0.
Que metodo debo de redefinir para que primero se guarde el formulario padre y luego el formulario empotrado.
Gracias excelente aporte..
hola, exelente el tutorial… pero tengo un problema, hice lo que dice este tutorial pero con mis propias clases pero cuando quiero guardar me dice que los id son invalidos, he verificado que se muestren los campos ocultos y en estos aparecen bien los id.
por favor si alguien puede ayudar.
Gracias!
Hola, Bueno el tuto pero al utilizarlo para un formulario que tiene i18n no me funciona, es algo raro me trae los campos con i18n pero no me trae el id y lang de cada idioma
haa por cierto estoy utilizando Doctrine
@Marcos
Listo solucione el error es un bug de symfony en su version 1.4.x con respecto a la formularios empotrados que contienen i18n.
La solucion es en el archivo symfony/lib/form/addon/sfFormObject.class.php intercambiar la linea 244 por 245
y con eso funciona correctamente.
Hola Nacho, excelente tuto, me ayudo a resolver uno de los problemas que tenia, tambien lei en el que describes el metodo bind() muy interesante tambien aunque muy avanzado para mi.
Te queria preguntar algo a ver si es posible que me des una idea, hice esto mismo que tienes aqui y todo funciona perfectamente, el problema surge cuando quiero embeber este formulario dinamico dentro de otro, ya que no se como hacer para que me reconozca los campos agregados dinamicamente, creo que el bind debe tener algo que ver, pero no logro dar con una solucion, muchas gracias por tu aporte.
Saludos
Hola, Moisés. Gracias por tu comentario. Respecto al problema que cuentas, te contesto al comentario que dejaste en el otro post sobre bind().
Hola, estoy trabajando con symfony 1.4 y propel. Tengo un formulario el cual debe actualizar o llenar otros campos si el usuario ingresa un valor que esta en la base de datos. Cómo hago esto en symfony 1.4 usando propel??? Les agradezco sus respuestas, es urgente :(
@Juan
Hola Juan, yo tambien estoy desarrollando un sistem donde necesito crear facturas.. estoy trabado en como hacer para eliminar detalles, todo funciona excelentemente, pero no se como pone un icono con un link a la accion eliminar de ese detalle, si lo resolviste comunicate , saludos