Symfony 1.0, la guía definitiva

10.3. Validación de formularios

En el Capítulo 6 se explica cómo utilizar los métodos validateXXX() en las acciones para validar los parámetros de la petición. Sin embaro, si se utiliza este método para validar los datos enviados en un formulario, se acaba escribiendo una y otra vez los mismos o parecidos trozos de código. Symfony incluye un mecanismo específico de validación de formularios realizado mediante archivos YAML, en vez de utilizar código PHP en la acción.

Para mostrar el funcionamiento de la validación de formularios, se va a utilizar el formulario del listado 10-17. Se trata del típico formulario de contacto que incluye los campos nombre, email, edad y mensaje.

Listado 10-17 - Ejemplo de formulario de contacto, en modules/contacto/templates/indexSuccess.php

<?php echo form_tag('contacto/enviar') ?>
  Nombre:  <?php echo input_tag('nombre') ?><br />
  Email:   <?php echo input_tag('email') ?><br />
  Edad:    <?php echo input_tag('edad') ?><br />
  Mensaje: <?php echo textarea_tag('mensaje') ?><br />
  <?php echo submit_tag() ?>
</form>

El funcionamiento básico de la validación en un formulario es que si el usuario introduce datos no válidos y envía el formulario, la próxima página que se muestra debería contener los mensajes de error. La siguiente lista explica con palabras sencillas lo que se consideran datos válidos en el formulario de prueba:

  • El campo nombre es obligatorio. Debe ser una cadena de texto de entre 2 y 100 caracteres.
  • El campo email es obligatorio. Debe ser una cadena de texto de entre 2 y 100 caracteres y debe contener una dirección de email válida.
  • El campo edad es obligatorio. Debe ser un número entero entre 0 y 120.
  • El campo mensaje es obligatorio.

Se podrían definir reglas de validación más complejas para el formulario de contacto, pero de momento solo es un ejemplo para mostrar las posibilidades de la validación de formularios.

Nota La validación de formularios se puede realizar en el lado del servidor y/o en el lado del cliente. La validación en el servidor es obligatoria para no corromper la base de datos con datos incorrectos. La validación en el lado del cliente es opcional, pero mejora enormemente la experiencia de usuario. La validación en el lado del cliente debe realizarse de forma manual con JavaScript.

10.3.1. Validadores

Los campos nombre y email del formulario de ejemplo comparten las mismas reglas de validación. Como algunas de las reglas de validación son tan comunes que aparecen en todos los formularios, Symfony ha creado unos validadores que encapsulan todo el código PHP necesario para realizarlos. Un validador es una clase que proporciona un método llamado execute(). El método requiere de un parámetro que es el valor del campo de formulario y devuelve true si el valor es válido y false en otro caso.

Symfony incluye varios validadores ya construidos (que se describen más adelante en la sección "Validadores estándar de Symfony") aunque ahora solo se va a estudiar el validador sfStringValidator. Este validador comprueba que el valor introducido es una cadena de texto y que su longitud se encuentra entre 2 límites indicados (definidos cuando se llama al método initialize()). Este validador es justo lo que se necesita para validar el campo nombre. El listado 10-18 muestra cómo utilizar este validador en un método de validación.

Listado 10-18 - Validando parámetros de la petición con validadores reutilizables, en modules/contacto/action/actions.class.php

public function validateEnviar()
{
  $nombre = $this->getRequestParameter('nombre');

  // El campo 'nombre' es obligatorio
  if (!$nombre)
  {
    $this->getRequest()->setError('nombre', 'El campo nombre no se puede dejar vacío');

    return false;
  }

  // El campo nombre debe ser una cadena de texto de entre 2 y 100 caracteres
  $miValidador = new sfStringValidator();
  $miValidador->initialize($this->getContext(), array(
    'min'       => 2,
    'min_error' => 'El nombre es muy corto (mínimo 2 caracteres)',
    'max'       => 100,
    'max_error' => 'El nombre es muy largo (máximo 100 caracteres)',
  ));
  if (!$miValidador->execute($nombre, $error))
  {
    return false;
  }

  return true;
}

