🎃 Grandes descuentos en libros en línea, eformaciones y vídeos*. Código CALABAZA30. Pulse aquí
¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí

Los formularios

Introducción

Los formularios están muy desarrollados en Symfony. Hay una multitud de opciones disponibles para trabajar con los campos de sus formularios a su gusto. Aquí le mostraremos lo que consideramos esencial conocer.

Como ejemplo, vamos a desarrollar la parte administrativa de nuestra aplicación para gestionar nuestros productos.

Para ello, creamos un nuevo controlador AdminController:

php bin/console make:controller 

Reemplazamos la acción index(), que por defecto existe en el controlador, por estas tres acciones: insert(), update() y delete().

<?php  
  
namespace App\Controller; 
  
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; 
use Symfony\Component\HttpFoundation\Response;  
use Symfony\Component\Routing\Annotation\Route;  
use Symfony\Component\HttpFoundation\Request;  
  
  
class AdminController extends AbstractController  
{  
    #[Route('/insert', name: 'app_admin')]  
    public function insert(Request $request)   
    {  
        return $this->render('Admin/create.html.twig');  
    }  
    #[Route("/update/{id}", name:"update")]  ...

Form Builder

Importante: antes de comenzar a trabajar con los formularios, vamos a desactivar la herramienta Symfony Turbo que instalamos en el capítulo Symfony UX Stimulus, sección HotWire y Turbo.

De hecho, Turbo, por defecto, se encarga de las presentaciones de los formularios y ejecuta una solicitud AJAX (en lugar de llamar a una página HTML) que devolverá las entradas del formulario en formato JSON. De esta manera, las entradas del formulario pueden ser procesadas por una aplicación JavaScript mientras permanecen en la misma página HTML (Single Page Application). 

Esto no es lo que queremos aquí. Queremos que los resultados de las entradas del formulario se transmitan en una nueva página HTML de manera estándar.

Para desactivar Turbo, vaya al archivo assets/controllers.json y establezca el valor de la propiedad "enabled" en false:

"@symfony/ux-turbo": {  
            "turbo-core": {  
                "enabled": false,  
                "fetch": "eager"  
            }, 

Vuelva a ejecutar el comando run watch para que estos cambios se tengan en cuenta:

npm run watch 

Esté tranquilo: en este capítulo, trabajará de forma «estándar».

No hay nada más sencillo que crear un formulario en la acción de un controlador.

Solo tiene que invocar el método createFormBuilder() y agregar los campos deseados con el método add().

Sintaxis:

$form=$this->createFormbuilder()  
          ->add('nombreCampo', Tipo de campo, [ options ]) 

Los tipos de campos disponibles están definidos en la documentación de Symfony: https://symfony.com/doc/current/reference/forms/types.html

Hay muchos tipos disponibles con muchas opciones.

Para usar un tipo, debe obtener la clase correspondiente.

Por ejemplo, para un TextType:

use Symfony\Component\Form\Extension\Core\Type\TextType;  
$form=$this->createFormbuilder()  
          ->add('nombreCampo'...

Formularios externos

1. Definición

Es conveniente tener formularios independientes de los controladores (a menos que el formulario sea realmente específico de un controlador).

Los formularios independientes pueden ser reutilizados en varias acciones sin necesidad de volver a definirlos. También podemos anidar formularios, heredar de otros formularios... Las posibilidades son numerosas.

Para crear un formulario externo, vamos a utilizar un comando en el terminal:

php bin/console make:form 

Tomemos un ejemplo. Vamos a crear un formulario para nuestra entidad Producto. Todos los nombres de formularios externalizados terminan con Type. Vamos a llamar a nuestro formulario: ProductoType.

Especifique también en el cuestionario el nombre de la entidad relacionada. Aquí es Producto:

php bin/console make:form  
  
The name of the form class (e.g. VictoriousJellybeanType): 
> ProductoType 
  
The name of Entity or fully qualified model class name that the new form 
will be bound to (empty for none): 
> Producto  
  
created: src/Form/ProductoType.php  
  
  Success! 

Se ha creado una carpeta src/Form y en su interior podemos encontrar el formulario  externo ProductoType.

Se han recuperado todos los campos de la entidad y se han colocado en el formulario: 

 public function buildForm(FormBuilderInterface $builder, array $options) 
    {  
        $builder  
            ->add('nombre')  
            ->add('precio')  
            ->add('cantidad')  
            ->add('agotado')  
            ->add('enlaceImagen')  
            ->add('referencia')  ...

Personalización de la visualización de un formulario

Hasta ahora, no nos hemos preocupado por el diseño del formulario. La ventaja con Symfony es que podemos desarrollar un formulario en PHP sin preocuparnos por la parte HTML. Nos hemos liberado de escribir las etiquetas <input>, que son pesadas y tediosas.

La instrucción:

{{ form(nombre_del_formulario) }} 

se encarga de todo.

Esta instrucción utiliza un tema por defecto para mostrar el formulario.

Es posible modificar este tema por defecto y, por ejemplo, utilizar un tema que incluya Bootstrap.

Para hacerlo, debemos modificar el archivo config/packages/twig.yaml y especificar el tema de Bootstrap 5:

twig:  
    default_path: '%kernel.project_dir%/templates'  
    globals:   
        autor: '%env(APP_AUTHOR)%'  
    form_themes: ['bootstrap_5_layout.html.twig'] 

Cuidado con la indentación. Hay cuatro espacios de desplazamiento para las opciones de Twig. No respetar la indentación produce un error en Symfony.

Hay que detener y reiniciar el servidor de Symfony para que el efecto funcione:

symfony server:start 

Por ahora, no notará muchas diferencias en el diseño del formulario, pero más adelante usaremos en la plantilla las clases de Bootstrap para darle formato.

De hecho, es posible ir más allá en el diseño personalizando cada elemento del tema.

Twig pone a su disposición funciones para renderizar las diferentes partes del formulario:

{{ form_start(my_form , { options }) }} 

describe la etiqueta de apertura del formulario. El parámetro { options } es opcional. Podemos usarlo para agregar valores a las propiedades del formulario (por ejemplo: { ’method’: ’POST’ }).

{{ form_end(my_form) }} 

describe la etiqueta de cierre del formulario.

{{ form_label(my_form.name, 'nombre del label', {'label_attr':  
{'class':   
'foo'...}}) }} 

describe la etiqueta <label> de un campo del formulario. form.name es el nombre del formulario seguido del nombre del campo (por ejemplo, my_form.nombre)....

Tratamiento de datos del formulario

Una vez que el formulario está listo, ahora nos ocupamos de recuperar la información introducida por el usuario y de insertarla en la base de datos.

En Symfony, todo ocurre en la misma acción, la creación y el procesamiento del formulario. Comenzaremos por tratar la acción insert() del AdminController.

Para recuperar los datos en la entidad asociada, debemos usar el método handleRequest() pasándole el objeto $request:

        $formProducto->handleRequest($request); 

Luego, debemos verificar si el método Post existe en el objeto $request. También es recomendable verificar el método isValid(). Este método comprueba que todos los datos del formulario cumplan con las restricciones de validación. Volveremos en una próxima sección sobre la validación del formulario.

if($request->isMethod('post') && $formFilm->isValid() ){...} 

Dentro de la instrucción de control if, es necesario para poder insertar los datos en la entidad.

En realidad, los datos del formulario ya están en la entidad. Fueron transmitidos al formulario por el método createForm() anteriormente (en nuestro ejemplo, está en la entidad $producto). Solo necesitamos persistir este objeto y ejecutar el método flush() para inyectarlos en la base de datos.

Pero antes, debemos recuperar la imagen.

La imagen se almacena en el campo enlaceImagen del formulario ($formProducto). Para recuperarla, debemos usar el método getData():

 $file = $formProducto['enlaceImagen']->getData(); 

$file nos permite recuperar el nombre original del archivo cargado:

 $filename=$file->getClientOriginalName(); 

También podríamos crear un nuevo nombre de archivo recuperando solo la extensión del archivo enviado (jpg, pdf...):

$extension = $file->guessExtension(); 

Luego, movemos el archivo al directorio que deseamos con el método move():

Sintaxis:

$file->move(nombre_carpeta, nombre_archivo); 

Para nuestro ejemplo, vamos a insertar las imágenes en el directorio public/img. Para configurar esta ubicación, vamos a crear un parámetro global que contenga esta ruta (será mejor para estructurar las imágenes que pueda tener en su aplicación).

Los parámetros globales...

Recuperación de datos de la entidad por defecto

Hemos desarrollado la acción insert(). Veamos cómo desarrollar la acción update() con un producto existente.

El código es prácticamente el mismo. Puede copiar y pegar el código de insert() en update($id).

Esto es lo que va a cambiar.

Recuperamos la entidad correspondiente al id transmitido como parámetro en la acción:

    $productoRepository=$entityManager->getRepository(Producto::class); 
    $producto=$productoRepository->find($id); 

A continuación, vamos a guardar en una variable el valor de la propiedad enlaceImagen de la entidad $producto.

¿Por qué hacer esto? 

Cuando recuperemos los datos del formulario, automáticamente se actualizará la entidad (método $formProducto->handleRequest($request)). Si el cliente desea conservar la imagen original de la entidad, no va a cargarla de nuevo. La propiedad en la entidad $producto será null y habremos perdido el valor de enlaceImagen. Por lo tanto, antes que nada, debemos guardar este valor:

 $img=$producto->getEnlaceImagen(); 

La presentación del formulario sigue siendo la misma.

 $formProducto= $this->createForm(ProductoType::class,$producto); 
  
        // añadimos un botón para enviar 
  
        $formProducto->add('crear', SubmitType::class,array( 
            'label'=>'Actualizar producto' 
        )); 
         $formProducto->handleRequest($request); 

El procesado de los datos es prácticamente el mismo, excepto el contenido del else en el caso de que el cliente no haya cargado ninguna imagen. Esta vez, ya no devolvemos un error, sino que actualizamos la entidad producto con el valor de la imagen que contenía, es decir, la que guardamos previamente en la variable $img:

if($request->isMethod('post') && $formProducto->isValid() ){ 
  
            // escritura en la base de datos 
  
            $file...

Añadir los botones de actualización en la vista lista

Para probar la acción de actualización, es más fácil agregar botones que apunten a esta acción en la vista lista_productos/index.html.twig. Aprovechamos para agregar también los botones de eliminación de un producto y de inserción de un nuevo producto.

El archivo de la vista lista_productos/index.html.twig queda así:

{% extends 'base.html.twig' %}  
  
{% block title %}Lista de productos   
{% endblock %}  
  
{% block body %}  
    <div class="alert alert-primary">Descuento del 20% en el producto: 
            {{ lastproducto.nombre }}</div> 
<a class="btn btn-info mb-2" href="{{ path('insert') }}" > 
    Añadir un nuevo producto  
</a>  
    <div class="d-flex flex-row justify-content-around flex-wrap">  
            {% for producto in listaproductos %}  
                    <div class="card" style="width: 18rem;">  
                        <img class="card-img-top" src="{{ asset  ...

Supresión de entidades

El manejo de la acción delete() no es difícil. Solo necesita recuperar la entidad correspondiente al parámetro $id transmitido y eliminarla con el método remove() del Administrador de Entidades.

En la acción delete() del AdminController:

#[Route("/delete/{id}", name:"delete")]  
function delete(Request $request, $id,EntityManagerInterface $entityManager) 
    { 
  
        $productoRepository=$entityManager->getRepository(Producto::class); 
        $producto=$productoRepository->find($id); 
        $entityManager->remove($producto); 
        $entityManager->flush(); 
        $session=$request->getSession(); 
        $session->getFlashBag()->add('message','producto eliminado de la lista'); 
        $session->set('statut','success'); 
        return $this->redirect($this->generateUrl('liste')); 
 } 

A partir de ahora, ya puede realizar las operaciones CRUD en la base de datos:...

La unión OneToOne

Recuerde: nuestra entidad tiene una propiedad $referencia que hace referencia a una unión OneToOne.

¿Cómo integrar esta unión en nuestro formulario?

Vamos a proceder con la anidación de formularios.

Primero, creemos el formulario externo para la entidad Referencia:

php bin/console make:form 

Nos pedirá el nombre de nuestro formulario tipo:

The name of the form class (e.g. DeliciousChefType):  
> ReferenciaType 

Luego, proporcionemos el nombre de la entidad a la que se vinculará el formulario:

The name of Entity or fully qualified model class name that the new form  
will be bound to (empty for none):  
>Referencia 

En el formulario ReferenciaType.php, configuremos el tipo de campo:

<?php  
namespace App\Form;  
  
use App\Entity\Referencia;  
use Symfony\Component\Form\AbstractType;  
use Symfony\Component\Form\FormBuilderInterface;  
use Symfony\Component\OptionsResolver\OptionsResolver;  
use Symfony\Component\Form\Extension\Core\Type\NumberType;  
  
class ReferenciaType extends AbstractType  
{  
    public function buildForm(FormBuilderInterface $builder, 
array $options)  
    {  
        $builder  
            ->add('numero',NumberType::class,array(  ...

La unión ManyToMany

La unión ManyToMany es un poco más compleja de implementar, ya que esta vez necesitamos anidar varios formularios tantas veces como uniones con la entidad inversa se requieran.

Siempre usaremos la propiedad que actúa como unión, pero esta vez el tipo de campo utilizado será CollectionType.

Por ejemplo, creemos un formulario para la entidad Distribuidor:

php bin/console make:form 

Definimos el nombre del formulario:

The name of the form class (e.g. OrangeElephantType):  
> DistribuidorType 

Le decimos en qué entidad se debe inspirar para crear el formulario:

The name of Entity or fully qualified model class name that the new form 
will be bound to (empty for none): 
>Distribuidor 

Dentro de DistribuidorType.php, añadimos el campo: nombre.

Borramos el tipo de la propiedad producto y la cambiamos por TextType:

<?php  
  
namespace App\Form;  
  
use App\Entity\Distribuidor;  
use Symfony\Component\Form\AbstractType;  
use Symfony\Component\Form\FormBuilderInterface;  
use Symfony\Component\OptionsResolver\OptionsResolver;  
use Symfony\Component\Form\Extension\Core\Type\TextType;  
  
class DistribuidorType extends AbstractType  
{  
    public function buildForm(FormBuilderInterface $builder, 
                 array $options)  
    {  
        $builder->add('nombre',TextType::class,array(  
            'label'=>'Nombre del distribuidor'  
        )) ;  
    }  
  
    public function configureOptions(OptionsResolver $resolver)  
    {  
        $resolver->setDefaults([  
            'data_class' => Distribuidor::class,  
        ]);  
    }  
} 

Luego...

Tipo EntityType

Este tipo permite gestionar los distribuidores y las uniones ya existentes. Dispone de varias opciones.

He aquí un ejemplo para los distribuidores. Comente el add(’distribuidores’) con CollectionType realizado anteriormente y agregue en ProductoType.php el siguiente campo:

->add('distribuidores',EntityType::Class,array(  
                    'class' => Distribuidor::class,  
                      'choice_label'=>'nombre',  
                    'label' =>'Selección de distribuidores',  
                    'multiple' => true,  
                    'required' => false  
                )) 

Añadimos el use del EntityType:

use \Symfony\Bridge\Doctrine\Form\Type\EntityType; 

lo mismo para Distribuidor:...

Creación de tipos de campos personalizados

Puede que necesite adaptar un tipo de campo existente para agregar opciones adicionales que no existen. Symfony le ofrece esta posibilidad gracias a la clase AbstractType. Veamos un ejemplo.

Para mejorar la visualización de la propiedad agotado a fin de resaltar este campo (actualmente de tipo Checkbox), vamos a crear un nuevo tipo de campo. Llamémoslo MyCheckboxType.

Cree el archivo correspondiente en una subcarpeta de form: src/Form/Type/MyCheckboxType.php

Todos los tipos de campo heredan de la clase AbstractType. Puede copiar y pegar un ejemplo de clase desde la página: https://symfony.com/doc/current/form/create_custom_field_type.html

Personalice el contenido de su clase MyCheckboxType de la siguiente manera:

<?php  
namespace App\Form\Type;  
  
use Symfony\Component\OptionsResolver\OptionsResolver; 
  
use Symfony\Component\Form\AbstractType;  
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;  
class MyCheckboxType extends AbstractType  
{  
  
  
    public function configureOptions(OptionsResolver $resolver)  
    {  
        $resolver->setDefaults(array(  
            'attr' =>...

Validación de los formularios

Nuestro formulario de administración funciona bien, pero no está a salvo de ciertas vulnerabilidades de seguridad.

De hecho, no estamos controlando el contenido de los campos completados por el usuario. El usuario podría «divertirse» inyectando código JavaScript en el campo Nombre de producto, por ejemplo. Este código se ejecutará automáticamente cada vez que recuperemos el nombre del producto para mostrarlo (en la lista de productos, por ejemplo). Puede imaginar el daño que esto puede causar.

Para evitar este tipo de problemas, vamos a añadir reglas de validación a nuestro formulario.

1. Reglas de validación

Hay dos formas de definir reglas de validación. La primera y más práctica es definir estas reglas en la entidad mediante anotaciones. También es posible definir reglas en un archivo aparte usando el formato YAML o PHP, pero es mucho menos práctico.

Vamos a ver cómo añadir reglas de validación mediante anotaciones.

Estas reglas se llaman afirmaciones (Asserts). Si la afirmación no se cumple, el formulario no se validará. Para usarlas, hay que añadir, en la entidad, el use correspondiente. Por ejemplo, vamos a añadirlo en nuestra entidad Producto:

use Symfony\Component\Validator\Constraints as Assert; 

De esta manera:

<?php  
  
namespace App\Entity;  
  
use App\Repository\ProductoRepository; 
use Doctrine\Common\Collections\ArrayCollection; 
use Doctrine\Common\Collections\Collection; 
use Doctrine\ORM\Mapping as ORM; 
use Symfony\Component\Validator\Constraints as Assert; 
  
#[ORM\Entity(repositoryClass: ProductoRepository::class)]  
class Producto  
{  
... 

Ahora podemos definir restricciones para todas las propiedades de la entidad Producto.

La lista de todas las restricciones disponibles se puede ver en la página de Symfony: https://symfony.com/doc/current/form/create_custom_field_type.html

Veamos un ejemplo.

En la entidad Producto, queremos que el tamaño del nombre de producto esté limitado a entre dos y cincuenta caracteres. Vamos a usar la restricción Length

Puede encontrar su descripción en la página: https://symfony.com/doc/current/reference/constraints/Length.html

Copie...