Formularios

Utilizar formularios HTML es una de las más comunes —y desafiantes— tareas para un desarrollador web. Symfony2 integra un componente Form que se ocupa de facilitarnos la utilización de formularios. En este capítulo, construirás un formulario complejo desde el principio, del cual, de paso, aprenderás las características más importantes de la biblioteca de formularios.

Nota

El componente Form de Symfony es una biblioteca independiente que puedes utilizar fuera de los proyectos Symfony2. Para más información, consulta el Componente Form de Symfony2 en Github.

Creando un formulario sencillo

Supón que estás construyendo una sencilla aplicación de tareas pendientes que necesita mostrar tus «pendientes». Debido a que tus usuarios tendrán que editar y crear tareas, tienes que crear un formulario. Pero antes de empezar, vamos a concentrarnos en la clase genérica Task que representa y almacena los datos para una sola tarea:

// src/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\Entity;

class Task
{
    protected $task;

    protected $dueDate;

    public function getTask()
    {
        return $this->task;
    }
    public function setTask($task)
    {
        $this->task = $task;
    }

    public function getDueDate()
    {
        return $this->dueDate;
    }
    public function setDueDate(\DateTime $dueDate = null)
    {
        $this->dueDate = $dueDate;
    }
}

Nota

Si estás codificando este ejemplo, primero crea el paquete AcmeTaskBundle ejecutando la siguiente orden (aceptando todas las opciones predeterminadas):

$ php app/console generate:bundle --namespace=Acme/TaskBundle

Esta clase es un «antiguo objeto PHP sencillo», ya que, hasta ahora, no tiene nada que ver con Symfony o cualquier otra biblioteca. Es simplemente un objeto PHP normal que directamente resuelve un problema dentro de tu aplicación (es decir, la necesidad de representar una tarea pendiente en tu aplicación). Por supuesto, al final de este capítulo, serás capaz de enviar datos a una instancia de Task (a través de un formulario), validar sus datos, y persistirla en una base de datos.

Construyendo el formulario

Ahora que has creado una clase Task, el siguiente paso es crear y reproducir el formulario HTML real. En Symfony2, esto se hace construyendo un objeto Form y luego pintándolo en una plantilla. Por ahora, esto se puede hacer en el interior de un controlador:

// src/Acme/TaskBundle/Controller/DefaultController.php
namespace Acme\TaskBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Acme\TaskBundle\Entity\Task;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    public function newAction(Request $request)
    {
        // crea una task y le asigna algunos datos ficticios para este ejemplo
        $task = new Task();
        $task->setTask('Write a blog post');
        $task->setDueDate(new \DateTime('tomorrow'));

        $form = $this->createFormBuilder($task)
            ->add('task', 'text')
            ->add('dueDate', 'date')
            ->getForm();

        return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
            'form' => $form->createView(),
        ));
    }
}

Truco

Este ejemplo muestra cómo crear el formulario directamente en el controlador. Más adelante, en la sección «Creando clases Form», aprenderás cómo construir tu formulario en una clase independiente, lo cual es muy recomendable puesto que vuelve reutilizable tu formulario.

La creación de un formulario requiere poco código relativamente, porque los objetos form de Symfony2 se construyen con un «generador de formularios». El propósito del generador de formularios es permitirte escribir sencillas «recetas» de formulario, y hacer todo el trabajo pesado de la contrucción de un formulario.

En este ejemplo, añadiste dos campos al formulario —task y dueDate— que corresponden a las propiedades task y dueDate de la clase Task. También asignaste a cada uno un «tipo» (por ejemplo, text, date), el cual entre otras cosas, determina qué etiqueta de formulario HTML se dibuja para ese campo.

Symfony2 viene con muchos tipos integrados que explicaremos en breve (consulta Tipos de campo integrados).

Reproduciendo el formulario