Si un usuario envía el formulario del listado 10-17 con el valor a en el campo nombre, el método execute() de sfStringValidator devuelve un valor false (porque la longitud de la cadena de texto es menor que el mínimo de 2 caracteres). El método validateSend() devolverá false y se ejecutará el método handleErrorEnviar() en vez del método executeEnviar().

Truco El método setError() del objeto sfRequest proporciona información a la plantilla para que se puedan mostrar los mensajes de error, como se explica más adelante en la sección "Mostrando mensajes de error en el formulario". Los validadores establecen los errores de forma interna, por lo que se pueden definir diferentes errores para los diferentes casos de error en la validación. Este es precisamente el objetivo de los parámetros min_error y max_error de inicialización de sfStringValidator.

Las reglas de validación definidas anteriormente se pueden traducir en validadores:

  • nombre: sfStringValidator (min=2, max=100)
  • email: sfStringValidator (min=2, max=100) y sfEmailValidator
  • edad: sfNumberValidator (min=0, max=120)

El hecho de que un campo sea requerido no es algo que se controle mediante un validador.

10.3.2. Archivo de validación

Aunque se podría realizar de forma sencilla la validación del formulario de contacto mediante los validadores en el método validateEnviar(), esta forma de trabajo supondría repetir mucho código PHP. Symfony ofrece una alternativa mucho mejor para definir las reglas de validación de un formulario, mediante el uso de archivos YAML. El listado 10-19 muestra por ejemplo como realizar la misma validación que el listado 10-18 pero mediante un archivo de validación.

Listado 10-19 - Archivo de validación, en modules/contacto/validate/enviar.yml

fields:
  name:
    required:
      msg:       El campo nombre no se puede dejar vacío
    sfStringValidator:
      min:       2
      min_error: El nombre es muy corto (mínimo 2 caracteres)
      max:       100
      max_error: El nombre es m uy largo (máximo 100 caracteres)

En el archivo de validación, la clave fields define la lista de campos que tienen que ser validados, si son requeridos o no y los validadores que deben utilizarse para comprobar su validez. Los parámetros de cada validador son los mismos que se utilizan para inicializar manualmente los validadores. Se pueden utilizar tantos validadores como sean necesarios sobre un mismo campo de formulario.

Nota El proceso de validación no termina cuando el validador falla. Symfony ejecuta todos los validadores y determina que la validación ha fallado si al menos uno de ellos falla. Incluso cuando algunas de las reglas de validación fallan, Symfony busca el método validateXXX() y lo ejecuta. De esta forma, las 2 técnicas de validación son complementarias. La gran ventaja es que si un formulario tiene muchos errores, se muestran todos los mensajes de error.

Los archivos de validación se encuentran en el directorio validate/ del módulo y su nombre se corresponde con el nombre de la acción que validan. El listado 10-19 por ejemplo se debe guardar en un archivo llamado validate/enviar.yml.

10.3.3. Mostrando el formulario de nuevo

Cuando la validación falla, Symfony por defecto busca un método handleErrorEnviar() en la clase de la acción o muestra la plantilla enviarError.php si el método no existe.

El procedimiento habitual para informar al usuario de que la validación ha fallado es el de volver a mostrar el formulario con los mensajes de error. Para ello, se debe redefinir el método handleErrorSend() para finalizar con una redirección a la acción que muestra el formulario (en este caso module/index) tal y como muestra el listado 10-20.

Listado 10-20 - Volviendo a mostrar el formulario, en modules/contacto/actions/actions.class.php

class ContactoActions extends sfActions
{
  public function executeIndex()
  {
    // Mostrar el formulario
  }

  public function handleErrorEnviar()
  {
    $this->forward('contacto', 'index');
  }

  public function executeEnviar()
  {
    // Procesar el envío del formulario
  }
}

Si se utiliza la misma acción para mostrar el formulario y para procesarlo, el método handleErrorEnviar() puede devolver el valor sfView::SUCCESS para volver a mostrar el formulario, como se indica en el listado 10-21.

Listado 10-21 - Una sola acción para mostrar y procesar el formulario, en modules/contacto/actions/actions.class.php

class ContactoActions extends sfActions
{
  public function executeEnviar()
  {
    if ($this->getRequest()->getMethod() != sfRequest::POST)
    {
      // Preparar los datos para la plantilla

      // Mostrar el formulario
      return sfView::SUCCESS;
    }
    else
    {
      // Procesar el formulario
      ...
      $this->redirect('mimodulo/otraaccion');
    }
  }
  public function handleErrorEnviar()
  {
    // Preparar los datos para la plantilla

    // Mostrar el formulario
    return sfView::SUCCESS;
  }
}

