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 claseContacto$formulario = $this->createForm(ContactoFormType::class, $contacto);Crea el formularioContactoFormTypey le pasa el$contacto$formulario->handleRequest($request);Symfony se encarga de manejar el formularioif ($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 delcontacto$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 rutaficha_contactoespera un parámetro llamadocodigoreturn $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:

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

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 email)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 email, por ejemplo, podríamos poner:
<?php
...
use Symfony\Component\Form\Extension\Core\Type\EmailType;
...
->add('email', EmailType::class, array('label' => 'Correo electrónico'))

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 email, además, podemos especificar que queremos que sea un email 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 email, 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 email válido e intentemos enviar el formulario:

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.
Una práctica habitual es hacerlo en la plantilla
base.html.twigpara que cualquiera que herede de ella adopte este estilo. Para ello, en la secciónstylesheetsdebemos 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>Editar el archivo de configuración
config/packages/twig.yamle 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í:

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:

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'}}) }}
...