Ahora que creaste el formulario, el siguiente paso es dibujarlo. Lo puedes hacer pasando un objeto view especial para formularios a tu plantilla (ten en cuenta la declaración $form->createView() en el controlador de arriba) y usando un conjunto de funciones ayudantes de formulario:

  • Twig
    {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
    <form action="{{ path('task_new') }}" method="post" {{ form_enctype(form) }}>
        {{ form_widget(form) }}
    
        <input type="submit" />
    </form>
    
  • PHP
    <!-- src/Acme/TaskBundle/Resources/views/Default/new.html.php -->
    <form action="<?php echo $view['router']->generate('task_new') ?>" method="post" <?php echo $view['form']->enctype($form) ?> >
        <?php echo $view['form']->widget($form) ?>
    
        <input type="submit" />
    </form>
    
../_images/form-simple.png

Nota

Este ejemplo asume que has creado una ruta llamada task_new que apunta al controlador AcmeTaskBundle:Default:new creado anteriormente.

¡Eso es todo! Al imprimir form_widget(form), se pinta cada campo en el formulario, junto con la etiqueta y un mensaje de error (si lo hay). Tan fácil como esto, aunque no es muy flexible (todavía). Por lo general, querrás reproducir individualmente cada campo del formulario para que puedas controlar la apariencia del formulario. Aprenderás cómo hacerlo en la sección «Reproduciendo un formulario en una plantilla».

Antes de continuar, observa cómo el campo de entrada task reproducido tiene el valor de la propiedad task del objeto $task (es decir, «Escribe una entrada del blog»). El primer trabajo de un formulario es: tomar datos de un objeto y traducirlos a un formato idóneo para reproducirlos en un formulario HTML.

Truco

El sistema de formularios es lo suficientemente inteligente como para acceder al valor de la propiedad protegida task a través de los métodos getTask() y setTask() de la clase Task. A menos que una propiedad sea pública, debe tener métodos «captadores» y «definidores» para que el componente Form pueda obtener y fijar datos en la propiedad. Para una propiedad booleana, puedes utilizar un método «isser» (por «es servicio», por ejemplo, isPublished()) en lugar de un captador (por ejemplo, getPublished() o getReminder()).

Nuevo en la versión 2.1: La compatibilidad para los métodos «hasser» se añadió en Symfony 2.1.

Procesando el envío del formulario

El segundo trabajo de un formulario es traducir los datos enviados por el usuario a las propiedades de un objeto. Para lograrlo, los datos presentados por el usuario deben estar vinculados al formulario. Añade la siguiente funcionalidad a tu controlador:

// ...
use Symfony\Component\HttpFoundation\Request;

public function newAction(Request $request)
{
    // sólo configura un objeto $task fresco (remueve los datos de prueba)
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task', 'text')
        ->add('dueDate', 'date')
        ->getForm();

    if ($request->isMethod('POST')) {
        $form->bind($request);

        if ($form->isValid()) {
            // realiza alguna acción, tal como guardar la tarea en la base de datos

            return $this->redirect($this->generateUrl('task_success'));
        }
    }

    // ...
}

Nuevo en la versión 2.1: El método bind se hizo más flexible en Symfony 2.1. Ahora acepta datos del cliente sin procesar (como antes) o un objeto Petición de Symfony. Este es preferible al método depreciado bindRequest.

Ahora, cuando se presente el formulario, el controlador vincula al formulario los datos presentados, los cuales se traducen en los nuevos datos de las propiedades task y dueDate del objeto $task. Esto sucede a través del método bind().

Nota

Tan pronto como se llame a bind(), los datos presentados se transfieren inmediatamente al objeto subyacente. Esto ocurre independientemente de si los datos subyacentes son válidos realmente o no.

Este controlador sigue un patrón común para el manejo de formularios, y tiene tres posibles rutas:

  1. Inicialmente, cuando se carga el formulario en un navegador, el método de la petición es GET, lo cual significa simplemente que se debe crear y reproducir el formulario;
  2. Cuando el usuario envía el formulario (es decir, el método es POST), pero los datos presentados no son válidos (la validación se trata en la siguiente sección), el formulario es vinculado y, a continuación reproducido, esta vez mostrando todos los errores de validación;
  3. Cuando el usuario envía el formulario con datos válidos, el formulario es vinculado y en ese momento tienes la oportunidad de realizar algunas acciones usando el objeto $task (por ejemplo, persistirlo a la base de datos) antes de redirigir al usuario a otra página (por ejemplo, una página de «agradecimiento» o «éxito»).

Nota

Redirigir a un usuario después de un exitoso envío de formulario evita que el usuario pueda hacer clic en «actualizar» y volver a enviar los datos.

Validando formularios

En la sección anterior, aprendiste cómo se puede presentar un formulario con datos válidos o no válidos. En Symfony2, la validación se aplica al objeto subyacente (por ejemplo, Task). En otras palabras, la cuestión no es si el «formulario» es válido, sino más bien si el objeto $task es válido después de aplicarle los datos enviados en el formulario. Invocar a $form->isValid() es un atajo que pregunta al objeto $task si tiene datos válidos o no.

La validación se realiza añadiendo un conjunto de reglas (llamadas restricciones) a una clase. Para ver esto en acción, añade restricciones de validación para que el campo task no pueda estar vacío y el campo dueDate no pueda estar vacío y debe ser un objeto \DateTime válido.

  • YAML
    # Acme/TaskBundle/Resources/config/validation.yml
    Acme\TaskBundle\Entity\Task:
        properties:
            task:
                - NotBlank: ~
            dueDate:
                - NotBlank: ~
                - Type: \DateTime
    
  • Annotations
    // Acme/TaskBundle/Entity/Task.php
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Task
    {
        /**
         * @Assert\NotBlank()
         */
        public $task;
    
        /**
         * @Assert\NotBlank()
         * @Assert\Type("\DateTime")
         */
        protected $dueDate;
    }
    
  • XML
    <!-- Acme/TaskBundle/Resources/config/validation.xml -->
    <class name="Acme\TaskBundle\Entity\Task">
        <property name="task">
            <constraint name="NotBlank" />
        </property>
        <property name="dueDate">
            <constraint name="NotBlank" />
            <constraint name="Type">\DateTime</constraint>
        </property>
    </class>
    
  • PHP
    // Acme/TaskBundle/Entity/Task.php
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\NotBlank;
    use Symfony\Component\Validator\Constraints\Type;
    
    class Task
    {
        // ...
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('task', new NotBlank());
    
            $metadata->addPropertyConstraint('dueDate', new NotBlank());
            $metadata->addPropertyConstraint('dueDate', new Type('\DateTime'));
        }
    }
    

¡Eso es todo! Si vuelves a enviar el formulario con datos no válidos, verás replicados los errores correspondientes en el formulario.

La validación es una característica muy poderosa de Symfony2 y tiene su propio capítulo dedicado.

Validando grupos

Truco

