Validación de datos

Como ya hemos visto cada tabla de nuestra base de datos es representada por medio de un Entity en el que usamos annotations para definir los metadatos. Para usar estos annotations importamos el paquete “use Doctrine\ORM\Mapping as ORM;” arriba del archivo y usamos los mismos por medio del alias ORM de esta manera “@ORM\Id”.

Para la validación de los datos importaremos otro namespace: “use Symfony\Component\Validator\Constraints as Assert;” y por medio del alias Assert, utilizando annotations definiremos los validadores para los campos que queramos. El listado completo de validadores o también llamados constraints los puedes ver en la documentación oficial.

Algo que resulta muy importante entender es que la definición de los metadatos que escribimos usando el alias @ORM no tiene el mismo propósito que cuando usamos el @Assert. Por ejemplo, en el caso de nuestro Entity Article, hemos definido que la propiedad (campo/columna) $title no permite nulos. Esto lo hicimos porque dejamos su mapeo [email protected]\Column(name=”title”, type=”string”, length=255) donde por defecto es not null pero esto no implica la validación de los datos ya que lo que acabamos de escribir es que para la creación de la tabla se debe tener en cuenta que no es nulo y esto sirve para generar correctamente el CREATE TABLE necesario.

Para asegurarnos de que no sea nulo a la hora de ingresar los datos debemos usar [email protected]\NotNull() cuyo objetivo es realmente decirle al validador de datos que efectivamente al intentar grabar datos por medio del entity este debe ser validado como que no permite valores nulos.

Estos annotations tienen la misma forma que los anteriores. Son llamadas a métodos que pueden recibir parámetros opcionales. Por ejemplo, si ponemos:

/**
* @var string $title
*
* @ORM\Column(name="title", type="string", length=255)
* @Assert\NotNull()
*/
private $title;

estamos diciendo que la propiedad title no debe permitir valores nulos y al momento de validarlos saldrá una mensaje diciendo eso en ingles, si queremos cambiar el mensaje por defecto lo hacemos agregando el argumento a la invocación de esta manera:

/**
 * @var string $title
 *
 * @ORM\Column(name="title", type="string", length=255)
 * @Assert\NotNull(message="Debe escribir un titulo")
 */
 private $title;

Si quisiéramos validar que un campo debe ser de tipo email usaremos el annotation @Assert\Email() de esta manera:

/**
 * @Assert\Email(
 *     message = "El mail '{{ value }}' ingresado no tiene el formato correcto.",
 * )
 */
 protected $email;

Haciendo referencia a {{ value }} va a mostrar el valor ingresado como parte del mensaje.

Como último ejemplo, si quisiéramos validar la máxima cantidad de caracteres ingresados, podríamos usar [email protected]\MaxLength():

/**
 * @var string $title
 *
 * @ORM\Column(name="title", type="string", length=255)
 * @Assert\NotNull(message="Debe escribir un titulo")
 * @Assert\MaxLength(255)
 */
 private $title;

Y si quisiéramos además de la máxima cantidad de caracteres, controlar la mínima cantidad simplemente lo agregamos también:

/**
 * @var string $title
 *
 * @ORM\Column(name="title", type="string", length=255)
 * @Assert\NotNull(message="Debe escribir un titulo")
 * @Assert\MaxLength(255)
 * @Assert\MinLength(5)
 */
 private $title;

Con esto ya estamos controlando que mínimamente debemos escribir 5 caracteres en el título y como máximo 255.
[tipexperto titulo = “Nota”]Cuando usamos el @Assert\MaxLength(), la cantidad de caracteres que permitimos debe ser menor o igual al length definido en el @ORM\Column() ya que de lo contrario la aplicación dejaría pasar valores mayores y al llegar a la base de datos nos devolvería un error pero del motor de datos.[/tipexperto]

Como ya había mencionado más arriba, en la documentación oficial tenemos los constraints soportados y si damos click sobre cada uno de ellos veremos como  se utilizan con un ejemplo. Entre ellos encontrarán NotNull, NotBlank, Email, MinLength y MaxLength (para cantidad de caracteres), Max y Min (para valores numéricos), Date, DateTime, Time, Choice (para campos que serán ingresados por medio de un combo de valores por ejemplo).

Al escribir los Asserts en realidad estamos configurando las validaciones que queremos tener pero para validar los datos debemos invocar al validador. Para esto usemos como base el ejemplo que teníamos para la inserción de artículos del capítulo anterior:

public function crearAction()
{
    $articulo = new Articles();
    $articulo->setTitle('Articulo de ejemplo 1');
    $articulo->setAuthor('John Doe');
    $articulo->setContent('Contenido');
    $articulo->setTags('ejemplo');
    $articulo->setCreated(new \DateTime());
    $articulo->setUpdated(new \DateTime());
    $articulo->setSlug('articulo-de-ejemplo-1');
    $articulo->setCategory('ejemplo');

    $em = $this->getDoctrine()->getEntityManager();
    $em->persist($articulo);
    $em->flush();

    return $this->render('MDWDemoBundle:Articulos:articulo.html.twig', array('articulo' => $articulo));
 }

En código anterior, sin validar nada y pasando por alto los constraints de nuestro Entity intenta grabar los datos y si por ejemplo no cargamos un dato obligatorio como el título, el error devuelto será el que la misma base de datos valida ya que la columna fue creada como not null pero lo que queremos es poder obtener la validación en la aplicación, antes que llegue el insert a la base de datos, y esto lo haremos por medio del validador agregando el siguiente código antes de la invocación al EntityManager:

$errores = $this->get('validator')->validate($articulo);

Por medio del objeto $this->get(‘validator’) le decimos que valide la entidad $articulo, quién ya sabe como validarse por si misma ya que los annotations están dentro de la misma. Este método validate() nos devolverá un array de errores que podemos iterar y obtenerlos por medio del método getMessage():

public function crearAction()
{
    $articulo = new Articles();
    //-- No cargamos el dato para title
    $articulo->setAuthor('John Doe');
    $articulo->setContent('Contenido');
    $articulo->setTags('ejemplo');
    $articulo->setCreated(new \DateTime());
    $articulo->setUpdated(new \DateTime());
    $articulo->setSlug('articulo-de-ejemplo-1');
    $articulo->setCategory('ejemplo');

    $errores = $this->get('validator')->validate($articulo);

    if(!empty($errores))
    {
        foreach($errores as $error)
        echo $error->getMessage();

        return new Response();
    }

    $em = $this->getDoctrine()->getEntityManager();
    $em->persist($articulo);
    $em->flush();

    return $this->render('MDWDemoBundle:Articulos:articulo.html.twig', array('articulo' => $articulo));
 }

En el código de arriba hemos borrado la línea del setTitle(). Esto nos mostrará en pantalla el mensaje “Debe escribir un titulo” y si tenemos más errores los mensajes por cada error.

Ahora bien, realmente no tiene mucho sentido mostrar de esta manera los mensajes de errores ya que finalmente ni siquiera cargamos los datos a mano como lo estamos haciendo, sino que son ingresados por un formulario y es aquí donde pasamos al siguiente tema, la creación de formularios.

Creación de Formularios

La posibilidad de crear formularios es uno de los temas que más me gusta de Symfony ya que los mismos no se escriben en HTML sino que son programados como objetos y el mismo framework se encarga de hacer render del HTML necesario y asegurándonos que serán escritos de la mejor manera posible incluso utilizando las validaciones de HTML5.

Un formulario siempre debería ser representado por un objeto que se lo conoce como Type. Este objeto hace referencia a otro que puede ser un Entity (del que ya hablamos en los capítulos anteriores) o un POPO (Plain Old PHP Object).
[tipexperto titulo = “Nota”]Un POPO (Plain Old PHP Object) es simplemente una clase con propiedades y métodos tradicionales, es decir que es muy parecido a los Entities pero sin los annotations. Por ejemplo en caso de ser una clase que representará a un formulario para contacto donde tendremos simplemente una propiedad asunto, email, nombre y texto con sus respectivos setters y getters.[/tipexperto]

Un objeto Type se debe tomar como la definición del formulario. Este objeto recibirá cual es el Entity o POPO en el cual se almacenan los datos cargados en el formulario. Podríamos tener más de un Type para un mismo objeto ya que dependiendo de ciertos perfiles por ejemplo, podríamos querer mostrar algunos campos u otros dependiendo de que el usuario sea operador normal o administrador.

Definición de nuestro formulario

Para nuestro ejemplo tomaremos en cuenta el Entity Article que venimos usando y crearemos un objeto Type para representar a este formulario. Los formularios se deben crear dentro de nuestro Bundle en una carpeta Form por lo que crearemos el archivo ArticleType dentro de nuestra carpeta src/MDW/DemoBundle/Form:

[tipexperto titulo = “Actualización”] Al momento de realizar la guía se trabajó en la versión 2.0, para quienes trabajan en la versión 2.1 hay una modificación al realizar la definición de nuestro formulario:

use Symfony\Component\Form\FormBuilderInterface;
public function buildForm(FormBuilderInterface $builder, array $options)

Ya no se debe usar la clase FormBuilder sino la interfaz FormBuilderInterface.

[/tipexperto]

<?php
namespace MDW\DemoBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class ArticleType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('title')
                ->add('author')
                ->add('created');
    }
    public function getName()
    {
        return 'article_form';
    }
}

Como podemos ver en el ejemplo, el nombre de la clase esta formado por un nombre que yo he elegido concatenado con el sufijo Type y debe heredar de AbstractType para contener las funcionalidades base.

En el método buildForm(), por medio del $builder, que se recibe como argumento, agregamos los campos que vamos a usar. Estos campos son los nombres de los campos que tendrá el formulario y deben coincidir con las propiedades de nuestro Entity Article aunque todavía no hemos dicho que el formulario representará a Article ya que eso lo hacemos en la invocación desde el controller. El argumento options nos servirá para crear el formulario con otras opciones de personalización.

El método getName() deberá retornar el identificador de nuestro formulario y este String puede tomar el nombre que queramos siempre y cuando sea único. Tenemos que tener en cuenta que este nombre será usado para los atributos “name” de los componentes de formularios. Por ejemplo vemos que tenemos un componente llamado “title” que hemos agregado en el método buildForm() por lo que la etiqueta creada será:

<input type="text" name="article_form[title]" />

Hay que notar que esta es la sintaxis para cargar datos en un array (usando los corchetes) por lo que “article_form” será simplemente un array con una clave asociativa “title” que contendrá el valor ingresado por el usuario. Esta sintaxis nos permite tener en un array todos los datos del formulario al hacer el submit.

Con esto lo que hemos hecho es crear la representación básica de nuestro formulario, diciéndole cual es el identificador del formulario y los campos que deberá contener.

[tipexperto titulo = “Nota”]Escribiendo los objetos Type NO definimos como será visualmente el formulario sino como será CONCEPTUALMENTE.[/tipexperto]

Invocación y Visualización del formulario

Para poder mostrar el formulario HTML en nuestra página debemos invocar a nuestra clase ArticleType desde nuestro controlador, o más específicamente desde el action que llama a nuestra página, para esto vamos a crear un action nuevo dentro de nuestro ArticulosController al que vamos a llamar newAction.

Primeramente creamos nuestra ruta en el archivo routing.yml

articulo_new:
 pattern: /articulo/new
 defaults: { _controller: MDWDemoBundle:Articulos:new }

Una vez creada nuestra ruta iremos a crear nuestro newAction en src\MDW\DemoBundle\Controller\ArticulosController.php (MDWDemoBundle:Articulos:new). Para esto agregamos el siguiente código:

//--  Al estar utilizando la clase ArticleType dentro de nuestro método no debemos olvidar importar el namespace al principio del archivo
use MDW\DemoBundle\Form\ArticleType; 
//-- Agregar este método como uno nuevo
public function newAction()
{
    $articulo = new Articles();
    $form = $this->createForm(new ArticleType(), $articulo);
    return $this->render('MDWDemoBundle:Articulos:new.html.twig', array(
        'form' => $form->createView(),
    ));
}

[tipexperto titulo = “Nota”]Un dato importante es que en el código de arriba hemos creado un nuevo $articulo desde un objeto vacío lo cual hará que el formulario se muestre vacío. Si queremos, por ejemplo en un formulario de modificación de registro, mostrar ya los datos del artículo a modificar esto simplemente implicaría obtener los datos desde la base de datos utilizando un DQL o el método find() que vimos en el capítulo anterior antes de pasarlo al método createForm().[/tipexperto]

El código que debe contener nuestro action es muy sencillo. Primeramente creamos un objeto Article y luego, por medio del método $this->createForm() invocamos a nuestro objeto ArticleType pasándole nuestro objeto recién creado $articulo, devolviéndonos un objeto de tipo formuario. Finalmente invocamos a la vista como siempre hacemos y pasamos como parámetro el resultado de ejecutar $form->createView().

Con esto ya seremos capaces de ver el código de nuestra vista  MDWDemoBundle:Articulos:new.html.twig que de acuerdo a este nombre lógico debemos crear el archivo new.html.twig dentro de la carpeta src/MDW/DemoBundle/Resources/views/Articulos/ con el siguiente código:

<form action="{{ path('articulo_new') }}" method="post">
    {{ form_widget(form) }}
    <input type="submit" />
</form>

