Symfony 1.4, la guía definitiva

10.4. Procesando el envío del formulario

Una vez que el usuario rellena un formulario y lo envía, la aplicación web del servidor debe obtener los datos y procesarlos como sea conveniente. La clase sfForm incluye todos los métodos necesarios para hacerlo en unas pocas líneas de código.

10.4.1. Procesado simple de formularios

Como los widgets al final siempre generan etiquetas HTML normales, la acción puede obtener sus valores directamente como parámetros de la petición. Siguiendo el ejemplo del formulario de contacto, su acción asociada podría ser la siguiente:

// en modules/mimodulo/actions/actions.class.php
public function executeContacto($request)
{
  // Definir el formulario
  $this->form = new sfForm();
  $this->form->setWidgets(array(
    'nombre'  => new sfWidgetFormInputText(),
    'email'   => new sfWidgetFormInput(array('default' => '[email protected]')),
    'asunto'  => new sfWidgetFormChoice(array('choices' => array('Asunto A', 'Asunto B', 'Asunto C'))),
    'mensaje' => new sfWidgetFormTextarea(),
  ));

  // Procesar la petición
  if ($request->isMethod('post'))
  {
    // Procesar los datos del formulario
    $nombre = $request->getParameter('nombre');

    ...

    $this->redirect('mimodulo/otraaccion');
  }
}

Si el método de la petición es GET, la acción devuelve como resultado sfView::SUCCESS y por tanto se muestra la plantilla contactoSuccess con el formulario de contacto. Si el método de la petición es POST, la acción se encarga de procesar los datos del formulario y después redirecciona a otra acción del mismo módulo. Para que el código anterior funcione bien, el atributo action de la etiqueta <form> debe ser igual a la acción que muestra el formulario. Ese es el motivo por lo que en los ejemplos anteriores el atributo action del formulario siempre era mimodulo/contacto:

// en modules/mimodulo/templates/contactoSuccess.php
<?php echo $form->renderFormTag('mimodulo/contacto') ?>
...

10.4.2. Procesado de formularios con validación

Las aplicaciones reales procesan los formularios de forma mucho más avanzada, no simplemente obteniendo los datos enviados por el usuario. Normalmente, el controlador de la aplicación se encarga de:

  1. Comprobar que los datos cumplen una serie de reglas de validación (algunos campos son obligatorios, el email debe tener un formato adecuado, etc.)
  2. Opcionalmente se pueden transformar los datos para facilitar su procesamiento (eliminar espacios en blanco sobrantes, convertir fechas al formato PHP , etc.)
  3. Si los datos no son válidos, se vuelve a mostrar el formulario con los mensajes de error apropiados.
  4. Si los datos son correctos, hacer algo con ellos y redirigir al usuario a otra acción.

Symfony incluye un mecanismo de validación automática que comprueba si los datos enviados por el usuario cumplen una serie de reglas predefinidas. Para ello, primero se define una serie de validadores para cada campo. A continuación, cuando el usuario envía el formulario, sus datos se unen al objeto del formulario, mediante un proceso llamado bind. Por último, se pide al formulario que compruebe si los datos son válidos. El siguiente ejemplo muestra cómo validar que el valor del widget email es realmente una dirección de email y que el campo mensaje tiene una longitud de al menos cuatro caracteres:

// en modules/mimodulo/actions/actions.class.php
public function executeContacto($request)
{
  // Definir el formulario
  $this->form = new sfForm();
  $this->form->setWidgets(array(
    'nombre'  => new sfWidgetFormInputText(),
    'email'   => new sfWidgetFormInput(array('default' => '[email protected]')),
    'asunto'  => new sfWidgetFormChoice(array('choices' => array('Asunto A', 'Asunto B', 'Asunto C'))),
    'mensaje' => new sfWidgetFormTextarea(),
  ));

  $this->form->setValidators(array(
    'nombre'  => new sfValidatorString(),
    'email'   => new sfValidatorEmail(),
    'asunto'  => new sfValidatorString(),
    'mensaje' => new sfValidatorString(array('min_length' => 4))
  ));

  // Procesar la petición
  if ($request->isMethod('post'))
  {
    $this->form->bind();
    if ($this->form->isValid())
    {
      // Procesar los datos del formulario

      ...

      $this->redirect('mimodulo/otraaccion');
    }
  }
}