Si no estás utilizando la validación de grupos, entonces puedes saltarte esta sección.

Si tu objeto aprovecha la validación de grupos, tendrás que especificar la validación de grupos que utiliza tu formulario:

$form = $this->createFormBuilder($users, array(
    'validation_groups' => array('registration'),
))->add(...);

Si vas a crear clases form (una buena práctica), entonces tendrás que agregar lo siguiente al método getDefaultOptions():

use Symfony\Component\OptionsResolver\OptionsResolverInterface;

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => array('registration')
    ));
}

En ambos casos, sólo se utilizará el grupo de validación registration para validar el objeto subyacente.

Grupos basados en datos presentados

Nuevo en la versión 2.1: La posibilidad de especificar una retrollamada o Cierre en validation_groups es nueva en la versión 2.1

Si necesitas alguna lógica avanzada para determinar los grupos de validación (por ejemplo, basándote en datos presentados), puedes poner la opción validation_groups a un arreglo de retrollamadas, o a un Cierre:

use Symfony\Component\OptionsResolver\OptionsResolverInterface;

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => array('Acme\\AcmeBundle\\Entity\\Client', 'determineValidationGroups'),
    ));
}

Esto llamará al método estático determineValidationGroups() en la clase Cliente después de vincular el formulario, pero antes de llevar a cabo la validación. El objeto formulario se pasa como argumento al método (ve el siguiente ejemplo). Además puedes definir tu lógica completa en línea usando un Cierre:

use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => function(FormInterface $form) {
            $data = $form->getData();
            if (Entity\Client::TYPE_PERSON == $data->getType()) {
                return array('person');
            } else {
                return array('company');
            }
        },
    ));
}

Tipos de campo integrados

Symfony estándar viene con un gran grupo de tipos de campo que cubre todos los campos de formulario comunes y tipos de datos necesarios:

Campos de fecha y hora

Otros campos

Campos agrupados

Campos ocultos

Campos base

También puedes crear tus propios tipos de campo personalizados. Este tema se trata en el artículo «Cómo crear un tipo de campo personalizado para formulario» del recetario.

Opciones del tipo de campo

Cada tipo de campo tiene una serie de opciones que puedes utilizar para configurarlo. Por ejemplo, el campo dueDate se está traduciendo como 3 cajas de selección. Sin embargo, puedes configurar el campo de fecha para que sea interpretado como un cuadro de texto (donde el usuario introduce la fecha como una cadena en el cuadro):

->add('dueDate', 'date', array('widget' => 'single_text'))
../_images/form-simple2.png

Cada tipo de campo tiene una diferente serie de opciones que le puedes pasar. Muchas de ellas son específicas para el tipo de campo y puedes encontrar los detalles en la documentación de cada tipo.

Deduciendo el tipo de campo

Ahora que has añadido metadatos de validación a la clase Task, Symfony ya sabe un poco sobre tus campos. Si le permites, Symfony puede «deducir» el tipo de tu campo y configurarlo por ti. En este ejemplo, Symfony lo puede deducir a partir de las reglas de validación de ambos campos, task es un campo de texto normal y dueDate es un campo date:

public function newAction()
{
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task')
        ->add('dueDate', null, array('widget' => 'single_text'))
        ->getForm();
}

El «adivino» se activa cuando omites el segundo argumento del método add() (o si le pasas null). Si pasas un arreglo de opciones como tercer argumento (hecho por dueDate arriba), estas opciones se aplican al campo inferido.

Prudencia

Si tu formulario utiliza una validación de grupo específica, el adivino del tipo de campo seguirá considerando todas las restricciones de validación cuando infiere el tipo de campo (incluyendo las restricciones que no son parte de la validación de grupo utilizada).

Opciones para deducir el tipo de campo

Además de deducir el «tipo» de un campo, Symfony también puede tratar de inferir los valores correctos a partir de una serie de opciones del campo.

Truco

Cuando estas opciones están configuradas, el campo se dibujará con los atributos HTML especiales proporcionados para la validación de HTML5 en el cliente. Sin embargo, no genera el equivalente de las restricciones de lado del servidor (por ejemplo, Assert\Length). Y aunque tendrás que agregar manualmente la validación de lado del servidor, estas opciones del tipo de campo entonces se pueden deducir a partir de esa información.

  • required: La opción required se puede deducir basándose en las reglas de validación (es decir, el campo es NotBlank o NotNull) o los metadatos de Doctrine (es decir, el campo es nullable). Esto es muy útil, ya que tu validación de lado del cliente se ajustará automáticamente a tus reglas de validación.
  • max_length: Si el campo es una especie de campo de texto, entonces la opción max_length se puede inferir a partir de las restricciones de validación (si utilizas Length o Range) o desde los metadatos de Doctrine (vía la longitud del campo).

Nota

Estas opciones de campo sólo se infieren si estás utilizando Symfony para deducir el tipo de campo (es decir, las omites por completo o pasas null como el segundo argumento de add()).

Si quieres cambiar uno de los valores inferidos, lo puedes redefinir pasando la opción en el arreglo de opciones del campo:

->add('task', null, array('max_length' => 4))

Reproduciendo un formulario en una plantilla

