Guía Zend: Sobre decorators en Zend_Form
Zend_Form es una solución altamente flexible al problema de los formularios, como vimos en el artículo anterior, se encarga de generarlos, validar y filtrar datos, etc. Otra de sus tareas también es, por supuesto, generar el markup del form y los distintos elementos, este suele ser el aspecto que más complicaciones nos genera.
Cuando empezamos a investigar como hacerlo suelen surgir miles de dudas. Quizá no entendemos del todo bien que son los decorators ni nos llegamos a hacer una idea de lo flexibles que son; comúnmente pensamos que no es posible hacer markups complicados, pero veremos que no es así.
Zend_Form_Decorator_Abstract
Esta es la clase base a partir de la cual extienden todos los decorators anteriormente nombrados. Los métodos más importantes que implementa son:
setOptions() //setea las diferenes opciones del decorator. setOptions("placement", APPEND / PREPEND) //si el contenido generado por el decorator se agrega al principio o al final. setOptions("separator", string) //separador entre el contenido viejo y el nuevo
Standard Decorators
Zend trae por default los siguientes decorators, veamos los métodos más usados de cada uno, todos los decorator tienen los métodos nombrados anteriormente para Zend_Form_Decorator_Abstract:
- Callback: permite generar contenido de acuerdo a un callback en particular.
setCallback($callback)
- Description: muestra la descripción seteada en un Zend_Form o Zend_Form_Element.
setTag($tag): el tag del elemento que contiene el texto, por defecto <p> setEscape($bool): si queremos escapar o no los caracteres HTML. Por defecto es true. setClass($class): class del elemento. Por defecto "hint".
- Errors: lista los errores que contiene el elemento.
- Fieldset: pone dentro de un fieldset el contenido recibido.
setLegend($legend): setea el legend del fieldset.
- Form: genera el tag >form> del formulario.
- FormElements: forms, display groups y subforms son colecciones de elementos. Este decorator recorre cada uno de estos elementos y genera su contenido, por lo que es un decorator que siempre debe estar presente.
- FormErrors: parecido al Errors, pero muestra todos los mensajes de error en la parte superior del formulario en lugar de hacerlo para cada elemento en particular.
- HtmlTag: nos permite generar el tag html que querramos.
setTag($tag) setOption("openOnly", $bool): crea solamente el tag de apertura. setOption("closeOnly", $bool): crea solamente el tag de cierre
- Label: en general todos los elementos del form tienen un texto asociado, este texto es generado por este decorator.
- ViewHelper: es el elemento propiamente dicho, por ejemplo un <input/>, un <textarea/>, etc.
Setear Decorators
Podemos agregar decorators a forms (y subforms) y elementos. Cada uno de estos objetos puede tener asociados todos los decorators que queramos. El orden de los decorators es importante porque generalmente determina el markup final, no es lo mismo tener <input /> <label /> que <label /> <input />, de un ejemplo a otro lo que cambia es el orden en el que fueron incluidos los decorators ViewHelper y Label.
Los decorators asociados a un objeto se comportan como una cola, por lo que si queremos agregar un decorator al principio (o en cualquier otro lugar que no sea el final) tendremos que setear todos los decorators otra vez.
Para agregar / setear decorators tenemos dos métodos, setDecorators() y addDecorator():
- setDecorators() setea los decorators del elemento, es decir que si ya hay algún decorator seteado lo sobreescribe.
- addDecorator() agrega un decorator al final de los decorators existentes.
Estos métodos son tanto para elementos, como para forms, subforms y display groups.
$element->setDecorators($decorators); $form->addDecorator($decorator);
Aquí suelen empezar las dudas, pero veamos algunos ejemplos de uso, suponiendo que nuestro elemento es un text input:
$element->setDecorators(array("ViewHelper", "Label", "HtmlTag"));
Logramos generar <div> <label /> <input /> </div>
$element->setDecorators(array("ViewHelper", "Label", "HtmlTag", "HtmlTag"));
Aquí podemos pensar que tenemos dos divs, pero no es el caso, porque el último html tag sobreescribe al primero. Para tener dos HtmlTag distintos hay que asignarle un alias a alguno de ellos. Antes de esto, veamos un poco más las diferentes formas de pasar un decorator.
Es importante ver y entender este ejemplo porque aquí vemos todas las posibilidades existentes. Obviamente no es del todo válido porque tenemos 3 decorators que se llaman igual (“HtmlTag”) y otros 2 que se llaman “miHtmlTag”, por lo que se estarían sobreescribiendo y solo quedaría uno de cada tipo, pero veamoslo como un ejemplo educativo.
$element->setDecorators(array( // agregamos un decorator del tipo HtmlTag "HtmlTag", // idéntico al primer ejemplo, el hecho de que lo pasemos dentro de un array no cambia nada array("HtmlTag"), /* aca vemos un ejemplo mas concreto del caso anterior. Pasamos el decorator como un array cuando queremos setear alguna opción adicional. el primer elemento del array es el nombre del decorator, y el segundo un array de opciones a setearle al decorator */ array("HtmlTag", array("tag" => "span")), /* este ejemplo es igual al anterior, pero ahora al decorator le damos un alias. En el futuro no nos referiremos a él como el decorator HtmlTag, sino como "miHtmlTag" */ array(array("miHtmTag" => "HtmlTag"), array("tag" => "span")), // por último, aqui creamos un HtmlTag dándole otra vez un alias, pero esta vez sin pasarle las opciones array(array("miHtmlTag" => "HtmlTag")) ));
Teoría de funcionamiento
Ahora que ya sabemos como agregar decorators, como darles un alias para poder usar varios decorators del mismo tipo y como setear las opciones; vamos a hablar sobre como van trabajando los distintos decorators.
Cada decorator tiene la función de generar un markup en particular. El contenido generado por un decorator es pasado al siguiente, este agrega (al principio o al final) su propio markup al contenido que recibe y se lo pasa al siguiente decorator que vuelve a hacer lo mismo.
Supongamos que nuestro elemento tiene los siguientes decorators:
$element->setDecorators(array(“ViewHelper", “Label", “HtmlTag", “Error"));
1) Al ejecutar el método render() se inicia el proceso. Al primer decorator se le pasa una cadena vacía (“”) .
$html = “";
2) El decorator ViewHelper genera el HTML del elemento asociado, si nuestro elemento es un text input obtendremos como resultado:
$html = $html . '<input type="text" name="…" />';
3) El siguiente decorator es el Label, por defecto el contenido que genera lo agrega al principio (placement = prepend):
$html = '<label>...</label>' . $html = '<label>...</label><input type="text" name="…" />';
4) El HtmlTag por defecto funciona como wraper, es decir que envuelve el contenido que recibe, y el tag por defecto es div:
$html = '<div>' . $html . '</div>' = '<div><label>...</label><input type="text" name="…" />>/div>';
5) El decorator Errors se aplica solamente si hay algún error. Si no hay ningún error no hace nada. Supongamos que ocurrió algún error al validar el form, el markup será así:
$html = $html . '<ul><li>Error 1</li><li>Error N</li></ul>';
Por lo que aquí termina el proceso, y el markup final es:
<div> <label>...>/label> <input type="text" name="…" /> </div> <ul> <li>Error 1>/li> <li>Error N>/li> </ul></div>
Como vimos el markup se fue armando paso a paso y los decorators trabajaron desde adentro hacia afuera, es decir que los decorators que están más afuera son los últimos en ser agregados, ya que los decorators agregan contenido adicional pero generalmente no modifican el contenido que reciben.
Un poco de teoría sobre placement
¿Estos dos decorators son iguales?
array('ViewHelper', 'Label') ¿=? array('Label', 'ViewHelper')
Quizás la respuesta rápida es decir que no, pero en realidad si, porque el comportamiento por default del Label es agregar el contenido al principio, entonces en el primer caso tenemos:
1) <input ... />
2) <label ... /><input ... />
y en el segundo caso
1) <label ... />
2) <label ... /><input ... />
Es un pequeño detalle que hay que tener en cuenta, porque muchas veces cuando pensamos que algo no se puede hacer o es muy difícil, se soluciona simplemente sabiendo manejar el placement de cada decorator.
Algunos ejemplos
Ahora veamos un ejemplo de un markup un poco mas complejo, por ejemplo tenemos el siguiente form:
public function init() { $this->addElement('text', 'uno, array('label' => 'Uno')); $this->addElement('text', 'dos, array('label' => 'Dos')); }
¿cómo generamos el siguiente markup?
>div class="row"> <div><label>Uno</label><input type='text' name='uno' /></div> <div><label>Dos</label><input type='text' name='dos' /></div> >/div>
Veamos:
public function init() { $this->addElement('text', 'uno', array('label' => 'Uno')); $this->addElement('text', 'dos', array('label' => 'Dos')); $this->addElement('text', 'tres', array('label' => 'Tres')); $this->setElementDecorators(array('Label', 'ViewHelper', 'HtmlTag')); // nos falta el div que los envuelve... $this->getElement('uno')->addDecorator('HtmlTag', array('class' => 'row', 'openOnly' => true)); $this->getElement('tres')->addDecorator('HtmlTag', array('closeOnly' => false)); }
Por si no se entiende bien explico lo que hicimos. Con setDecorators() setemos los decorators de todos los elementos del form agregados hasta el momento. Como los tres elementos tienen los mismos decorators, en vez de setearlos de a uno lo hacemos asi, si en el futuro los queremos cambiar los cambiamos en un solo lugar y listo.
Como cada decorator funciona sobre un elemento, no podemos envolver los tres elementos aplicando un decorator a uno solo de ellos. Por lo tanto, al primer decorator le agregamos el tag de apertura y al último el tag de cierre. Al terminar el render(), el resultado final será el esperado.
Por último, otro ejemplo bastante común es como maquetar una tabla:
<form> <table> <tr><td><label>Uno</label></td><td><input name='uno' /></td></tr> <tr><td><label>Dos</label></td><td><input name='dos' /></td></tr> <tr><td><label>Tres</label></td><td><input name='tres' /></td></tr> </table> </form>
Los decorators de los elementos los seteamos así:
$this->setElementDecorators( array( "ViewHelper", array("HtmlTag", array("tag" => td")), array("Label" , array("tag" => "td")), array(array("tr" => "HtmlTag"), array("tag" => "tr")) ) );
Explicado rápidamente, creamos el input, lo envolvemos en un >td>, creamos el label con otro tag td y se ubica al principio (ya tenemos los dos >td> en orden), y por último un HtmlTag >tr> que envuelva todo lo anterior.
Ahora faltaría el tag table. Podemos hacer como en el ejemplo anterior, usar dos HtmlTag en el primer y último elemento y listo. Pero veamos otra posibilidad. Modificamos los decorators del form de la siguiente manera:
$this->setDecorators( array( "FormElements", array("HtmlTag", array("tag" => "table")), "Form" ) );
El decorator FormElements renderiza todos los elementos. Es decir, que nos quedaría la tabla sin el tag table no? entonces le agregamos el tag table que falta, y por último el tag form. Listo! No me van a decir que no fue fácil eh! Resumiendo el tema, para saber maquetar cualquier formulario necesitamos conocer a fondo los decorators y todas las opciones de configuración de cada uno, y quizás un poquito de imaginación.
Por último un consejo: el manual oficial es nuestro mejor amigo, ténganlo en cuenta ante cualquier duda, ya que todas las respuestas están ahí. Pero mucho más amigo aún, es el código fuente, es la manera más rápida de entender el funcionamiento de las cosas y conocer métodos que no sabemos que existen. En este caso, estamos hablando de los decorators pero se puede generalizar a cualquier otra cosa.
Continuando con la guía:
Si tienen dudas de como funciona algo no hay nada mejor que mirar el código y entenderlo. ZF se caracteriza por su simpleza, así que no piensen que mirar el código del framework es una pérdida de tiempo. Van a entender mejor las cosas y a ganar una cantidad de conocimiento impresionante.
editor
[…] Zend sobre decorators en Zend_Form crea y maneja formularios con […]
Muy buena la guía
La encontré ayer y me la fumé toda en un rato.
Ahora que estaba codeando, y tuve que hacer un form releí esta parte y me vino la pregunta…
Que beneficios reales tiene hacer un formulario así?
O sea, mi punto real es porque la clase Form_Registro debe crear el form ademas de validarlo, devolver errores, etc.
Es una cuestion teorica mas que practica…
Tal vez en un futuro con mas uso de Zend este de acuerdo con esta manera de armar formularios, pero hoy creo que la clase X que se encarga de manejar el formulario X, deberia validarlo, devolver lo que tenga que devolver, etc, etc. pero no escribirlo literalmente…
Eso es lo que no me termina de cerrar…
Me voy a poner a armar un par de formularios a ver que onda :p
Saludos!
Hola Guille 😀
A que te referis con “crear un formulario asi”?? Tu duda es porque creamos la clase Form_Registro?? En principio porque al hacerlo asi es reutilizable, si queremos usar el mismo form en varios lugares simplemente incluimos dicha clase, por otro lado los controladores quedan mucho mas legibles porque sacamos todo ese codigo de ahi
Y si, la clase X se encarga de crear el formulario, agregarle los elementos que lleve, validarlo (o mejor dicho setear los validators que vayamos a usar, que no es exactamente lo mismo :P), etc.
Que todo lo relacionado con el formulario se haga en el formulario mismo nos facilita mucho las cosas
Gracias por la respuesta Rodrigo.
Me parece perfecta la creación de la clase que maneja el form, lo que no me gusta es el hecho de que la clase “literalmente” escribe el formulario.
No me cierra el no tener manejo sobre el código hotmail entero de un template, en el mismo template.
Primero, no tenemos un manejo html real del formulario, cosa que nos impide (por decir algo) agregar fácilmente un br en medio de dos elementos (repito…solo por tirar un nombre…podría ser cualquier tag,clase o lo que fuese)
Segundo, adiós al trabajo en grupo con gente encargada de backend y de frontend. No veo normalmente a los diseñadores programando en ZF, ni a un programador accediendo a las demandas constantes de un diseñador de “agregame este campo acá, ponerme está clase alla, cámbiame este label de ahí”…
Y tercero, me parece confusa la idea de que si quiero modificar un template tenga que andar navegando por archivos html, php, clases diferentes… no me cierra…
Yo lo veo como:
juancito diseñador dice “he, agregue el campo xxx al form de registro y es obligatorio”
Y pepito backend haría algo como (ejemplo al vuelo) $form->addValidator(‘field’,array(‘required’ -> true));
Y listo… eso ya validaria el nuevo campo agregado por juancito diseñador en el phtml.
Gracias por todo.
PD: disculpen los ejemplos al vuelo y las posibles faltas…lo escribí desde el cel
Mmmm
Estaba en facebook bloud*** y de repente me vino a la mente “tal vez si hago el form tal cual como vos lo explicaste en la clase anterior y no hago el echo $this->form en la vista se me solucionaria todo el problema” :p
No se si sera lo correcto pero bueno…
Me fui a dormir que mañana trabajo :p
Saludos!
Hola Guille
Entiendo que no te guste que la clase literamente escriba el formulario, hubo miles de discusiones al respecto, sobre si le corresponde a Zend_Form realizar todas las tareas que realiza o no, pero la realidad es que tenemos una clase Formulario que se encarga de crear el HTML del form, de validarlo y de filtrarlo… esta bien o esta mal?? todo depende del punto de vista de cada uno, a mi personalmente me encanta Zend_Form y nunca en mi vida volveria a hacer un formulario “a la antigua”, pero es todo cuestion de gustos y de costumbre
El manejo del formulario que tenemos es total, podes meter un “br” donde quieras y de varias maneras diferentes, el tema es conocer bien como funcionan los decorators, pero que se puede se puede, no hay ningun tipo de limite con el html que podemos armar
Sobre el tema programadores backend/frontend, en mi caso los diseñadores maquetan todo y me lo dan maquetado y yo creo el formulario que recibi usando Zend_Form
Tercero, modificar algo del form es modificar la clase correspondiente al form, ningun otro archivo
Si tu idea es que los diseñadores puedan armar el HTML manualmente y que tu trabajo sea validarlo y filtrarlo, entonces no uses Zend_Form que los diseñadores armen el form como ellos quieren (html puro a mano) y luego validas/filtras $_POST/$_GET con Zend_Validate/Zend_Filter, ambos componentes los podes usar por afuera de Zend_Form asi que ya no tendrias ese problema 😉
Se me ocurren varias ideas interesantes para implementar esto que vos decis, casi casi que me pondria a hacerlo jaja ^^
Saludos!
Hola a todos,
Alguien puede ayudarme a hacer un autocompletar en zend?
Necesito tener un campo que muestre diferentes opciones, como las que muestra google.
Si alguien tiene un ejemplo o puede ayudarme se lo agredeceria.
Excelente explicación.
Me surge una duda, como independizo el “Errors” ya se enmarcan dentro de un , y esto hace que cuando creo los forms enmarcado en una tabla, cuando aparece un error, la tabla se daña.
Pues el Errors es parte del del input.
Como hacer que Errors tenga en vez de un ???