El método setValidators() utiliza una sintaxis similar a la de setWidgets(). Las clases sfValidatorEmail y sfValidatorString son solamente dos de los muchos validadores que incluye Symfony y que se explican más adelante. La clase sfForm también proporciona el método setValidator() para definir los validadores uno a uno.

El método sfForm::bind() se emplea para introducir los datos enviados por el usuario dentro del objeto del formulario. Como se trata de unir los datos y el formulario, este proceso se denomina bind, que significa "unir" en inglés.

A continuación, el método isValid() comprueba si los datos cumplen las reglas de todos los validadores. Si este es el caso, el método isValid() devuelve true y la acción ya puede continuar con el procesado de los datos. Si el formulario no es válido, la acción termina su ejecución y devuelve el valor sfView::SUCCESS que hace que se vuelva a mostrar el formulario. Sin embargo, ahora ya no se muestra el formulario original, sino que se rellenan todos los campos con los datos introducidos por el usuario y se añaden los mensajes de error en aquellos campos que no cumplen las reglas de validación.

Invalid form

Figura 10.2 Invalid form

Nota El proceso de validación no se detiene al encontrar el primer error. El método isValid() procesa todos los datos enviados y comprueba todos los validadores de cada campo, para que el usuario no se encuentre con errores de validación cada vez que envía el formulario.

10.4.3. Utilizando datos de formulario limpios

El código de los ejemplos anteriores no especifica qué datos de la petición se unen al formulario. El problema es que la petición contiene muchos más datos que los del formulario. Entre otros, contiene las cabeceras, las cookies y los datos pasados como parámetros GET. Esta información ajena al formulario podría producir problemas en la fase bind del formulario. Por tanto, una buena práctica consiste en pasar al método bind() solamente los datos del formulario.

Afortunadamente, Symfony permite cambiar el nombre de todos los campos del formulario para que sigan la sintaxis de los arrays. Define el formato del nombre de los atributos con el método setNameFormat() dentro de la acción donde se crea el formulario:

// en modules/mimodulo/actions/actions.class.php
// Definir el formulario
...
$this->form->setNameFormat('contacto[%s]');

De esta forma, todos los campos del formulario generado tienen un nombre contacto[nombre-del-widget] en vez de simplemente nombre-del-widget:

<label for="contacto_nombre">Nombre</label>
<input type="text" name="contacto[nombre]" id="contacto_nombre" />
...
<label for="contacto_email">Email</label>
<input type="text" name="contacto[email]" id="contacto_email" value="[email protected]" />
...
<label for="contacto_asunto">Asunto</label>
<select name="contacto[asunto]" id="contacto_asunto">
  <option value="0">Asunto A</option>
  <option value="1">Asunto B</option>
  <option value="2">Asunto C</option>
</select>
...
<label for="contacto_mensaje">Mensaje</label>
<textarea rows="4" cols="30" name="contacto[mensaje]" id="contacto_mensaje"></textarea>

Ahora la acción puede obtener todos los datos del formulario mediante el parámetro contacto de la petición. Esta variable será un array con todos los datos introducidos por el usuario:

// en modules/mimodulo/actions/actions.class.php
// Procesar la petición
if ($request->isMethod('post'))
{
  $this->form->bind($request->getParameter('contacto'));
  if ($this->form->isValid())
  {
    // Procesar los datos del formulario
    $contacto = $this->form->getValues();
    $nombre = $contacto['nombre'];

    // Si quieres obtener un valor específico
    $nombre = $this->form->getValue('nombre');

    ...

    $this->redirect('mimodulo/otraaccion');
  }
}

