1.2.1. Las clases sfForm y sfWidget

Los usuarios introducen la información en los campos de los formularios. En Symfony un formulario es un objeto que hereda de la clase sfForm. En nuestro ejemplo vamos a crear una clase llamada ContactoForm y que hereda de la clase sfForm.

Nota sfForm es la clase base de todos los formularios y simplifica la gestión de su flujo de trabajo.

Para empezar a configurar el formulario, se añaden widgets mediante el método configure().

Un widget representa un campo del formulario. En nuestro ejemplo tenemos que añadir tres widgets, que son los tres campos del formulario: nombre, email y mensaje. El listado 1-1 muestra la primera versión de la clase ContactoForm.

Listado 1-1 - La clase ContactoForm con tres campos

// lib/form/ContactoForm.class.php
class ContactoForm extends sfForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'nombre'    => new sfWidgetFormInput(),
      'email'     => new sfWidgetFormInput(),
      'mensaje'   => new sfWidgetFormTextarea(),
    ));
  }
}

Los widgets se definen en el método configure(). Este método se invoca automáticamente desde el constructor de la clase sfForm.

El método setWidgets() se utiliza para definir los widgets del formulario. Este método acepta como parámetro un array asociativo en el que las claves son los nombres de los campos y los valores son los objetos de tipo widget. Cada widget es un objeto que hereda de la clase sfWidget. En el ejemplo anterior se han utilizado dos tipos de widgets:

  • sfWidgetFormInput: este widget representa un campo de tipo <input>
  • sfWidgetFormTextarea: este widget representa un campo de tipo <textarea>

Nota Las clases de los formularios se guardan por convención en el directorio lib/form/. No obstante, puedes guardar los formularios en cualquier otro directorio incluido en el mecanismo de carga automática de clases de Symfony. Más adelante se verá que el propio framework utiliza el directorio lib/form/ para generar automáticamente los formularios a partir de los objetos del modelo.

1.2.2. Visualizando el formulario

Nuestro formulario ya está listo para ser utilizado. A continuación se crea un módulo en la aplicación para visualizar el formulario:

$ cd /ruta/hasta/el/proyecto
$ php symfony generate:module frontend contacto

En el módulo contacto se modifica la acción index para pasar una instancia del formulario a la plantilla, tal y como se muestra en el listado 1-2.

Listado 1-2 - La clase de las acciones del módulo contacto

// apps/frontend/modules/contacto/actions/actions.class.php
class contactoActions extends sfActions
{
  public function executeIndex()
  {
    $this->formulario = new ContactoForm();
  }
}

Al crear un formulario mediante new ContactoForm(), se invoca automáticamente el método configure() definido anteriormente.

Ahora ya sólo es necesario crear una plantilla como la del listado 1-3 para visualizar el formulario.

Listado 1-3 - La plantilla utilizada para visualizar el formulario

// apps/frontend/modules/contacto/templates/indexSuccess.php
<form action="<?php echo url_for('contacto/enviar') ?>" method="POST">
  <table>
    <?php echo $formulario ?>
    <tr>
      <td colspan="2">
        <input type="submit" />
      </td>
    </tr>
  </table>
</form>

Los formularios de Symfony sólo se encargan de los widgets que muestran la información a los usuarios. En la plantilla indexSuccess, la línea <?php echo $formulario ?> sólo muestra los tres campos del formulario. El resto de elementos, como la etiqueta <form> y el botón de envío, los debe añadir el programador. Aunque este comportamiento puede parecer poco intuitivo al principio, pronto se verá lo útil y sencillo que es para crear formularios más complejos.

Utilizar la instrucción <?php echo $formulario ?> es muy útil para crear prototipos y definir formularios. Permite a los programadores concentrarse en la lógica de la aplicación sin preocuparse de los detalles gráficos. El capítulo tres explica cómo personalizar la plantilla y el diseño del formulario.

Nota Cuando se muestra un objecto usando <?php echo $formulario ?>, el intérprete de PHP muestra la representación en texto del objeto $formulario. Para convertir el objeto en una cadena de texto, PHP intenta ejecutar el método mágico __toString(). Todos los widgets implementan este método para convertir el objeto en código HTML. Por lo tanto, ejecutar <?php echo $formulario ?> es equivalente a ejecutar <?php echo $formulario->__toString() ?>.

Ahora ya es posible visualizar el formulario en un navegador (figura 1-4) y comprobar el resultado accediendo a la dirección de la acción contacto/index (/frontend_dev.php/contacto).

Formulario de contacto generado

Figura 1.4 Formulario de contacto generado

A continuación se muestra el código HTML generado por la plantilla:

Listado 1-4 Código HTML generado por la plantilla

