¿Qué es Symfony?
Symfony es dos cosas a la vez. Es un conjunto de componentes de PHP reutilizables —HttpFoundation, Routing, Console, Mailer, Serializer y docenas más— y es un framework web completo ensamblado a partir de esos componentes. Esta naturaleza dual explica casi todo sobre él: el framework es estable porque sus partes son estables, y sus partes son útiles por sí solas.
Creado por Fabien Potencier en 2005 y mantenido por SensioLabs, Symfony se ha convertido en el motor del PHP moderno. Laravel está construido sobre componentes de Symfony, Drupal y phpBB dependen de ellos, y miles de librerías los utilizan sin llegar a tocar el framework completo.
Symfony te exige más que un framework “batteries-included”. A cambio, te ofrece un sistema sobre el cual puedes razonar, extender y actualizar durante años.
Componentes: el producto real
Los componentes son el corazón del proyecto. Cada uno resuelve un único problema y no tiene dependencias del framework.
- HttpFoundation — clases
RequestyResponseorientadas a objetos. - Routing — mapeo de URLs a controladores mediante atributos, YAML o PHP.
- Console — creación de herramientas de línea de comandos con argumentos y opciones.
- Mailer — envío de correos electrónicos a través de transports y mensajes basados en plantillas.
- Serializer — conversión de objetos a JSON, XML y viceversa.
- EventDispatcher — desacoplamiento de código mediante eventos y listeners.
Puedes instalar cualquiera de ellos directamente:
composer require symfony/http-foundation
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
$page = $request->query->getInt('page', 1);
Esa portabilidad es la razón por la cual los componentes de Symfony aparecen en todas partes, incluso en proyectos que nunca adoptarían el framework completo.
El kernel de HTTP y los controladores
En una aplicación completa, cada solicitud fluye a través del HttpKernel. Este despacha un evento, coincide con una ruta, llama a un controlador y convierte el Response devuelto en una salida. Los controladores extienden AbstractController para obtener helpers convenientes.
class BlogController extends AbstractController
{
#[Route('/blog/{slug}', name: 'blog_show')]
public function show(string $slug, PostRepository $posts): Response
{
$post = $posts->findOneBy(['slug' => $slug]);
if (!$post) {
throw $this->createNotFoundException('Post not found');
}
return $this->render('blog/show.html.twig', ['post' => $post]);
}
}
Un controlador debe devolver un Response. $this->render() construye uno a partir de una plantilla de Twig, $this->json() devuelve JSON y $this->redirectToRoute() devuelve una redirección. Debido a que el objeto de respuesta es explícito, las pruebas y el middleware son sencillos.
Las plantillas de Twig extienden un diseño base y rellenan bloques, y la salida se escapa a menos que decidas lo contrario:
{# templates/blog/show.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.publishedAt|date('F j, Y') }}</p>
{{ post.body|raw }}
</article>
{% endblock %}
Enrutamiento por atributos
Las rutas se declaran junto a la acción que las maneja utilizando atributos de PHP 8.
#[Route('/blog', name: 'blog_index', methods: ['GET'])]
#[Route('/blog/{slug}', name: 'blog_show', methods: ['GET'])]
El enrutamiento también puede residir en config/routes.yaml cuando prefieras mantenerlo separado. Los atributos son la opción predeterminada en el Symfony moderno porque la ruta y el método permanecen juntos. Puedes inspeccionar qué está registrado en cualquier momento con:
php bin/console debug:router
El contenedor de servicios y el autowiring
Casi todo en Symfony es un servicio, y el contenedor se encarga de construirlos y conectarlos. Con el autowiring habilitado, solo necesitas añadir el type-hint en el constructor.
class PostPublisher
{
public function __construct(
private EntityManagerInterface $em,
private MailerInterface $mailer,
) {}
}
config/services.yaml indica al contenedor que debe aplicar autowiring a tus clases:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/Entity/'
- '../src/Kernel.php'
Cuando existan dos implementaciones de una misma interfaz, vincula la correcta de forma explícita. El contenedor se compila a PHP puro para producción, razón por la cual es rápido a pesar de ser tan dinámico al escribirlo.
Bundles y recetas de Flex
Un bundle empaqueta funcionalidades en una unidad distribuible. La funcionalidad de terceros —Doctrine, seguridad, mailer, paneles de administración— llega en forma de bundle, y el propio framework está compuesto por bundles principales.
Instalar uno requiere un solo comando:
composer require symfony/orm-pack
Symfony Flex es el plugin de Composer que hace que este proceso sea sencillo. Cuando un paquete tiene una receta, Flex crea los archivos de configuración, registra el bundle y, a menudo, añade variables de entorno o un servicio docker-compose. Es por esto que las aplicaciones modernas de Symfony tienen una configuración pequeña y explícita en lugar de páginas enteras de código repetitivo.
Doctrine ORM
Doctrine es la capa de persistencia predeterminada. Las entidades son clases PHP sencillas anotadas con atributos de mapeo, y los repositorios encapsulan las consultas.
#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $title;
#[ORM\Column(type: 'text')]
private string $body;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $publishedAt = null;
}
Las consultas suelen expresarse mediante la API del repositorio o DQL en lugar de cadenas SQL:
$latest = $posts->findBy([], ['publishedAt' => 'DESC'], limit: 10);
Los cambios de esquema se gestionan con Doctrine Migrations, que comparan tu mapeo con la base de datos y generan archivos de migración versionados que puedes revisar antes de ejecutarlos.
Configuración y entornos
Symfony separa la configuración del código y la aplica por entorno. APP_ENV selecciona el entorno, y los archivos bajo config/ se superponen entre sí.
.envcontiene los valores predeterminados locales y las variables de entorno.config/packages/configura los bundles para todos los entornos.config/packages/dev/yconfig/packages/prod/sobrescriben la configuración por entorno.config/routes.yamlyconfig/services.yamlson los ajustes propios de la aplicación.
El entorno dev habilita el profiler y las herramientas de depuración, test utiliza una base de datos independiente y prod compila todo para optimizar la velocidad. Limpiar la caché después de realizar cambios en la configuración es una parte normal del flujo de trabajo:
php bin/console cache:clear
Messenger y trabajo asíncrono
El componente Messenger desplaza la carga de trabajo fuera de la solicitud. Envías un mensaje a un bus y un handler lo procesa, ya sea de forma inmediata o mediante un worker.
final class UserRegistered
{
public function __construct(public readonly int $userId) {}
}
#[AsMessageHandler]
final class SendWelcomeEmail
{
public function __invoke(UserRegistered $message): void
{
// send the email here
}
}
Envíalo desde cualquier lugar y consume la cola en un proceso independiente:
$bus->dispatch(new UserRegistered($user->getId()));
php bin/console messenger:consume async
Los transports pueden ser Doctrine, Redis, AMQP o un servicio de terceros, y los mensajes fallidos pueden reintentarse o almacenarse, lo que convierte el procesamiento en segundo plano en una funcionalidad de primer nivel.
Pruebas con PHPUnit
Symfony incluye PHPUnit y una clase base WebTestCase que arranca el kernel y realiza solicitudes HTTP reales.
class BlogControllerTest extends WebTestCase
{
public function testShowPost(): void
{
$client = static::createClient();
$client->request('GET', '/blog/hello-world');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'Hello world');
}
}
Dado que los servicios se inyectan, las pruebas unitarias pueden reemplazar una dependencia con un stub o un mock sin tocar el contenedor. Reserva las pruebas de integración para la capa HTTP y las pruebas unitarias para los servicios que contienen las reglas de negocio.
Mejores prácticas
- Prefiere la inyección por constructor en lugar de obtener servicios directamente del contenedor.
- Mantén los controladores ligeros; mueve la lógica a los servicios.
- Utiliza repositorios para las consultas y evita escribir SQL en los controladores.
- Declara las rutas como atributos junto a la acción que invocan.
- Deja que Flex gestione la configuración del bundle en lugar de escribir código repetitivo.
- Separa la configuración por entorno y guarda los secretos en variables de entorno.
- Delega las tareas lentas a Messenger en lugar de procesarlas durante la solicitud.
- Consulta el profiler cuando algo sea lento o sorprendente.
Errores comunes
- Llamar a
$container->get()en lugar de inyectar dependencias. - Colocar lógica de negocio en controladores y entidades.
- Olvidar registrar o vincular una interfaz y luego preguntarse qué resolvió el contenedor.
- Hacer commit de secretos en
.enven lugar de mantenerlos fuera del control de versiones. - Editar migraciones generadas después de que se hayan ejecutado.
- Mezclar estilos de configuración aleatoriamente entre YAML, XML y PHP.
- Ignorar los avisos de deprecación hasta la siguiente actualización mayor.
Próximos pasos
Symfony te proporciona los componentes y la disciplina necesarios para construir sistemas duraderos, y sus ideas son evidentes en diversos frameworks de todo el ecosistema. Lee la guía de Laravel para ver cómo se utilizan esos componentes para el desarrollo rápido de aplicaciones, o compara su filosofía con el enfoque minimalista de Express. Para diseñar y evolucionar las API que expongas, continúa con REST y versionado de API.