Cuando método bind() recibe un array de parámetros, Symfony aplica automáticamente un mecanismo de seguridad que impide al usuario enviar más datos de los que contiene el formulario original. Por tanto, el proceso de validación produce un error si el array contacto contiene información de campos que no se encuentran en el formulario original.

Otra diferencia importante en el código del último ejemplo es que la acción no obtiene los datos directamente de la petición, sino que emplea el objeto del formulario ($form->getValues()). Los validadores filtran y limpian los datos que se les pasan, por lo que siempre es mejor confiar en los datos del formulario (mediante getValues() o getValue()) en vez de confiar en los datos de la petición. Además, si el widget es complejo (como por ejemplo los widgets de fechas), los datos devueltos por getValues() ya están correctamente formateados:

// Las fechas son widgets complejos formados por varios campos
<label for="contacto_fecha">Fecha de nacimiento</label>
<select id="contacto_fecha_month" name="contacto[fecha][month]">...</select> /
<select id="contacto_fecha_day" name="contacto[fecha][day]">...</select> /
<select id="contacto_fecha_year" name="contacto[fecha][year]">...</select>

// Para obtener la fecha, se deben procesar los campos en la acción
$contacto = $request->getParameter('contacto');
$mes = $contacto['fecha']['month'];
$dia = $contacto['fecha']['day'];
$ano = $contacto['fecha']['year'];
$fechaNacimiento = mktime(0, 0, 0, $mes, $dia, $ano);

// Si utilizas el método getValues(), obtienes la fecha directamente
$contacto = $this->form->getValues();
$fechaNacimiento = $contacto['fecha'];

Te aconsejamos que siempre utilices esta sintaxis de arrays para nombrar a los campos de tu formulario (mediante setNameFormat()) y que siempre obtengas los datos limpios a través del formulario (mediante getValues()).

10.4.4. Modificando los mensajes de error

Una de las imágenes anteriores mostraba el formulario con algunos mensajes de error. ¿De dónde han salido estos errores? Como se explicó anteriormente, cada widget está compuesto de cuatro elementos y el mensaje de error es uno de ellos. De hecho, el formateador de tabla que utiliza Symfony por defecto muestra cada campo de la siguiente forma:

<?php if ($field->hasError()): ?>
<tr>
  <td colspan="2">
    <?php echo $field->renderError() ?>           // Lista de errores
  </td>
</tr>
<?php endif; ?>
<tr>
  <th><?php echo $field->renderLabel() ?></th>    // Título
  <td>
    <?php echo $field->render() ?>                // Widget
    <?php if ($field->hasHelp()): ?>
    <br /><?php echo $field->renderHelp() ?>      // Mensaje de ayuda
    <?php endif; ?>
  </td>
</tr>

Haciendo uso de los métodos anteriores puedes modificar el aspecto y la posición de los mensajes de error de cada campo. También puedes mostrar un mensaje de error global en el formulario avisando al usuario de que al menos uno de sus campos no es válido:

<?php if ($form->hasErrors()): ?>
  El formulario contiene algunos errores que debes solucionar.
<?php endif; ?>

10.4.5. Modificando los validadores

Todos los campos del formulario deben tener asociado un validador y además, por defecto todos los campos son obligatorios. Si un campo del formulario es opcional, utiliza la opción required con un valor false en su validador. El siguiente ejemplo muestra cómo hacer obligatorio el campo name y opcional el campo email:

$this->form->setValidators(array(
  'nombre'  => new sfValidatorString(),
  'email'   => new sfValidatorEmail(array('required' => false)),
  'asunto'  => new sfValidatorString(),
  'mensaje' => new sfValidatorString(array('min_length' => 4))
));