La lógica que se emplea para preparar los datos del formulario se puede refactorizar en un método de tipo protected de la clase de la acción, para evitar su repetición en los métodos executeSend() y handleErrorSend().

Con esta nueva configuración, cuando el usuario introduce un nombre inválido, se vuelve a mostrar el formulario pero los datos introducidos se pierden y no se muestran los mensajes de error. Para arreglar este último problema, se debe modificar la plantilla que muestra el formulario para insertar los mensajes de error cerca del campo que ha provocado el error.

10.3.4. Mostrando los mensajes de error en el formulario

Cuando un campo del formulario no supera con éxito su validación, los mensajes de error definidos como parámetros del validador se añaden a la petición (de la misma forma que se añadían manualmente mediante el método setError() en el listado 10-18). El objeto sfRequest proporciona un par de métodos útiles para obtener el mensaje de error: hasError() y getError(), cada uno de los cuales espera como argumento el nombre de un campo de formulario. Además, se puede mostrar un mensaje de aviso al principio del formulario para llamar la atención del usuario e indicarle que el formulario contiene errores mediante el método hasErrors(). Los listados 10-22 y 10-23 muestran cómo utilizar estos métodos.

Listado 10-22 - Mostrando mensajes de error al principio del formulario, en templates/indexSuccess.php

<?php if ($sf_request->hasErrors()): ?>
  <p>Los datos introducidos no son correctos.
  Por favor, corrija los siguientes errores y vuelva a enviar el formulario:</p>
  <ul>
  <?php foreach($sf_request->getErrors() as $nombre => $error): ?>
    <li><?php echo $nombre ?>: <?php echo $error ?></li>
  <?php endforeach; ?>
  </ul>
<?php endif; ?>

Listado 10-23 - Mostrando mensajes de error dentro del formulario, en templates/indexSuccess.php

<?php echo form_tag('contacto/enviar') ?>
  <?php if ($sf_request->hasError('nombre')): ?>
    <?php echo $sf_request->getError('nombre') ?> <br />
  <?php endif; ?>
  Nombre:    <?php echo input_tag('nombre') ?><br />
  ...
  <?php echo submit_tag() ?>
</form>

La condición utilizada antes del método getError() en el listado 10-23 es un poco larga de escribir. Por este motivo, Symfony incluye un helper llamado form_error() y que puede sustituirlo. Para poder utilizarlo, es necesario declarar de forma explícita el uso de este grupo de helpers llamado Validation. El listado 10-24 modifica al listado 10-23 para utilizar este helper.

Listado 10-24 - Mostrando mensajes de error dentro del formulario, forma abreviada

<?php use_helper('Validation') ?>
<?php echo form_tag('contacto/enviar') ?>

           <?php echo form_error('nombre') ?><br />
  Nombre:  <?php echo input_tag('nombre') ?><br />
  ...
  <?php echo submit_tag() ?>
</form>

El helper form_error() añade por defecto un carácter antes y después del mensaje de error para hacerlos más visibles. Por defecto, el carácter es una flecha que apunta hacia abajo (correspondiente a la entidad &darr;), pero se puede definir otro carácter en el archivo settings.yml:

all:
  .settings:
    validation_error_prefix:    ' &darr;&nbsp;'
    validation_error_suffix:    ' &nbsp;&darr;'

Si ahora falla la validación, el formulario muestra correctamente los mensajes de error, pero los datos introducidos por el usuario se pierden. Para mejorar el formulario es necesario volver a mostrar los datos que introdujo anteriormente el usuario.

10.3.5. Mostrando de nuevo los datos introducidos

Como los errores se manejan mediante el método forward() (como se muestra en el listado 10-20), la petición original sigue siendo accesible y por tanto los datos introducidos por el usuario se encuentran en forma de parámetros de la petición. De esta forma, es posible mostrar los datos introducidos en el formulario utilizando los valores por defecto, tal y como se muestra en el listado 10-25.