La creación de la etiqueta formulario la hacemos normalmente así como también el botón de submit. Lo único importante aquí es que el action del form debe apuntar a la misma página por lo que creamos el link por medio de path(‘articulo_new’).

La parte mágica está en {{ form_widget(form) }} donde, por medio de form_widget y Twig, pasamos como argumento la variable que nuestro action nos ha enviado y se imprime en la página el código necesario para nuestro formulario. Es decir que veremos el formulario al ingresar a la dirección:  http://localhost/Symfony/web/app_dev.php/articulo/new

Si miramos el código HTML veremos lo siguiente:

<form action="/Symfony/web/app_dev.php/articulo/new" method="post">
    <div id="article_form">
        <input type="hidden" id="article_form__token" name="article_form[_token]" value="62bc1a503b32de46b8755e9a5f5d8855bc8eb877" />
        <div>
            <label for="article_form_title" class=" required">Title</label>
            <input type="text" id="article_form_title" name="article_form[title]" required="required" maxlength="20" />
        </div>
       <div>
            <label for="article_form_author" class=" required">Author</label>
            <input type="text" id="article_form_author" name="article_form[author]" required="required" maxlength="255" />
       </div>
       <div>
             <label class=" required">Created</label>
             <div id="article_form_created">
                 <select id="article_form_created_year" name="article_form[created][year]" required="required">
                     <option value="2007">2007</option>
                     <option value="2008">2008</option>
                     ...
                 </select>
             </div>
        </div>
    </div>

    <input type="submit" />

</form>

[tipexperto titulo = “Nota”]Muy importante es notar que a parte de los campos que hemos agregado para que sean mostrados en el $builder, también se muestra un campo article_form[_token] con un valor aleatorio. Esto lo hace automáticamente para luchar contra uno de los ataques más usados por los hackers llamado CSRF. Con eso ya vemos como Symfony nos propone ya un estándar de seguridad. A esta seguridad también se suma que por medio de Doctrine también tenemos validado los problemas de SQL Injection.[/tipexperto]

Si miramos el código podemos notar los atributos “name” como los explicamos arriba y también vemos que mágicamente el campo “created” se muestra como un campo para seleccionar una fecha. Esto es debido a que el framework reconoce el tipo de input a mostrar ya que sabe, por medio del objeto Articles, que esa propiedad es una fecha. Esto es tremendamente útil ya que muchas veces podría ya reconocer que type agregarle a las etiquetas input, pero si necesitamos definir por nosotros mismos el atributo type lo hacemos agregando un segundo argumento al momento de agregar el campo al $builder:

public function buildForm(FormBuilder $builder, array $options)
{
    $builder->add('title')
        ->add('author', 'checkbox')
        ->add('created');
}

Mientras que si necesitamos hacer que un campo no sea obligatorio lo hacemos enviando un array como tercer argumento ya que por defecto todos los campos son puestos como requeridos con validaciones HTML5:

public function buildForm(FormBuilder $builder, array $options)
{
    $builder->add('title')
        ->add('author', 'text', array('required' => false))
        ->add('created');
}

Como vemos el formulario HTML es impreso directamente en la página usando el {{ form_widget(form) }} incluyendo divs que nos ayudarán a formatear por medio de CSS y mejorar la estructura de mismo pero en caso de querer crear formularios más complejos en diseño también se cuentan con las siguientes opciones que para no extender mucho este capítulo lo veremos quizá en otra entrega:

  • form_errors(form): Renderiza lo errores que se encuentren en el formulario.
  • form_rest(form): Renderiza los campos de formulario que no hayan sido agregados manualmente con el form_row.
  • form_row(form.field): Renderiza un campo específico dentro de un div.
  • form_errors(form.field): Renderiza el error para un campo específico.
  • form_label(form.field): Renderiza la etiqueta label para un campo específico.
  • form_widget(form.field): Renderiza un campo específico.

Procesamiento del Formulario

Ahora que ya vimos como mostrar el formulario en la página y habiendo dicho el action de un form va al mismo action para ser procesado, entremos en detalle de las modificaciones que tenemos que tener en cuenta en el código original dentro del método newAction().

Lo primero que tenemos que pensar es que si para procesar el formulario llamamos al mismo action, ¿Cómo sabemos cuándo mostrar el formulario y cuándo procesarlo?. La respuesta es bien sencilla, cuando el request fue de tipo GET lo deberíamos de mostrar pero en caso de que se haya dado click en el botón submit se ejecuta un request de tipo POST y por lo tanto se debería procesar. Veamos el código modificado de nuestro newAction():