Los campos también pueden tener más de un validador. El campo email podría por ejemplo validarse mediante sfValidatorEmail para comprobar que su formato es adecuado y mediante fValidatorString para asegurar que su longitud es de al menos cuatro caracteres. Para combinar dos validadores, se utiliza el validador sfValidatorAnd y se le pasan como argumentos tanto sfValidatorEmail como sfValidatorString:

$this->form->setValidators(array(
  'nombre'  => new sfValidatorString(),
  'email'   => new sfValidatorAnd(array(
    new sfValidatorEmail(),
    new sfValidatorString(array('min_length' => 4)),
  ), array('required' => false)),
  'asunto'  => new sfValidatorString(),
  'mensaje' => new sfValidatorString(array('min_length' => 4))
));

Si se cumplen las condiciones de los dos validadores, el campo email se considera válido. De la misma forma, existe un método llamado sfValidatorOr que permite combinar varios validadores. Si se cumple al menos uno de esos validadores, el campo se declara válido.

Cada validador que no se cumple genera un mensaje de error para el campo. Aunque estos mensajes están escritos en inglés, Symfony utiliza los helpers de internacionalización al mostrarlos por pantalla, por lo que puedes traducirlos fácilmente al idioma que quieras. Otra forma de traducir los mensajes de error es utilizar el tercer parámetro opcional de los validadores, que permite modificar sus mensajes de error. Todos los validadores cuentan con al menos dos mensajes de error: el mensaje required y el mensaje invalid. Algunos validadores definen más mensajes de error, que también se pueden redefinir mediante este tercer parámetro:

// en modules/mimodulo/actions/actions.class.php
$this->form->setValidators(array(
  'nombre'  => new sfValidatorString(),
  'email'   => new sfValidatorEmail(array(), array(
    'required'   => 'Introduce por favor un email',
    'invalid'    => 'Escribe por favor una dirección de email válida (por ej. [email protected])'
  )),
  'asunto'  => new sfValidatorString(),
  'mensaje' => new sfValidatorString(array('min_length' => 4), array(
    'required'   => 'Escribe por favor un mensaje',
    'min_length' => 'Escribe por favor un mensaje más largo (escribe al menos cuatro caracteres)'
  ))
));

Los mensajes de error propios también se muestran por pantalla mediante los helpers de internacionalización, por lo que se pueden traducir fácilmente a cualquier idioma (el Capítulo 13 lo explica en detalle).

10.4.6. Aplicando un mismo validador a varios campos

Con todo lo explicado anteriormente no es posible validar que varios campos diferentes cumplen una misma condición. Este caso es muy común por ejemplo en los formularios de registro de usuario, que suelen contender dos campos diferentes de contraseña cuyos valores deben coincidir. Cada contraseña es un campo independiente, pero su validación sólo es posible de forma combinada.

En estos casos se utiliza un validador múltiple en el método setPostValidator(), que es un validador que se ejecuta después de todos los demás validadores y que recibe un array con todos los valores limpios. Si quieres validar los datos originales, debes utilizar el método setPreValidator().

Un formulario de registro típico tendría el siguiente aspecto:

// en modules/mimodulo/actions/actions.class.php
// Definir el formulario
$this->form = new sfForm();
$this->form->setWidgets(array(
  'login'     => new sfWidgetFormInputText(),
  'password1' => new sfWidgetFormInputText(),
  'password2' => new sfWidgetFormInputText()
);

$this->form->setValidators(array(
  'login'     => new sfValidatorString(), // login es obligatorio
  'password1' => new sfValidatorString(), // password1 es obligatorio
  'password2' => new sfValidatorString(), // password2 es obligatorio
));

$this->form->setPostValidators(new sfValidatorSchemaCompare('password1', '==', 'password2'));

El validador sfValidatorSchemaCompare es un tipo especial de validador múltiple que recibe todos los valores limpios y puede elegir dos de ellos para compararlos. Si quieres definir más de un post-validador, puedes hacer uso de los métodos sfValidatorAnd y sfValidatorOr.