<form action="/frontend_dev.php/contacto/enviar" method="POST">
  <table>

    <!-- Código generado por <?php echo $formulario ?> -->
    <tr>
      <th><label for="nombre">Nombre</label></th>
      <td><input type="text" name="nombre" id="nombre" /></td>
    </tr>
    <tr>
      <th><label for="email">Email</label></th>
      <td><input type="text" name="email" id="email" /></td>
    </tr>
    <tr>
      <th><label for="mensaje">Mensaje</label></th>
      <td><textarea rows="4" cols="30" name="mensaje" id="mensaje"></textarea></td>
    </tr>
    <!-- Fin del código generado por <?php echo $formulario ?> -->

    <tr>
      <td colspan="2">
        <input type="submit" />
      </td>
    </tr>
  </table>
</form>

El código HTML generado por el formulario está compuesto de tres filas de tabla (etiqueta <tr>). Por ese motivo el código de la plantilla utilizaba antes una etiqueta <table> para encerrar los contenidos del formulario. Cada fila de formulario incluye una etiqueta <label> y una etiqueta de formulario (<input> o <textarea>).

1.2.3. Labels

Los títulos o labels de cada campo se generan de forma automática. La lógica que se utiliza por defecto para convertir los nombres de los campos en sus títulos sigue dos reglas:

  • La primera letra se convierte en mayúscula
  • Los guiones bajos se convierten en espacios en blanco

A continuación se muestra otro ejemplo:

$this->setWidgets(array(
  'codigo_postal'     => new sfWidgetFormInput(), // Título generado: "Codigo postal"
  'fecha_nacimiento'  => new sfWidgetFormInput(), // Título generado: "Fecha nacimiento"
));

Aunque la generación automática de los títulos es muy útil, el framework también permite definir títulos personalizados con el método setLabels():

$this->widgetSchema->setLabels(array(
  'nombre'  => 'Tu nombre',
  'email'   => 'Tu correo electrónico',
  'mensaje' => 'Tu mensaje',
));

También se puede modificar un solo título mediante el método setLabel():

$this->widgetSchema->setLabel('email', 'Tu correo electrónico');

El capítulo 3 muestra cómo se pueden personalizar los títulos desde la plantilla para personalizar aun más los formularios.

1.2.4. Más allá de las tablas generadas

Aunque por defecto el formulario se muestra con una tabla HTML, su diseño se puede modificar completamente. Los diferentes tipos de diseños o layouts se definen en clases que heredan de sfWidgetFormSchemaFormatter. El estilo por defecto de los formularios corresponde a la clase sfWidgetFormSchemaFormatterTable. También es posible utilizar el diseño list, basado en una lista HTML:

class ContactoForm extends sfForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'nombre'  => new sfWidgetFormInput(),
      'email'   => new sfWidgetFormInput(),
      'mensaje' => new sfWidgetFormTextarea(),
    ));

    $this->widgetSchema->setFormFormatterName('list');
  }
}

Los diseños de tabla y de lista están incluídos por defecto en Symfony. En el capítulo 5 se explica cómo crear tus propias clases de formato para personalizar el diseño de los formularios. Después de mostrar cómo se visualiza un formulario, el siguiente paso consiste en realizar el envío de los datos introducidos por el usuario.

1.2.5. Enviando el formulario

Al crear la plantilla que muestra el formulario, se empleó la URL interna contacto/enviar en la etiqueta <form> para enviar el formulario. Por tanto, el siguiente paso consiste en añadir la acción enviar en el módulo contacto. El listado 1-5 muestra cómo obtener en la acción la información enviada por el usuario y cómo se le puede redirigir a la página de agradecimiento, donde se vuelve a mostrar la misma información.

Listado 1-5 - La acción enviar en el módulo contacto

public function executeEnviar($request)
{
  $this->forward404Unless($request->isMethod('post'));

  $parametros = array(
    'nombre'  => $request->getParameter('nombre'),
    'email'   => $request->getParameter('email'),
    'mensaje' => $request->getParameter('mensaje'),
  );

  $this->redirect('contacto/gracias?'.http_build_query($parametros));
}

public function executeGracias()
{
}
// apps/frontend/modules/contacto/templates/graciasSuccess.php
<ul>
  <li>Nombre:  <?php echo $sf_params->get('nombre') ?></li>
  <li>Email:   <?php echo $sf_params->get('email') ?></li>
  <li>Mensaje: <?php echo $sf_params->get('mensaje') ?></li>
</ul>

Nota http_build_query es una función propia de PHP que genera una cadena de texto de tipo query string a partir de los parámetros pasados a través de un array y con sus valores correctamente codificados para incluirlos en una URL.

El método executeEnviar() realiza tres acciones:

  • Por razones de seguridad, se comprueba que la página se ha enviado utilizando el método POST. Si no es así, se redirige al usuario a una página de error 404. En la plantilla indexSuccess, se había declarado que el método de envío del formulario debe ser POST (<form ... method="POST">):
