Generación de formularios

Índice

Hasta este apartado hemos aprendido algunos conceptos útiles de Symfony y algunos de sus bundles más destacados, como por ejemplo la generación de vistas con el motor de plantillas Twig y la comunicación con la base de datos a través del ORM Doctrine. Hemos hecho algunos controladores de ejemplo para buscar datos, pero todavía no disponemos de un mecanismo para que se envíen datos de inserción desde el cliente.

Para el funcionamiento de un formulario nos hace falta:

  • Un formulario definido en su propia clase
  • Un método en el controlador
  • Una plantilla que muestre el formulario

2.1 Creación de la clase para el formulario

En primer lugar, hemos de instalar las dependencias para crear formularios y validarlos

composer require form validator

Para crear el formulario usaremos el maker bundle:

php bin/console make:form ContactoFormType Contacto

Donde ContactoFormType es el nombre de la clase a crear y Contacto es el nombre de la entidad.

Este comando nos generará un formulario por defecto en la carpeta Form con el siguiente contenido:

<?php

namespace App\Form;

use App\Entity\Contacto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class ContactoFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('nombre')
            ->add('telefono')
            ->add('email')
            ->add('save', SubmitType::class, array('label' => 'Enviar'));
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Contacto::class,
        ]);
    }
}

Por defecto, cada campo lo crea de tipo TextType, es decir, un <input> de tipo text. Además le hemos añadido un botón para enviar el formulario.

2.2 Creación del controlador

Vamos a crear un método para renderizar el formulario:

...
use App\Form\ContactoFormType;
use Symfony\Component\HttpFoundation\Request;
...
#[Route('/contacto/nuevo', name: 'nuevo')]
public function nuevo(ManagerRegistry $doctrine, Request $request)
{
    $contacto = new Contacto();
    $formulario = $this->createForm(ContactoFormType::class, $contacto);
    $formulario->handleRequest($request);

    if ($formulario->isSubmitted() && $formulario->isValid()) {
        $contacto = $formulario->getData();
        $entityManager = $doctrine->getManager();
        $entityManager->persist($contacto);
        $entityManager->flush();
        return $this->redirectToRoute('contacto', ["codigo" => $contacto->getId()]);
    }
    return $this->render('nuevo.html.twig', array('formulario' => $formulario->createView()));
}
  • $contacto = new Contacto(); Creamos una instancia de la clase Contacto
  • $formulario = $this->createForm(ContactoFormType::class, $contacto); Crea el formulario ContactoFormType y le pasa el $contacto
  • $formulario->handleRequest($request); Symfony se encarga de manejar el formulario
  • if ($formulario->isSubmitted() && $formulario->isValid()) { Si el usuario ha dado a Enviar y es válido (ya lo veremos más adelante)
  • $contacto = $formulario->getData(); automáticamente, Symfony rellena los datos del contacto
  • $entityManager = $doctrine->getManager(); Usamos el manager de Doctrine que nos permitirá guardar el contacto
  • $entityManager->persist($contacto); Esto prepara el contacto para guardarlo en la base de datos.
  • $entityManager->flush(); Ahora sí que lo guardamos (si se nos olvida, no grabará nada en la base de datos)
  • return $this->redirectToRoute('ficha_contacto', ["codigo" => $contacto->getId()]); Ahora redirigimos a la ficha del contacto, pasándole como parámetro $contacto->getId() ya que la ruta ficha_contacto espera un parámetro llamado codigo
  • return $this->render('nuevo.html.twig', array('formulario' => $formulario->createView())); Si por el contrario no se ha pulsado enviar o el formulario no es válido, se renderiza la plantilla que se muestra a continuación.

Esta es la plantilla

{% extends 'base.html.twig' %}
{% block title %}Nuevo contacto
{% endblock %}
{% block body %}
	<h1>Nuevo contacto</h1>
	{{ form(formulario) }}
{% endblock %}

Si ahora accedemos a http://127.0.0.1:8080/contacto/nuevo podremos ver el formulario:

image-20220109174250630

Y al rellenar los datos y pulsar Enviar lo grabará y mostrará a continuación:

image-20260701120514312

Existen multitud de tipos de campo, entre los que están lo siguientes:

  • TextType (cuadros de texto de una sola línea, como el ejemplo anterior. Son el control por defecto)
  • TextareaType (cuadros de texto de varias líneas)
  • EmailType (cuadros de texto de tipo e­mail)
  • IntegerType (cuadros de texto para números enteros)
  • NumberType (cuadros de texto para números en general)
  • PasswordType (cuadros enmascarados para passwords)
  • EntityType (desplegables para elegir valores vinculados a otra entidad)
  • DateType (para fechas)
  • CheckboxType (para checkboxes)
  • RadioType (para botones de radio o radio buttons)
  • HiddenType (para controles ocultos)
  • … etc.

Puedes acceder a todos los tipos de campos aquí

2.2.1 Etiquetas personalizadas

Como podemos ver para el caso del botón de submit, podemos especificar un tercer parámetro en el método add que es un array de propiedades del control en cuestión. Una de ellas es la propiedad label, que nos permite especificar qué texto tendrá asociado el control. Por defecto, se asocia el nombre del atributo correspondiente en la entidad, pero podemos cambiarlo por un texto personalizado. Para el e­mail, por ejemplo, podríamos poner:

<?php
...
use Symfony\Component\Form\Extension\Core\Type\EmailType;
...
->add('email', EmailType::class, array('label' => 'Correo electrónico'))

image-20220109174433735

2.2.2 Modificación de datos

Lo que hemos hecho en el ejemplo anterior es una inserción de un nuevo contacto, pero… ¿cómo hacer una modificación de contacto existente?. El funcionamiento es muy similar, pero con un pequeño cambio: la ruta del controlador recibirá como parámetro el código del contacto a modificar, y a partir de ahí, buscaríamos el contacto y lo cargaríamos en el formulario, incluyendo su id. De esta forma, al hacer persist se modificaría el contacto existente.

Podemos probarlo con este controlador:

#[Route('/contacto/editar/{codigo}', name: 'editar', requirements:["codigo"=>"\d+"])]
public function editar(ManagerRegistry $doctrine, Request $request, int $codigo) {
    $repositorio = $doctrine->getRepository(Contacto::class);
    //En este caso, los datos los obtenemos del repositorio de contactos
    $contacto = $repositorio->find($codigo);
    if ($contacto){
        // A partir de $contacto, rellena automáticamente el formulario y el resto es igual que para nuevo
        $formulario = $this->createForm(ContactoFormType::class, $contacto);

        $formulario->handleRequest($request);

        if ($formulario->isSubmitted() && $formulario->isValid()) {
            // Guardamos y redirigimos a la ficha
            $contacto = $formulario->getData();
            $entityManager = $doctrine->getManager();
            $entityManager->persist($contacto);
            $entityManager->flush();
            return $this->redirectToRoute('contacto', ["codigo" => $contacto->getId()]);
        }
        // Ponemos los datos del contacto
        return $this->render('editar.html.twig', array(
            'formulario' => $formulario->createView()
        ));
    }else{
        return $this->render('contacto.html.twig', [
            'contacto' => NULL
        ]);
    }
}

Esta es la plantilla

{% extends 'base.html.twig' %}
{% block title %}Modificar contacto
{% endblock %}
{% block body %}
	<h1>Modificar contacto</h1>
	{{ form(formulario) }}
{% endblock %}

Ahora, si accedemos a http://127.0.0.1:8080/contacto/editar/1, por ejemplo (suponiendo que tengamos un contacto con id = 1 en la base de datos), se cargará el formulario con sus datos, y al enviarlo, se modificarán los campos que hayamos cambiado, y se redirigirá a la página del contacto con los datos ya modidificados.

2.3 Validación de formularios

Ahora que ya sabemos crear, enviar y gestionar formularios, veamos un último paso, que sería la validación de datos de dichos formularios previa a su envío. En el caso de Symfony, la validación no se aplica al formulario, sino a la entidad subyacente (es decir, a la clase Contacto, por ejemplo).

Por lo tanto, la validación la obtendremos añadiendo una serie de restricciones o comprobaciones a estas clases. Por ejemplo, para indicar que el nombre, teléfono y e­-mail del contacto no pueden estar vacíos, añadimos estas anotaciones en los atributos de la clase Contacto:

<?php
namespace App\Entity;

use App\Repository\ContactoRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: ContactoRepository::class)]
class Contacto
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    private ?string $nombre = null;

    #[ORM\Column(length: 15)]
    #[Assert\NotBlank]
    private ?string $telefono = null;

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    private ?string $email = null;
   
    public function getId(): ?int
    {
        return $this->id;
    }

    public function getNombre(): ?string
    {
        return $this->nombre;
    }

    public function setNombre(?string $nombre): static
    {
        $this->nombre = $nombre;

        return $this;
    }

    public function getTelefono(): ?string
    {
        return $this->telefono;
    }

    public function setTelefono(?string $telefono): static
    {
        $this->telefono = $telefono;

        return $this;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(?string $email): static
    {
        $this->email = $email;

        return $this;
    }

  
}