Listado 10-25 - Indicando valores por defecto para mostrar los datos introducidos por el usuario anteriormente después de un fallo en la validación, en templates/indexSuccess.php

<?php use_helper('Validation') ?>
<?php echo form_tag('contacto/enviar') ?>
            <?php echo form_error('nombre') ?><br />
  Nombre:   <?php echo input_tag('nombre', $sf_params->get('nombre')) ?><br />
            <?php echo form_error('email') ?><br />
  Email:    <?php echo input_tag('email', $sf_params->get('email')) ?><br />
            <?php echo form_error('edad') ?><br />
  Edad:     <?php echo input_tag('edad', $sf_params->get('edad')) ?><br />
            <?php echo form_error('mensaje') ?><br />
  Mensaje: <?php echo textarea_tag('mensaje', $sf_params->get('mensaje')) ?><br />
  <?php echo submit_tag() ?>
</form>

Una vez más, se trata de un mecanismo bastante tedioso de escribir. Symfony ofrece una alternativa para volver a mostrar los datos de todos los campos de un formulario. Esta alternativa se realiza mediante el archivo YAML de validación y no mediante la modificación de los valores por defecto de los elementos. Solamente es necesario activar la opción fillin: del formulario, con la sintaxis descrita en el listado 10-26.

Listado 10-26 - Activando la opción fillin para volver a mostrar los datos del formulario cuando la validación falla, en validate/enviar.yml

fillin:
  enabled: true  # Habilita volver a mostrar los datos
  param:
    name: prueba   # Nombre del formulario (no es necesario indicarlo si solo hay 1 formulario en la página)
    skip_fields:   [email]  # No mostrar los datos introducidos en estos campos
    exclude_types: [hidden, password] # No mostrar los campos de estos tipos
    check_types:   [text, checkbox, radio, select, hidden] # Muestra los datos de estos tipos de campos
    content_type:  html  # html es el formato por defecto. Las otras opciones son xml y xhtml (esta última es igual que XML, salvo que no se incluye la declaración XML)

Por defecto, se vuelven a mostrar los datos de los campos de tipo cuadro de texto, checkbox, radio button, áreas de texto y listas desplegables (sencillas y múltiples). No se vuelven a mostrar los datos en los campos de tipo contraseña y en los campos ocultos. Además, la opción fillin no funciona para los campos utilizados para adjuntar archivos.

Nota La opción fillin funciona procesando el contenido XML de la respuesta antes de enviarla al usuario. Por defecto los datos se vuelven a mostrar en formato HTML.

Si necesitas mostrar los datos en formato XHTML, la opción content-type debe valer xml. Además, si la respuesta no es un documento XHTML estrictamente válido, la opción fillin puede que no funcione.

El tercer valor posible de la opción content_type es xhtml, que es idéntico a xml, salvo que no incluye la declaración de los archivos XML, lo que evita que se active el modo quirks en el navegador Internet Explorer 6.

Antes de volver a mostrar los datos introducidos por el usuario, puede ser necesario modificar sus valores. A los campos del formulario se les pueden aplicar mecanismos de escape, reescritura de URL, transformación de caracteres especiales en entidades y cualquier otra transformación que se pueda llevar a cabo llamando a una función. Las conversiones se definen bajo la clave converters:, como muestra el listado 10-27.

Listado 10-27 - Convirtiendo los datos del usuario antes del fillin, en validate/enviar.yml

fillin:
  enabled: true
  param:
    name: prueba
    converters:         # Conversiones aplicadas
     htmlentities:     [nombre, comentarios]
     htmlspecialchars: [comentarios]

10.3.6. Validadores estándar de Symfony

Symfony contiene varios validadores ya definidos y que se pueden utilizar directamente en los formularios:

  • sfStringValidator
  • sfNumberValidator
  • sfEmailValidator
  • sfUrlValidator
  • sfRegexValidator
  • sfCompareValidator
  • sfPropelUniqueValidator
  • sfFileValidator
  • sfCallbackValidator

Cada uno dispone de una serie de parámetros y de mensajes de error, pero se pueden redefinir fácilmente mediante el método initialize() del validador o mediante el archivo YAML. Las siguientes secciones describen los validadores y muestran ejemplos de su uso.

10.3.6.1. Validador de cadenas de texo