public function newAction()
{
    //-- Obtenemos el request que contendrá los datos
    $request = $this->getRequest();

    $articulo = new Articles();
    $form = $this->createForm(new ArticleType(), $articulo);

    //-- En caso de que el request haya sido invocado por POST
    //   procesaremos el formulario
    if($request->getMethod() == 'POST')
    {
        //-- Pasamos el request el método bindRequest() del objeto 
        //   formulario el cual obtiene los datos del formulario
        //   y los carga dentro del objeto Article que está contenido
        //   también dentro del objeto Type
        $form->bindRequest($request);

        //-- Con esto nuestro formulario ya es capaz de decirnos si
        //   los dato son válidos o no y en caso de ser así
        if($form->isValid())
        {
            //-- Procesamos los datos que ya están automáticamente
            //   cargados dentro de nuestra variable $articulo, ya sea
            //   grabándolos en la base de datos, enviando un mail, etc

            //-- Finalmente, al finalizar el procesamiento, siempre es
            //   importante realizar una redirección para no tener el
            //   problema de que al intentar actualizar el navegador
            //   nos dice que lo datos se deben volver a reenviar. En
            //   este caso iremos a la página del listado de artículos
            return $this->redirect($this->generateURL('articulos'));
        }
    }
    return $this->render('MDWDemoBundle:Articulos:new.html.twig', array(
        'form' => $form->createView(),
    ));
}

Como vemos en las explicaciones del código casi todo es automáticamente realizado por el objeto ArticleType quién al conocer el request ya nos devuelve el mismo objeto original $articulo que le fue entregado en el createForm(new ArticleType(), $articulo);.

En caso de que los datos no sean válidos y el método isValid() retorne false seguirá el hasta mostrar nuevamente el formulario llamando al método $this->render() y el {{ form_widget(form) }} puesto en nuestra misma vista se encargará de mostrar los errores de validación.

[tipexperto titulo = “Nota”]Symfony2 agrega las validaciones de los formularios en HTML5 y del lado del servidor. Si el navegador no soporta las validaciones por medio de HTML5 el método isValid() lo valida en el servidor y al retornar la respuesta por el método render() se mostrarán los mensajes de validación del servidor. Puede que tu navegador ya acepte las validaciones HTML5 por lo que al intentar enviar los datos no notes la validación del lado del servidor aunque lo mismo se están realizando.

Por ejemplo el campo $title está puesto como <input type=”text” id=”article_form_title” name=”article_form[title]” required=”required” maxlength=”255″ pattern=”.{10,255}” /> donde se puede ver que las validaciones de HTML5 fueron ya puestas.

Si no tienes un navegador que NO soporte HTML5 para probar como se muestran los mensajes de validación del servidor puedes, utilizando el Firebug del Firefox, eliminar el texto required=”required” maxlength=”255″ pattern=”.{10,255}” de la etiqueta input y luego presionar el botón de submit :-)

Como verás, los hackers que quieren usar esta técnica también serán detenidos por las validaciones del servidor.[/tipexperto]

Resumen Final

En este capítulo hemos trabajado muchísimo viendo dos temas sumamente importantes: la validación de los Entities y los formularios.

Para las validaciones hemos hablado sobre los @Asserts, simples anotaciones que realizan validaciones poderosas con poco código y vemos que Symfony2 ya nos provee de la gran mayoría que necesitaremos usar.

Hablando sobre los formularios hemos notado la gran diferencia de diseñar los formularios y programar los formularios por medio de clases. Me gusta decir que en Symfony, el concepto de un formulario NO es simplemente introducción de texto sino introducción de texto VÁLIDO para la aplicación, libre de los problemas que hoy se tienen al crear un formulario a mano y tener que recordar pelear con ataques CSRF, XSS, SQL Injection y cambios en caliente con herramientas como Firebug.

El sub-framework de formularios es uno de los que más me hicieron sentir la diferencia entre usar un framework y no hacerlo y todavía hay muchas otras herramientas que nos permite usar como los formularios embebidos.

En el primer capítulo de esta guía hablamos sobre que uno de los objetivos de Symfony es plantear que cada cosa debe ir en su lugar, respetando el concepto del MVC. Con esto podemos ver que no solo podríamos tener un equipo de desarrollo, con personas expertas en cada área, trabajando con el modelado, otras con los controladores y a los diseñadores en la vista, sino que también podríamos hablar de personas que trabajen netamente en la creación de los formularios de la aplicación.

En el siguiente capítulo hablaremos sobre la integración de Ajax en nuestras aplicaciones hechas con Symfony2.