Hasta ahora, has visto cómo se puede reproducir todo el formulario con una sola línea de código. Por supuesto, generalmente necesitarás mucha más flexibilidad al reproducirlo:

  • Twig
    {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
    <form action="{{ path('task_new') }}" method="post" {{ form_enctype(form) }}>
        {{ form_errors(form) }}
    
        {{ form_row(form.task) }}
        {{ form_row(form.dueDate) }}
    
        {{ form_rest(form) }}
    
        <input type="submit" />
    </form>
    
  • PHP
    <!-- src/Acme/TaskBundle/Resources/views/Default/newAction.html.php -->
    <form action="<?php echo $view['router']->generate('task_new') ?>" method="post" <?php echo $view['form']->enctype($form) ?>>
        <?php echo $view['form']->errors($form) ?>
    
        <?php echo $view['form']->row($form['task']) ?>
        <?php echo $view['form']->row($form['dueDate']) ?>
    
        <?php echo $view['form']->rest($form) ?>
    
        <input type="submit" />
    </form>
    

Échale un vistazo a cada parte:

  • form_enctype(form) — Si por lo menos un campo es para carga de archivos, se reproduce el obligado enctype="multipart/form-data";
  • form_errors(form) — Dibuja cualquier error global para todo el formulario (los errores específicos al campo se muestran junto a cada campo);
  • form_row(form.dueDate) — Dibuja la etiqueta, cualquier error, y el elemento gráfico HTML del formulario para el campo en cuestión (por ejemplo, dueDate), por omisión, en el interior de un elemento div;
  • form_rest(form) — Pinta todos los campos que aún no se han reproducido. Por lo general es buena idea realizar una llamada a este ayudante en la parte inferior de cada formulario (en caso de haber olvidado sacar un campo o si no quieres preocuparte de reproducir manualmente los campos ocultos). Este ayudante también es útil para tomar ventaja de la Protección CSRF automática.

La mayor parte del trabajo la realiza el ayudante form_row, el cual de manera predeterminada reproduce la etiqueta, los errores y el elemento gráfico HTML de cada campo del formulario dentro de una etiqueta div. En la sección Tematizando formularios, aprenderás cómo puedes personalizar form_row en diferentes niveles.

Truco

Puedes acceder a los datos reales de tu formulario vía form.vars.value:

  • Twig
    {{ form.vars.value.task }}
    
  • PHP
    <?php echo $view['form']->get('value')->getTask() ?>
    

Reproduciendo cada campo a mano

El ayudante form_row es magnífico porque rápidamente puedes reproducir cada campo del formulario (y también puedes personalizar el formato utilizado para la «fila»). Pero, puesto que la vida no siempre es tan simple, también puedes dibujar cada campo totalmente a mano. El producto final del siguiente fragmento es el mismo que cuando usas el ayudante form_row:

  • Twig
    {{ form_errors(form) }}
    
    <div>
        {{ form_label(form.task) }}
        {{ form_errors(form.task) }}
        {{ form_widget(form.task) }}
    </div>
    
    <div>
        {{ form_label(form.dueDate) }}
        {{ form_errors(form.dueDate) }}
        {{ form_widget(form.dueDate) }}
    </div>
    
    {{ form_rest(form) }}
    
  • PHP
    <?php echo $view['form']->errors($form) ?>
    
    <div>
        <?php echo $view['form']->label($form['task']) ?>
        <?php echo $view['form']->errors($form['task']) ?>
        <?php echo $view['form']->widget($form['task']) ?>
    </div>
    
    <div>
        <?php echo $view['form']->label($form['dueDate']) ?>
        <?php echo $view['form']->errors($form['dueDate']) ?>
        <?php echo $view['form']->widget($form['dueDate']) ?>
    </div>
    
    <?php echo $view['form']->rest($form) ?>
    

Si la etiqueta generada automáticamente para un campo no es del todo correcta, la puedes especificar explícitamente:

  • Twig
    {{ form_label(form.task, 'Task Description') }}
    
  • PHP
    <?php echo $view['form']->label($form['task'], 'Task Description') ?>
    

Algunos tipos de campo tienen opciones adicionales para su representación que puedes pasar al elemento gráfico. Estas opciones están documentadas con cada tipo, pero una opción común es attr, la cual te permite modificar los atributos en el elemento del formulario. Lo siguiente debería añadir la clase task_field al campo de entrada de texto reproducido:

  • Twig
    {{ form_widget(form.task, { 'attr': {'class': 'task_field'} }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['task'], array(
        'attr' => array('class' => 'task_field'),
    )) ?>
    

Si necesitas dibujar campos de formulario «a mano», entonces puedes acceder a los valores individuales de los campos tal como el id nombre y etiqueta. Por ejemplo, para conseguir el id:

  • Twig
    {{ form.task.vars.id }}
    
  • PHP
    <?php echo $form['task']->get('id') ?>
    

Para recuperar el valor utilizado para el atributo nombre del campo en el formulario necesitas utilizar el valor full_name:

  • Twig
    {{ form.task.vars.full_name }}
    
  • PHP
    <?php echo $form['task']->get('full_name') ?>
    

Referencia de funciones de plantilla Twig

Si estás utilizando Twig, hay disponible una referencia completa de las funciones de reproducción de formularios en el Manual de referencia. Estúdiala para conocer todo acerca de los ayudantes y las opciones disponibles que puedes utilizar con cada uno.

Creando clases Form

Como viste, puedes crear un formulario y utilizarlo directamente en un controlador. Sin embargo, una mejor práctica es construir el formulario en una clase separada, independiente de las clases PHP, misma que puedes reutilizar en cualquier lugar de tu aplicación. Crea una nueva clase que albergará la lógica para la construcción del formulario de la tarea:

// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('task');
        $builder->add('dueDate', null, array('widget' => 'single_text'));
    }

    public function getName()
    {
        return 'task';
    }
}

Esta nueva clase contiene todas las indicaciones necesarias para crear el formulario de la tarea (observa que el método getName() devolverá un identificador único para este «tipo» de formulario). La puedes utilizar para construir rápidamente un objeto formulario en el controlador:

// src/Acme/TaskBundle/Controller/DefaultController.php

// agrega esta nueva declaración use en lo alto de la clase
use Acme\TaskBundle\Form\Type\TaskType;

public function newAction()
{
    $task = ...;
    $form = $this->createForm(new TaskType(), $task);

    // ...
}

Colocar la lógica del formulario en su propia clase significa que fácilmente puedes reutilizar el formulario en otra parte del proyecto. Esta es la mejor manera de crear formularios, pero la decisión en última instancia, depende de ti.

Truco

Al asignar formularios a objetos, se asignan todos los campos. Todos los campos del formulario que no existen en el objeto asignado provocarán que se lance una excepción.

En los casos donde necesites más campos en el formulario (por ejemplo: para una casilla de verificación «Estoy de acuerdo con estos términos») que no se asociará al objeto subyacente, necesitas establecer la opción property_path a false:

use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->add('task');
    $builder->add('dueDate', null, array('mapped' => false));
}