$this->forward404Unless($request->isMethod('post'));
  • A continuación se obtienen los valores introducidos por el usuario y se guardan en un array llamado parametros:
$parametros = array(
  'nombre'  => $request->getParameter('nombre'),
  'email'   => $request->getParameter('email'),
  'mensaje' => $request->getParameter('mensaje'),
);
  • Por último, se redirige al usuario a la página de agradecimiento (contacto/gracias) para mostrarle la información que ha introducido:
$this->redirect('contacto/gracias?'.http_build_query($parametros));

En vez de redirigir al usuario a otra página, se podría haber creado una plantilla llamada enviarSuccess.php. No obstante, aunque es posible hacerlo, después de un envío por el método POST se recomienda redirigir al usuario a otra página, ya que:

  • Se evita que el usuario envíe de nuevo el formulario si recarga la página de agradecimiento.
  • El usuario puede volver a la página anterior sin que se le muestre el mensaje de enviar el formulario de nuevo.

Nota Quizás has observado que el método executeEnviar() es diferente de executeIndex(). Cuando se invocan estos dos métodos, Symfony les pasa automáticamente como primer argumento el objeto de tipo sfRequest que representa a la petición actual. Como en PHP no es necesario recoger todos los parámetros que se pasan a una función, el método executeIndex() no incluye el parámetro $request en su definición, ya que no lo utiliza para nada.

La figura 1-5 muestra el flujo de trabajo de todos los métodos que intervienen en la interacción del usuario.

Flujo de trabajo de los métodos

Figura 1.5 Flujo de trabajo de los métodos

Nota Cuando se vuelve a mostrar la información del usuario en la plantilla, se corre el riesgo de sufrir un ataque de tipo XSS Cross-Site Scripting). Puedes consultar el capítulo sobre la parte de la vista del libro oficial de Symfony para conocer más detalles sobre cómo evitar los ataques XSS.

Después de enviar el formulario, deberías ver la página de la figura 1-6.

Página que se muestra después de enviar el formulario

Figura 1.6 Página que se muestra después de enviar el formulario

Por otra parte, en vez de crear el array parametros, sería más sencillo recoger la información del usuario directamente en un array. El listado 1-6 modifica el atributo name de HTML de los widgets para guardar los valores en un array llamado contacto.

Listado 1-6 - Modificando el atributo name de HTML de los widgets

class ContactoForm extends sfForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'nombre'  => new sfWidgetFormInput(),
      'email'   => new sfWidgetFormInput(),
      'mensaje' => new sfWidgetFormTextarea(),
    ));

    $this->widgetSchema->setNameFormat('contacto[%s]');
  }
}

El método setNameFormat() permite modificar el atributo name en todos los widgets. Cuando se genera el formulario, el valor %s se reemplaza automáticamente por el nombre de cada campo. Si se toma como ejemplo el campo email, su atributo name de HTML será contacto[email]. Si se utilizan estos nombres, PHP crea automáticamente un array que incluye todos los valores enviados en la petición. De esta forma, los valores de los campos se pueden acceder mediante el array contacto.

Ahora en la acción se puede obtener el array contacto directamente a partir del objeto de la petición, tal y como se muestra en el listado 1-7.

Listado 1-7 - Nuevo formato de los atributos name de los widgets

public function executeEnviar($request)
{
  $this->forward404Unless($request->isMethod('post'));

  $this->redirect('contacto/gracias?'.http_build_query($request->getParameter('contacto')));
}

Si observas el código HTML del formulario generado, verás que Symfony no sólo crea un atributo name a partir del nombre y formato de cada widget, sino que también crea un atributo id. En realidad, el atributo id es una copia del atributo name en la que se han sustituido los caracteres problemáticos por un guión bajo (_):

Nombre del widget Atributo name Atributo id
nombre contacto[nombre] contacto_nombre
email contacto[email] contacto_email
mensaje contacto[mensaje] contacto_mensaje

1.2.6. Una solución alternativa

En el ejemplo anterior se utilizan dos acciones para gestionar el formulario: acción index para mostrarlo y acción enviar para enviarlo. Como el formulario se visualiza con el método GET y se envía con el método POST, se pueden fusionar los dos métodos en una única acción index como se muestra en el listado 1-8.

Listado 1-8 - Fusionando las dos acciones utilizadas en el formulario

class contactoActions extends sfActions
{
  public function executeIndex($request)
  {
    $this->formulario = new ContactoForm();

    if ($request->isMethod('post'))
    {
      $this->redirect('contacto/gracias?'.http_build_query($request->getParameter('contacto')));
    }
  }
}

Para que el código anterior funcione correctamente, no olvides modificar el atributo action del formulario en la plantilla indexSuccess.php:

<form action="<?php echo url_for('contacto/index') ?>" method="POST">

Como se verá más adelante, es mejor utilizar esta sintaxis porque es más breve y hace que el código resultante sea más coherente y fácil de entender.