sfStringValidator permite establecer una serie de restricciones relacionadas con las cadenas de texto.

sfStringValidator:
  values:       [valor1, valor2]
  values_error: Los únicos valores aceptados son valor1 y valor2
  insensitive:  false  # Si vale true, la comparación con los valores no tiene en cuenta mayúsculas y minúsculas
  min:          2
  min_error:    Por favor, introduce por lo menos 2 caracteres
  max:          100
  max_error:    Por favor, introduce menos de 100 caracteres

10.3.6.2. Validador de números

sfNumberValidator verifica si un parámetro es un número y permite establecer una serie de restricciones sobre su valor.

sfNumberValidator:
  nan_error:    Por favor, introduce un número entero
  min:          0
  min_error:    El valor debe ser como mínimo 0
  max:          100
  max_error:    El valor debe ser inferior o igual a 100

10.3.6.3. Validador de email

sfEmailValidator verifica si el valor de un parámetro es una dirección válida de email.

sfEmailValidator:
  strict:       true
  email_error:  Esta dirección de email no es válida

La recomendación RFC822 define el formato de las direcciones de correo electrónico. No obstante, el formato válido es mucho más permisivo que el de las direcciones habituales de email. Según la recomendación, un email como yo@localhost es una dirección válida, aunque es una dirección que seguramente será poco útil. Si se establece la opción strict a true (que es su valor por defecto) solo se consideran válidas las direcciones de correo electrónico con el formato [email protected]. Si la opción strict vale false, se utilizan las normas de la recomendación RFC822.

10.3.6.4. Validador de URL

sfUrlValidator comprueba si el valor de un campo es una URL válido.

sfUrlValidator:
  url_error:    La URL no es válida

10.3.6.5. Validador de expresiones regulares

sfRegexValidator permite comprar el valor de un campo con una expresión regular compatible con Perl.

sfRegexValidator:
  match:        No
  match_error:  Los comentarios con más de una URL se consideran spam
  pattern:      /http.*http/si

El parámetro match determina si el parámetro debe cumplir el patrón establecido (cuando vale Yes) o no debe cumplirlo para considerarse válido (cuando vale No).

10.3.6.6. Validador para comparaciones

sfCompareValidator comprueba si dos parámetros de la petición son iguales. Su mayor utilidad es para comparar dos contraseñas.

fields:
  password1:
    required:
      msg:      Por favor, introduce una contraseña
  password2:
    required:
      msg:      Por favor, vuelve a introducir la contraseña
    sfCompareValidator:
      check:    password1
      compare_error: Las 2 contraseñas son diferentes

El parámetro check contiene el nombre del campo cuyo valor debe coincidir con el valor del campo actual para considerarse válido.

10.3.6.7. Validador Propel para valores únicos

sfPropelUniqueValidator comprueba que el valor de un parámetro de la petición no existe en la base de datos. Se trata de un validador realmente útil para las columnas que deben ser índices únicos.

fields:
  nombre:
    sfPropelUniqueValidator:
      class:        Usuario
      column:       login
      unique_error: Ese login ya existe. Por favor, seleccione otro login.

En este ejemplo, el validador busca en la base de datos los registros correspondientes a la clase Usuario y comprueba si alguna fila tiene en su columna login el mismo valor que el parámetro que se pasa al validador.

Nota El validador sfPropelUniqueValidator puede sufrir problemas de tipo "condición de carrera" race condition). Aunque la probabilidad de que ocurra es muy baja, en un entorno multiusuario, el resultado puede cambiar justo cuando se devuelve su valor. Por este motivo, la aplicación debe estar preparada para tratar los errores que se producen con INSERT duplicados.

10.3.6.8. Validador de archivos

sfFileValidator permite restringir el tipo (mediante un array de mime-types) y el tamaño de los archivos subidos por el usuario.

fields:
  image:
    required:
      msg:      Por favor, sube un archivo de imagen
    file:       True
    sfFileValidator:
      mime_types:
        - 'image/jpeg'
        - 'image/png'
        - 'image/x-png'
        - 'image/pjpeg'
      mime_types_error: Solo se permiten los formatos PNG y JPEG
      max_size:         512000
      max_size_error:   El tamaño máximo es de 512Kb

El atributo file debe valer True para ese campo y el formulario de la plantilla debe declararse de tipo multipart.

