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, o para insertar. Pero, en este último caso, al no disponer aún de un mecanismo para que se envíen datos de inserción desde el cliente, hemos optado por ahora por insertar unos datos prefijados o dummy data, es decir, un contacto con unos datos ya predefinidos en el código.
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
3.1 Creación de la clase para el formulario
En primer lugar, hemos de instalar la dependencia para crear formularios y validarlos
composer require form validator
Para crear el formulario usaremos el maker bundle:
php bin/console make:form ContactoForm Contacto
Donde ContactoForm 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 App\Entity\Provincia;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
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('provincia', EntityType::class, [
'class' => Provincia::class,
'choice_label' => 'nombre',
])
->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, excepto el campo provincia que lo crea de tipo Entity porque es una clave ajena a la entidad Provincia. Además le hemos añadido un botón para enviar el formulario.
3.2 Creación del controlador
Vamos a crear un método para renderizar el formulario:
...
use App\Form\ContactoFormType as ContactoType;
use Symfony\Component\HttpFoundation\Request;
...
#[Route('/contacto/nuevo', name: 'nuevo')]
public function nuevo(ManagerRegistry $doctrine, Request $request) {
$contacto = new Contacto();
$formulario = $this->createForm(ContactoType::class, $contacto);
$formulario->handleRequest($request);
if ($formulario->isSubmitted() && $formulario->isValid()) {
$contacto = $formulario->getData();
$entityManager = $doctrine->getManager();
$entityManager->persist($contacto);
$entityManager->flush();
return $this->redirectToRoute('ficha_contacto', ["codigo" => $contacto->getId()]);
}
return $this->render('nuevo.html.twig', array(
'formulario' => $formulario->createView()
));
}
- la línea 3 crea un objecto de la clase
Contacto - la línea 4 crea el formulario mediante la clase que define el formulario y la entidad base del mismo
- la línea 6 comprueba si el formulario ha sido enviado y también comprueba si es válido, que veremos más adelante.
- la línea 7 fija los datos de la entidad con los datos del formulario
- las lineas 9-11 guardan los datos en la BD.
- la línea 12 hace que se muestre la ruta
ficha_contacto(que es elnameque hemos puesto en la ruta definida en el controlador) - las líneas 14-17 permiten renderizar la plantilla, pasándole un parámetro llamado
formulario
Plantilla
Esta es la plantilla
{% extends 'base.html.twig' %}
{% block title %}Nuevo contacto{% endblock %}
{% block body %}
<h1>Nuevo contacto</h1>
{{ form(formulario) }}
{% endblock %}
la línea 6 renderiza el plantilla
Si ahora accedemos a http://127.0.0.1:8080/contacto/nuevo podremos ver el formulario:

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í
3.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'))

3.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 es 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){
$formulario = $this->createForm(ContactoType::class, $contacto);
$formulario->handleRequest($request);
if ($formulario->isSubmitted() && $formulario->isValid()) {
//Esta parte es igual que en la ruta para insertar
$contacto = $formulario->getData();
$entityManager = $doctrine->getManager();
$entityManager->persist($contacto);
$entityManager->flush();
return $this->redirectToRoute('ficha_contacto', ["codigo" => $contacto->getId()]);
}
return $this->render('nuevo.html.twig', array(
'formulario' => $formulario->createView()
));
}else{
return $this->render('ficha_contacto.html.twig', [
'contacto' => NULL
]);
}
}
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 cargará la página de inicio.
3.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;
#[ORM\ManyToOne(inversedBy: 'contactos')]
#[Assert\NotBlank]
private ?Provincia $provincia = null;
public function getId(): ?int
{
return $this->id;
}
public function getNombre(): ?string
{
return $this->nombre;
}
public function setNombre(?string $nombre): self
{
$this->nombre = $nombre;
return $this;
}
public function getTelefono(): ?string
{
return $this->telefono;
}
public function setTelefono(?string $telefono): self
{
$this->telefono = $telefono;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): self
{
$this->email = $email;
return $this;
}
public function getProvincia(): ?Provincia
{
return $this->provincia;
}
public function setProvincia(?Provincia $provincia): self
{
$this->provincia = $provincia;
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 devulto para que se devuelva a sí mismo. Es lo que se llama fluent setter; esto permite encadenar los setters, por ejemplo: $contacto->setNombre()->setEmail();
<?php
public function setTelefono(?string $telefono): self
En el caso del email, además, podemos especificar que queremos que sea un email válido, lo que se consigue con esta otra anotación:
<?php
/**
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank()
*/
private $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:
<?php
/**
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank()
* @Assert\Email(message="El email {{ value }} no es válido")
*/
private $email;
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í
3.4 Otras consideraciones finales
Para finalizar este apartado, veamos algunas cuestiones que hemos dejado en el tintero y no dejan de ser igualmente importantes.
3.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í.
3.4.1 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'}}) }}
...