Además, si hay algunos campos en el formulario que no se incluyen en los datos presentados, esos campos explícitamente se establecerán en null.

Los datos del campo se pueden acceder en un controlador con:

$form->get('dueDate')->getData();

Formularios y Doctrine

El objetivo de un formulario es traducir los datos de un objeto (por ejemplo, Task) a un formulario HTML y luego traducir los datos enviados por el usuario al objeto original. Como tal, el tema de la persistencia del objeto Task a la base de datos es del todo ajeno al tema de los formularios. Pero, si has configurado la clase Task para persistirla a través de Doctrine (es decir, que le has añadido metadatos de asignación), entonces persistirla después de la presentación de un formulario se puede hacer cuando el formulario es válido:

if ($form->isValid()) {
    $em = $this->getDoctrine()->getManager();
    $em->persist($task);
    $em->flush();

    return $this->redirect($this->generateUrl('task_success'));
}

Si por alguna razón, no tienes acceso a tu objeto $task original, lo puedes recuperar desde el formulario:

$task = $form->getData();

Para más información, consulta el capítulo ORM de Doctrine.

La clave es entender que cuando el formulario está vinculado, los datos presentados inmediatamente se transfieren al objeto subyacente. Si deseas conservar los datos, sólo tendrás que conservar el objeto en sí (el cual ya contiene los datos presentados).

Integrando formularios

A menudo, querrás crear un formulario que incluye campos de muchos objetos diferentes. Por ejemplo, un formulario de registro puede contener datos que pertenecen a un objeto User, así como a muchos objetos Address. Afortunadamente, esto es fácil y natural con el componente Form.

Integrando un solo objeto

Supongamos que cada Task pertenece a un simple objeto Categoría. Inicia, por supuesto, creando el objeto Categoría:

// src/Acme/TaskBundle/Entity/Category.php
namespace Acme\TaskBundle\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class Category
{
    /**
     * @Assert\NotBlank()
     */
    public $name;
}

A continuación, añade una nueva propiedad categoría a la clase Task:

// ...

class Task
{
    // ...

    /**
     * @Assert\Type(type="Acme\TaskBundle\Entity\Category")
     */
    protected $category;

    // ...

    public function getCategory()
    {
        return $this->category;
    }

    public function setCategory(Category $category = null)
    {
        $this->category = $category;
    }
}

Ahora que actualizaste tu aplicación para reflejar las nuevas necesidades, crea una clase formulario para que el usuario pueda modificar un objeto Categoría:

// src/Acme/TaskBundle/Form/Type/CategoryType.php
namespace Acme\TaskBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class CategoryType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\TaskBundle\Entity\Category',
        ));
    }

    public function getName()
    {
        return 'category';
    }
}

El objetivo final es permitir que la Categoría de una Task sea modificada justo dentro del mismo formulario de la tarea. Para lograr esto, añade un campo categoría al objeto TaskType cuyo tipo es una instancia de la nueva clase CategoryType:

use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...

    $builder->add('category', new CategoryType());
}

Los campos de CategoryType ahora se pueden reproducir junto a los de la clase TaskType. Para activar la validación en CategoryType, añade la opción cascade_validation como TaskType:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'Acme\TaskBundle\Entity\Task',
        'cascade_validation' => true,
    ));
}