Estas aserciones repercuten directamente sobre el código HTML del formulario, donde se añadirá el atributo required para que se validen los datos en el cliente. Para probarlo, hay que modificar el atributo required mediante Firebug.

Además, en todos los setters hemos de modificar el valor devuelto para que se devuelva a sí mismo. Es lo que se llama fluent setter; esto permite encadenar los setters, por ejemplo: $contacto->setNombre('Juan')->setEmail('correo@c.com');

<?php
public function setTelefono(?string $telefono): static

En el caso del e­mail, además, podemos especificar que queremos que sea un e­mail válido, lo que se consigue con la anotación @Assert\Email:

Estas funciones de validación admiten una serie de parámetros útiles. Uno de los más útiles es message, que se emplea para determinar el mensaje de error que mostrar al usuario en caso de que el dato no sea válido. Por ejemplo, para el e­mail, podemos especificar este mensaje de error:

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    #[Assert\Email(message: "El email '{{ value }}' no es válido.")]
    private ?string $email = null;

Y se disparará cuando no escribamos un e­mail válido e intentemos enviar el formulario:

1549870910746

Recordad que para probar que funciona la validación en el lado del servidor debéis cambiar con las herramientas de desarrollador del navegador el tipo de campo a text

Puedes consultar más información aquí

2.4 Otras consideraciones finales

Para finalizar este apartado, veamos algunas cuestiones que hemos dejado en el tintero y no dejan de ser igualmente importantes.

2.4.1 Añadiendo estilo a los formularios

Los formularios que hemos generado en esta sesión son muy funcionales, pero poco vistosos, ya que carecen de estilos CSS propios. Si quisiéramos añadir CSS a estos formularios, tenemos varias opciones.

Una opción rudimentaria consiste en añadir clases (atributo class) a los controles del formulario para dar estilos personalizados. Después, en nuestro CSS bastaría con indicar el estilo para la clase en cuestión.

Pero Symfony ofrece diversos temas (themes) que podemos aplicar a los formularios (y webs en general) para darles una apariencia general tomada de algún framework conocido, como Bootstrap o Foundation. Si queremos optar por la apariencia de Bootstrap, debemos hacer lo siguiente: Incluir la hoja de estilos CSS y el archivo Javascript de Bootstrap en nuestras plantillas.

  1. Una práctica habitual es hacerlo en la plantilla base.html.twig para que cualquiera que herede de ella adopte este estilo. Para ello, en la sección stylesheets debemos añadir el código HTML que hay en la documentación oficial de Bootstrap para incluir su hoja de estilo, y en la sección javascripts los enlaces a las diferentes librerías que se indican en la documentación de Bootstrap también. Al final tendremos algo como esto:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <title>{% block title %}Welcome!{% endblock %}</title>
            <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
            {# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
            {% block stylesheets %}
                <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
                <link href="{{ asset('css/estilos.css') }}" rel="stylesheet" />
            {% endblock %}
    
            {% block javascripts %}
                {{ encore_entry_script_tags('app') }}
                <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
            {% endblock %}
        </head>
        <body>
            {% block body %}{% endblock %}
        </body>
    </html>
    
  2. Editar el archivo de configuración config/packages/twig.yaml e indicar que los formularios usarán el tema de Bootstrap (en este caso, Bootstrap 5):

    twig:
        # ....
        form_themes: ['bootstrap_5_horizontal_layout.html.twig']
    

Con estos dos cambios, la apariencia de nuestro formulario de contactos queda así:

1549872641935

Se tienen otras alternativas, como por ejemplo no indicar esta configuración general en config/packages/twig.yaml e indicar formulario por formulario el tema que va a tener:

{% form_theme formulario 'bootstrap_5_horizontal_layout.html.twig' %}
{{ form_start(formulario) }}
...

Existen también otros temas disponibles que utilizar. Podéis consultar más información aquí.

2.4.2 Añadir estilos para las validaciones

En el caso de las validaciones de datos del formulario, también podemos definir estilos para que los mensajes de error que se muestran (parámetro message o similares en las anotaciones de la entidad) tengan un estilo determinado. Esto se consigue fácilmente eligiendo alguno de los temas predefinidos de Symfony. Por ejemplo, eligiendo Bootstrap, la apariencia de los errores de validación queda así automáticamente:

1549872771901

Estamos hablando de las validaciones en el servidor, ya que las que se efectúan desde el cliente por parte del propio HTML5 no tienen un estilo controlable desde Symfony.

Podríamos desactivar esta validación para que todo corra a cargo del servidor, si fuera el caso. Para ello, basta con añadir un atributo novalidate en el formulario al renderizarlo:

{{ form_start(formulario, {'attr': {'novalidate': 'novalidate'}}) }}
...