10.3.6.9. Validador de callback

sfCallbackValidator delega la validación en un método o función externa. El método que se invoca debe devolver true o false como resultado de la validación.

fields:
  numero_cuenta:
    sfCallbackValidator:
      callback:      is_numeric
      invalid_error: Por favor, introduce un número.
  numero_tarjeta_credito:
    sfCallbackValidator:
      callback:      [misUtilidades, validarTarjetaCredito]
      invalid_error: Por favor, introduce un número correcto de tarjeta de crédito.

El método o función que se llama recibe como primer argumento el valor que se debe comprobar. Se trata de un método muy útil cuando se quieren reutilizar los métodos o funciones existentes en vez de tener que volver a crear un código similar para la validación.

Nota También es posible crear validadores propios, como se describe más adelante en la sección "Creando validadores propios".

10.3.7. Validadores con nombre

Si se utilizan de forma constante las mismas opciones para un validador, se pueden agrupar bajo un validador con nombre. En el ejemplo del formulario de contacto, el campo email requiere las mismas opciones en sfStringValidator que el campo name. De esta forma, es posible crear un validador con nombre miStringValidator para evitar tener que repetir las mismas opciones. Para ello, se añade una etiqueta miStringValidator bajo la clave validators:, y se indica la class y los param del validador que se quiere utilizar. Después, este validador ya se puede utilizar como cualquier otro validador indicando su nombre en la sección fields, como se muestra en el listado 10-28.

Listado 10-28 - Reutilizando validadores con nombre en un archivo de validación, en validate/enviar.yml

validators:
  miStringValidator:
    class: sfStringValidator
    param:
      min:       2
      min_error: Este campo es demasiado corto (mínimo 2 caracteres)
      max:       100
      max_error: Este campo es demasiado largo (mínimo 100 caracteres)

fields:
  nombre:
    required:
      msg:       El nombre no se puede dejar vacío
    miStringValidator:
  email:
    required:
      msg:       El email no se puede dejar vacío
    miStringValidator:
    sfEmailValidator:
      email_error:  La dirección de email no es válida

10.3.8. Restringiendo la validación a un método

Por defecto, los validadores indicados en el archivo de validación se ejecutan cuando la acción se llama mediante un método POST. Se puede redefinir esta opción de forma global o campo a campo especificando otro valor en la clave methods, de forma que se pueda utilizar una validación diferente para métodos diferentes, como muestra el listado 10-29.

Listado 10-29 - Definiendo cuando se valida un campo, en validate/enviar.yml

methods:         [post]     # Opción por defecto

fields:
  nombre:
    required:
      msg:       El nombre no se puede dejar vacío
    miStringValidator:
  email:
    methods:     [post, get] # Redefine la opción global
    required:
      msg:       El email no se puede dejar vacío
    miStringValidator:
    sfEmailValidator:
      email_error:  La dirección de email no es válida

10.3.9. ¿Cuál es el aspecto de un archivo de validación?

Hasta ahora solamente se han mostrado partes del archivo de validación. Cuando se juntan todas las partes, las reglas de validación se pueden definir de forma sencilla en el archivo YAML. El listado 10-30 muestra el archivo de validación completo para el formulario de contacto, incluyendo todas las reglas definidas anteriormente.

Listado 10-30 - Ejemplo de archivo de validación completo

fillin:
  enabled:      true

validators:
  miStringValidator:
    class: sfStringValidator
    param:
      min:       2
      min_error: Este campo es demasiado corto (mínimo 2 caracteres)
      max:       100
      max_error: Este campo es demasiado largo (máximo 100 caracteres)

fields:
  nombre:
    required:
      msg:       El nombre no se puede dejar vacío
    miStringValidator:
  email:
    required:
      msg:       El email no se puede dejar vacío
    myStringValidator:
    sfEmailValidator:
      email_error:  La dirección de email no es válida
  edad:
    sfNumberValidator:
      nan_error:    Por favor, introduce un número
      min:          0
      min_error:    "Aun no has nacido, ¿cómo vas a enviar un mensaje?"
      max:          120
      max_error:    "Abuela, ¿no es usted un poco mayor para navegar por Internet?"
  mensaje:
    required:
      msg:          El mensaje no se puede dejar vacío