Reproduce los campos de Categoría de la misma manera que los campos de la Task original:

  • Twig
    {# ... #}
    
    <h3>Category</h3>
    <div class="category">
        {{ form_row(form.category.name) }}
    </div>
    
    {{ form_rest(form) }}
    {# ... #}
    
  • PHP
    <!-- ... -->
    
    <h3>Category</h3>
    <div class="category">
        <?php echo $view['form']->row($form['category']['name']) ?>
    </div>
    
    <?php echo $view['form']->rest($form) ?>
    <!-- ... -->
    

Cuando el usuario envía el formulario, los datos presentados para los campos de Categoría se utilizan para construir una instancia de Categoría, que entonces se establece en el campo categoría de la instancia de Task.

La instancia de Categoría es accesible naturalmente vía $task->getCategory() y la puedes persistir en la base de datos o utilizarla como necesites.

Integrando una colección de formularios

Puedes integrar una colección de formularios en un solo formulario (imagina un formulario Categoría con muchos subformularios Producto). Esto se consigue usando el tipo de campo collection.

Para más información consulta el artículo «Cómo integrar una colección de formularios» del recetario y la referencia del tipo de campo collection.

Tematizando formularios

Puedes personalizar cómo se reproduce cada parte de un formulario. Eres libre de cambiar la forma en que se dibuja cada «fila» del formulario, cambiar el formato que sirve para reproducir errores, e incluso personalizar la forma en que se debe reproducir una etiqueta textarea. Nada está fuera de límites, y puedes usar diferentes personalizaciones en diferentes lugares.

Symfony utiliza plantillas para reproducir todas y cada una de las partes de un formulario, como las etiquetas label, etiquetas input, mensajes de error y todo lo demás.

En Twig, cada «fragmento» del formulario está representado por un bloque Twig. Para personalizar alguna parte de cómo se reproduce un formulario, sólo hay que reemplazar el bloque adecuado.

En PHP, cada «fragmento» del formulario se reproduce vía un archivo de plantilla individual. Para personalizar cualquier parte de cómo se reproduce un formulario, sólo hay que reemplazar la plantilla existente creando una nueva.

Para entender cómo funciona esto, vamos a personalizar el fragmento form_row añadiendo un atributo «class» al elemento div que envuelve cada fila. Para ello, crea un nuevo archivo de plantilla que almacenará el nuevo marcado:

  • Twig
    {# src/Acme/TaskBundle/Resources/views/Form/fields.html.twig #}
    {% block form_row %}
    {% spaceless %}
        <div class="form_row">
            {{ form_label(form) }}
            {{ form_errors(form) }}
            {{ form_widget(form) }}
        </div>
    {% endspaceless %}
    {% endblock form_row %}
    
  • PHP
    <!-- src/Acme/TaskBundle/Resources/views/Form/form_row.html.php -->
    <div class="form_row">
        <?php echo $view['form']->label($form, $label) ?>
        <?php echo $view['form']->errors($form) ?>
        <?php echo $view['form']->widget($form, $parameters) ?>
    </div>
    

El fragmento field_row del formulario se usa cuando al dibujar la mayoría de los campos a través de la función form_row. Para decir al componente Form que utilice tu nuevo fragmento field_row definido anteriormente, añade lo siguiente en la parte superior de la plantilla que dibuja el formulario:

  • Twig
    {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
    {% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' %}
    
    {% form_theme form 'AcmeTaskBundle:Form:fields.html.twig'
                       'AcmeTaskBundle:Form:fields2.html.twig' %}
    
    <form ...>
  • PHP
    <!-- src/Acme/TaskBundle/Resources/views/Default/new.html.php -->
    <?php $view['form']->setTheme($form, array('AcmeTaskBundle:Form')) ?>
    
    <?php $view['form']->setTheme($form, array('AcmeTaskBundle:Form', 'AcmeTaskBundle:Form')) ?>
    
    <form ...>

La etiqueta form_theme (en Twig) «importa» los fragmentos definidos en la plantilla dada y los utiliza al reproducir el formulario. En otras palabras, cuando más adelante en esta plantilla se invoque la función form_row, se utilizará el bloque field_row de tu tema personalizado (en lugar del bloque field_row predefinido suministrado con Symfony).

Tu tema personalizado no tiene que sustituir todos los bloques. Cuando dibujes un bloque que no se reemplaza en tu tema personalizado, el motor de creación de temas caerá de nuevo en el tema global (definido a nivel del paquete).

Si hay varios temas personalizados siempre se buscará en el orden listado antes de caer de nuevo al tema global.

Para personalizar cualquier parte de un formulario, sólo tienes que reemplazar el fragmento apropiado. Saber exactamente qué bloque sustituir es el tema de la siguiente sección.

Nuevo en la versión 2.1: Introduce una sintaxis alterna para el form_theme de Twig. Esta acepta cualquier expresión Twig válida (la diferencia más notable es el uso de un arreglo cuando utilizas múltiples temas).

{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}

{% form_theme form with 'AcmeTaskBundle:Form:fields.html.twig' %}

{% form_theme form with ['AcmeTaskBundle:Form:fields.html.twig',
                         'AcmeTaskBundle:Form:fields2.html.twig'] %}

Para una explicación más extensa, consulta Cómo personalizar la reproducción de un formulario.

Nombrando fragmentos de formulario

En Symfony, cada parte de un formulario reproducido —elementos HTML de formulario, errores, etiquetas, etc.— se definen en base a un tema, el cual es una colección de bloques en Twig y una colección de archivos de plantilla en PHP.

En Twig, cada bloque necesario se define en un solo archivo de plantilla (form_div_layout.html.twig) que vive dentro del puente Twig. Dentro de este archivo, puedes ver todos los bloques necesarios para reproducir un formulario y cada tipo de campo predeterminado.

En PHP, los fragmentos son archivos de plantilla individuales. De manera predeterminada se encuentran en el directorio Resources/views/Form del paquete de la plataforma (ver en GitHub).

El nombre de cada fragmento sigue el mismo patrón básico y se divide en dos partes, separadas por un solo carácter de guión bajo (_). Algunos ejemplos son:

  • form_row — usado por form_row para reproducir la mayoría de los campos;
  • textarea_widget — usado por form_widget para dibujar un campo de tipo textarea;
  • form_errors — usado por form_errors para dibujar los errores de un campo;

Cada fragmento sigue el mismo patrón básico: type_part. La porción type corresponde al tipo del campo que se está reproduciendo (por ejemplo, textarea, checkbox, date, etc.), mientras que la porción part corresponde a qué se está reproduciendo (por ejemplo, label, widget, errors, etc.). Por omisión, hay cuatro posibles partes de un formulario que puedes pintar:

label (p. ej. form_label) dibuja la etiqueta de los campos
widget (p. ej. form_widget) dibuja la representación HTML de los campos
errors (p. ej. form_errors) dibuja los errores de los campos
row (p. ej. form_row) dibuja el renglón completo de los campos (etiqueta, elemento gráfico y errores)

Nota

En realidad, hay otras 3 partesrows, rest y enctype— pero rara vez o quizá nunca te tengas que preocupar de cómo sustituirlas.

Al conocer el tipo de campo (por ejemplo, textarea) y cual parte deseas personalizar (por ejemplo, widget), puedes construir el nombre del fragmento que se debe redefinir (por ejemplo, textarea_widget).

Heredando fragmentos de plantilla

En algunos casos, parece que falta el fragmento que deseas personalizar. Por ejemplo, no hay fragmento textarea_errors en los temas predeterminados provistos con Symfony. Entonces, ¿cómo se reproducen los errores de un campo textarea?

La respuesta es: a través del fragmento field_errors. Cuando Symfony pinta los errores del tipo textarea, primero busca un fragmento textarea_errors antes de caer de nuevo al fragmento form_errors. Cada tipo de campo tiene un tipo padre (el tipo primario del textarea es text, y su padre es el form), y Symfony utiliza el fragmento para el tipo del padre si no existe el fragmento base.

Por lo tanto, para sustituir sólo los errores de los campos textarea, copia el fragmento form_errors, renómbralo como textarea_errors y personalízalo. Para sustituir la reproducción predeterminada para error de todos los campos, copia y personaliza el fragmento form_errors directamente.

Truco

El tipo «padre» de cada tipo de campo está disponible en la referencia del tipo form para cada tipo de campo.

Tematizando formularios globalmente

En el ejemplo anterior, utilizaste el ayudante form_theme (en Twig) para «importar» fragmentos de formulario personalizados sólo para ese formulario. También puedes decirle a Symfony que importe formularios personalizados a través de tu proyecto.

Twig

Para incluir automáticamente en todas las plantillas los bloques personalizados de la plantilla fields.html.twig creada anteriormente, modifica el archivo de configuración de tu aplicación:

  • YAML
    # app/config/config.yml
    twig:
        form:
            resources:
                - 'AcmeTaskBundle:Form:fields.html.twig'
        # ...
    
  • XML
    <!-- app/config/config.xml -->
    <twig:config ...>
            <twig:form>
                <resource>AcmeTaskBundle:Form:fields.html.twig</resource>
            </twig:form>
            <!-- ... -->
    </twig:config>
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
        'form' => array(
            'resources' => array(
                'AcmeTaskBundle:Form:fields.html.twig',
            ),
        ),
        // ...
    ));
    

Ahora se utilizan todos los bloques dentro de la plantilla fields.html.twig a nivel global para definir el formulario producido.

PHP

Para incluir automáticamente todas las plantillas personalizadas del directorio Acme/TaskBundle/Resources/views/Form creado anteriormente, modifica el archivo de configuración de tu aplicación:

  • YAML
    # app/config/config.yml
    framework:
        templating:
            form:
                resources:
                    - 'AcmeTaskBundle:Form'
    # ...
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config ...>
        <framework:templating>
            <framework:form>
                <resource>AcmeTaskBundle:Form</resource>
            </framework:form>
        </framework:templating>
        <!-- ... -->
    </framework:config>
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'templating' => array(
            'form' => array(
                'resources' => array(
                    'AcmeTaskBundle:Form',
                ),
            ),
        )
        // ...
    ));
    

Cualquier fragmento dentro del directorio Acme/TaskBundle/Resources/views/Form ahora se utiliza globalmente para definir la salida del formulario.

Protección CSRF

CSRF (Cross-site request forgery) —o Falsificación de petición en sitios cruzados— es un método por el cual un usuario malintencionado intenta hacer que tus usuarios legítimos, sin saberlo, presenten datos que no tienen la intención de enviar. Afortunadamente, los ataques CSRF se pueden prevenir usando un elemento CSRF dentro de tus formularios.

La buena nueva es que, por omisión, Symfony integra y valida elementos CSRF automáticamente. Esto significa que puedes aprovechar la protección CSRF sin hacer nada. De hecho, ¡cada formulario en este capítulo se ha aprovechado de la protección CSRF!

La protección CSRF funciona añadiendo un campo oculto al formulario —por omisión denominado _token— el cual contiene un valor que sólo tú y tu usuario conocen. Esto garantiza que el usuario —y no alguna otra entidad— es el que presenta dichos datos. Symfony automáticamente valida la presencia y exactitud de este elemento.

El campo _token es un campo oculto y será reproducido automáticamente si se incluye la función form_rest() de la plantilla, la cual garantiza que se presenten todos los campos producidos.

El elemento CSRF se puede personalizar formulario por formulario. Por ejemplo:

use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class TaskType extends AbstractType
{
    // ...

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class'      => 'Acme\TaskBundle\Entity\Task',
            'csrf_protection' => true,
            'csrf_field_name' => '_token',
            // una clave única para ayudar generar la ficha secreta
            'intention'       => 'task_item',
        ));
    }

    // ...
}

Para desactivar la protección CSRF, fija la opción csrf_protection a false. Las personalizaciones también se pueden hacer a nivel global en tu proyecto. Para más información, consulta la sección referencia de configuración de formularios.

Nota

La opción intention es opcional pero mejora considerablemente la seguridad del elemento generado produciendo uno diferente para cada formulario.

Usando un formulario sin clase

En la mayoría de los casos, un formulario está ligado a un objeto, y los campos del formulario obtienen y almacenan sus datos en las propiedades de ese objeto. Esto exactamente es lo que has visto hasta ahora en este capítulo con la clase Task.

Pero a veces, es posible que sólo desees utilizar un formulario sin una clase, y devolver un arreglo de los datos presentados. Esto realmente es muy fácil:

// asegúrate de importar el espacio de nombres Request antes de la clase
use Symfony\Component\HttpFoundation\Request;
// ...

public function contactAction(Request $request)
{
    $defaultData = array('message' => 'Type your message here');
    $form = $this->createFormBuilder($defaultData)
        ->add('name', 'text')
        ->add('email', 'email')
        ->add('message', 'textarea')
        ->getForm();

        if ($request->isMethod('POST')) {
            $form->bind($request);

            // data es un arreglo con claves 'name', 'email', y 'message'
            $data = $form->getData();
        }

    // ... pinta el formulario
}

Por omisión, un formulario en realidad asume que deseas trabajar con arreglos de datos, en lugar de con un objeto. Hay exactamente dos maneras en que puedes cambiar este comportamiento y en su lugar enlazar el formulario a un objeto:

  1. Pasa un objeto al crear el formulario (como primer argumento de createFormBuilder o segundo argumento de createForm);
  2. Declara la opción data_class en tu formulario.

Si no haces ninguna de estas, entonces el formulario devolverá los datos como un arreglo. En este ejemplo, debido a que $defaultData no es un objeto (y no se ha establecido la opción data_class), en última instancia $form->getData(), devuelve un arreglo.

Truco

También puedes acceder a los valores POST (en este caso «name») directamente a través del objeto Petición, de la siguiente manera:

$this->get('request')->request->get('name');

Ten en cuenta, sin embargo, que en la mayoría de los casos una mejor opción es utilizar el método getData(), ya que devuelve los datos (generalmente un objeto), después de que la infraestructura del formulario los ha transformado.

Añadiendo validación

La única pieza faltante es la validación. Por lo general, cuando llamas a $form->isValid(), el objeto es validado leyendo las restricciones que aplicaste a esa clase. Si tu formulario está vinculado a un objeto (es decir, estás utilizando la opción data_class o pasando un objeto a tu formulario), este casi siempre es el enfoque que quieres usar. Ve Validando para más detalles.

Pero si no está vinculado a un objeto y en cambio recuperaste un simple arreglo de los datos presentados, ¿cómo puedes agregar restricciones a los datos de tu formulario?

La respuesta es configurar las restricciones tú mismo, y anexarlas a los campos individuales. El enfoque general está cubierto un poco más en el capítulo de validación, pero aquí está un pequeño ejemplo:

Nuevo en la versión 2.1: La opción constraints, que acepta una única restricción o un arreglo de restricciones (antes de 2.1, la opción fue llamada validation_constraint, y sólo acepta una única restricción) es nueva para Symfony 2.1.

use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

$builder
   ->add('firstName', 'text', array(
       'constraints' => new Length(array('min' => 3)),
   ))
   ->add('lastName', 'text', array(
       'constraints' => array(
           new NotBlank(),
           new Length(array('min' => 3)),
       ),
   ))
;

Truco

Si utilizas grupos de validación, necesitas o bien hacer referencia al grupo Default al crear el formulario, o establecer el grupo correcto en la restricción que estás añadiendo.

new NotBlank(array('groups' => array('create', 'update'))

Consideraciones finales

Ahora ya conoces todos los bloques de construcción necesarios para elaborar formularios complejos y funcionales para tu aplicación. Cuando construyas formularios, ten en cuenta que el primer objetivo de un formulario es traducir los datos de un objeto (Task) a un formulario HTML para que el usuario pueda modificar esos datos. El segundo objetivo de un formulario es tomar los datos presentados por el usuario y volverlos a aplicar al objeto.

Todavía hay mucho más que aprender sobre el poderoso mundo de los formularios, tal como la forma de manejar archivos subidos con Doctrine o cómo crear un formulario donde puedes agregar dinámicamente una serie de subformularios (por ejemplo, una lista de tareas donde puedes seguir añadiendo más campos a través de Javascript antes de presentarlos). Consulta el recetario para estos temas. Además, asegúrate de apoyarte en la referencia de tipos de campo, que incluye ejemplos de cómo utilizar cada tipo de campo y sus opciones.

Bifúrcame en GitHub