Symfony 8: qué se rompe al actualizar desde 7.4 y cómo decidir entre LTS y la rama normal
Tabla de Contenidos
Si estás en Symfony 8.0, llevas corriendo código sin soporte desde el 30 de julio de 2026 — 8.0 tuvo exactamente ocho meses de vida y ya se acabaron. Si estás en 7.4, tienes años por delante, pero igual te toca decidir si te quedas en el LTS o brincas a 8.1, publicado el 29 de mayo de 2026. Ninguno de los dos caminos es una reescritura. Los dos tienen cambios que no lanzan error — simplemente cambian en silencio qué hace tu aplicación.
Symfony 7.4 y 8.0 salieron el mismo día, el 27 de noviembre de 2025, con funcionalidades idénticas. La única diferencia es que 7.4 conservó las capas de deprecación acumuladas desde 7.0, y 8.0 las borró todas. Esa es toda la historia de “Symfony 8”: es 7.4 sin la red de seguridad. 8.1, que salió medio año después, sí agrega funcionalidad nueva encima y es la versión a la que llegas si sigues la rama normal (no LTS).
¿De dónde vienes?
| Estás en | Estado hoy (sep 2026) | Qué hacer |
|---|---|---|
| Symfony 8.0 | Sin mantenimiento desde el 30 jul 2026 — ni fixes ni parches de seguridad | Sube a 8.1 ya. Es el mismo código sin deprecaciones, sin cambios obligatorios bajo la promesa de BC |
| Symfony 7.4 (LTS) | Bug fixes hasta nov 2028, seguridad hasta nov 2029 | Quédate, o brinca a 8.1 por las funciones nuevas. No hay urgencia de ningún lado |
| Symfony 6.4 o anterior | Ya sin soporte de seguridad | Pasa por 7.4 primero. Corrige cada deprecación ahí con phpunit --display-deprecations, y luego decide entre 7.4-LTS y 8.x |
La tabla de referencia, tal cual sale de symfony.com/releases:
| Versión | Tipo | Lanzamiento | PHP mínimo | Bug fixes hasta | Seguridad hasta |
|---|---|---|---|---|---|
| 7.4 | LTS | 27 nov 2025 | 8.2.0 | nov 2028 | nov 2029 |
| 8.0 | Normal | 27 nov 2025 | 8.4.0 | jul 2026 | jul 2026 |
| 8.1 | Normal | 29 may 2026 | 8.4.0 | ene 2027 | ene 2027 |
Hay dos cosas que vale la pena leer dos veces: toda la ventana de soporte de 8.0 fue de ocho meses, bug fixes y seguridad juntos — y esa es la política estándar para un minor no-LTS, no una recortada a propósito. Y 8.1 exige PHP 8.4, dos versiones completas por delante de lo que pide 7.4. Si sigues en PHP 8.2 u 8.3, subir la versión de PHP es trabajo de infraestructura real, aparte de todo lo que sigue en esta lista — hazlo primero, verifícalo, y hasta entonces toca composer.json.
Impacto alto
1. La configuración en XML desapareció — y rompió en silencio primero
Esta es la que agarra a la gente desprevenida, porque no falla el día que debería. Symfony 7.4 deprecó la configuración en XML; Symfony 8.0 la eliminó por completo. Si tus bundles, rutas o servicios están configurados en XML y nadie estaba revisando el log de deprecaciones durante la ventana de 7.4, el salto a 8.0 u 8.1 es un alto total: el loader ya no existe, no es que esté desaconsejado.
// Eliminado sin alternativa en Symfony 8.0
ExtensionInterface::getXsdValidationBasePath()
ExtensionInterface::getNamespace()
Routing, FrameworkBundle y WebProfilerBundle ya no cargan rutas en XML, punto. YAML sigue siendo el formato por default y sigue totalmente soportado. PHP es la alternativa recomendada para configuración basada en código, y también cambió de forma (ver el punto 4). Existe un convertidor automático para mantenedores de bundles — symfony-config-xml-to-php —, pero la configuración XML a nivel de aplicación la vas a reescribir a mano.
La razón por la que esto califica como “rompe en silencio”: el aviso de deprecación en 7.4 es nada más una línea de log. Nadie hace grep de logs de deprecación con regularidad. La falla aparece meses después, en el upgrade a 8.x, como un error fatal que parece no tener nada que ver con lo que tocaste esa semana.
2. Security: credenciales borradas, listeners del firewall, exposición de errores
Tres cambios caen en el mismo componente y se acumulan:
UserInterface::eraseCredentials()yTokenInterface::eraseCredentials()se eliminaron. Usa__serialize()para controlar qué sobrevive en el token en su lugar.- Los listeners del firewall tienen que extender
AbstractListenero implementarFirewallListenerInterface. Registrar un callable simple como listener ya no funciona. hide_user_not_founddesapareció; usaexpose_security_errors.
# Symfony <= 7.x
security:
hide_user_not_found: false
# Symfony >= 8.0
security:
expose_security_errors: account_status # none | account_status | all
expose_security_errors es más preciso que el booleano al que reemplaza — account_status muestra excepciones solo a usuarios que ya dieron la contraseña correcta, all no oculta nada, none es el default seguro. Si tu security.yaml todavía trae hide_user_not_found, el contenedor no compila en 8.x. Ese es ruidoso. Lo que no es ruidoso: las cookies de remember-me. RememberMeDetails ya no incluye el nombre completo de la clase del usuario dentro del payload de la cookie. Las cookies viejas, emitidas antes del upgrade, dejan de autenticar en silencio — al usuario no le sale ningún error, simplemente se le cierra la sesión y tiene que volver a iniciarla. Si manejas una app con tráfico alto, espera un pico de tickets de soporte la semana que despliegues, no una alerta de incidente.
3. Comandos de consola: se eliminaron los métodos estáticos de metadata
// Symfony <= 7.x
class SyncCommand extends Command
{
protected static function getDefaultName(): string
{
return 'app:sync';
}
}
// Symfony >= 8.0
#[AsCommand(name: 'app:sync')]
class SyncCommand extends Command
{
}
Command::getDefaultName() y getDefaultDescription() se eliminaron — usa el atributo #[AsCommand]. Application::add() se reemplaza por Application::addCommand(). Si registras comandos de forma dinámica instanciando subclases de Command y metiéndolas a la aplicación, esto es un cambio mecánico pero obligatorio.
Impacto medio
4. La config PHP fluida desapareció; hay un formato nuevo de array-shape
Symfony 5.3 introdujo un formato de configuración PHP fluido, tipo builder (SecurityConfig, FrameworkConfig y compañía). Se eliminó en 8.0. Lo que lo reemplaza no es un regreso a los arrays de siempre — es un formato PHP nuevo de array-shape con metadata tipada que los IDEs y los analizadores estáticos sí pueden entender.
// Eliminado: configuración PHP fluida
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security) {
$security->firewall('main')->pattern('^/*')->lazy(true);
};
// Nuevo: configuración PHP array-shape
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'security' => [
'firewalls' => [
'main' => ['pattern' => '^/*', 'lazy' => true],
],
],
]);
La razón declarada es que los builders fluidos no podían representar cualquier forma semántica del árbol de configuración, lo que hacía innecesariamente difícil mantener actualizadas las recipes de Symfony de forma automática. YAML sigue siendo el default recomendado; este formato es para equipos que específicamente quieren PHP.
5. Doctrine: ya no auto-mapea entidades en los argumentos del controller
El auto-mapeo estilo ParamConverter, el que resolvía parámetros de ruta directo en argumentos de entidades Doctrine, se eliminó. Si una acción de un controller tipaba una entidad Doctrine y confiaba en que el bundle la resolviera implícitamente desde la ruta, esa resolución ya no existe — ahora lo cableas explícitamente con #[MapEntity] o la obtienes tú mismo dentro del cuerpo de la acción. DoctrineExtractor::getTypes() también desapareció; usa getType().
6. HttpFoundation: Request::get() eliminado, el method override se acotó
// Eliminado
$request->get('id');
// Usa el bag específico
$request->attributes->get('id');
$request->query->get('id');
$request->request->get('id');
Request::get() buscaba primero en attributes, luego en query, luego en el body de la petición, en ese orden — cómodo, y una fuente frecuente de bugs cuando dos bags tenían la misma llave. Ya no existe; hay que ser explícito sobre qué bag quieres. Aparte, el method override por HTTP (X-HTTP-METHOD-OVERRIDE / _method) ya no aplica a GET, HEAD, CONNECT ni TRACE — esos métodos están pensados para ser seguros e idempotentes, y ahora Symfony lo hace cumplir en la capa de request en vez de confiar en el header de override.
Novedades de 8.1 que cambian el comportamiento sin lanzar error
Esta sección vale la pena leerla con calma si ya pasaste 8.0 y estás evaluando 8.1, porque ninguno de estos cambios lanza excepción — simplemente hacen algo distinto.
7. Messenger ya no lanza excepción al fallar el decode
Los serializers ahora regresan un Envelope que envuelve una MessageDecodingFailedException en vez de lanzarla. Si tu transporte o middleware capturaba MessageDecodingFailedException alrededor del decode, ese bloque catch deja de dispararse. La falla ya no se expone como excepción — es un dato dentro del envelope que tienes que revisar tú. El código que no se actualice para esto va a procesar (o descartar) en silencio un fallo de decode que antes era imposible de pasar por alto.
8. UrlType deja de adivinar un protocolo
// Symfony <= 8.0: default_protocol usa 'http' por default
// escribir "example.com" en el campo guarda "http://example.com"
// Symfony >= 8.1: default_protocol usa null por default
// escribir "example.com" guarda "example.com" — sin protocolo agregado
Si tus formularios usan UrlType y después muestras o enlazas ese valor asumiendo que siempre trae un esquema, ahora ya no lo trae a menos que el usuario lo haya escrito. Nada lanza error; te queda un string con pinta de ruta relativa que se comporta como un link roto la primera vez que alguien le da clic.
9. ParameterBag::getInt() / getBoolean() empiezan a lanzar excepción
En dirección contraria a los dos puntos anteriores: estos métodos antes forzaban en silencio un valor no convertible a 0 o false. En 8.1 lanzan UnexpectedValueException en su lugar. Este es un caso donde 8.1 convierte una falla anterior que era silenciosa en una ruidosa — bueno para detectar input malo, pero significa que el código que dependía del fallback viejo ahora necesita un try/catch o validación previa.
10. El ChoiceType colapsado y requerido renderiza su placeholder como hidden
Un campo ChoiceType requerido y no expandido ahora marca su <option> de placeholder con el atributo hidden en vez de ser nada más una opción de valor vacío. Es puramente visual, pero si tienes JavaScript o CSS que selecciona sobre la opción de placeholder, o pruebas de UI automatizadas que hacen assert sobre ese markup, es un cambio silencioso en el DOM. Puedes restaurar el comportamiento anterior con placeholder_attr: [] si lo necesitas.
Lo que NO se rompe
- Symfony 8.0 y 8.1 no son “el siguiente 7.x”. Eliminaron deprecaciones; no agregaron una ola de comportamiento incompatible nuevo más allá de esa eliminación. La mayor parte de lo que rompe en el salto 7.4→8.0 es algo que ya estaba deprecado y registrado en 7.x — si limpiaste deprecaciones en 7.4, el upgrade a 8.0 es casi un no-evento.
- YAML como configuración no va a ningún lado. Es explícitamente el formato que las recipes de Symfony siguen apuntando.
- El upgrade de 8.0 a 8.1 no exige cambios de código bajo la promesa de retrocompatibilidad — 8.1 es un minor sobre 8.0, no un major.
El flujo real de migración
- Aterriza en 7.4 primero si no estás ahí. Aunque tu destino sea 8.x, subir al LTS te da la capa de deprecación contra la cual trabajar.
- Corre el reporte de deprecaciones y arregla lo que es tuyo:
php bin/phpunit --display-deprecations
# o, a nivel de proyecto con el bridge:
SYMFONY_DEPRECATIONS_HELPER=weak_vendors php bin/phpunit
weak_vendors solo hace fallar la suite por deprecaciones que dispara tu propio código, no las de paquetes de terceros que todavía no controlas — útil para priorizar qué es de verdad accionable este sprint.
- Automatiza los renombres mecánicos con Rector.
rector/rector-symfonytrae sets de upgrade que cubren buena parte de esta lista —getDefaultName()a atributo, llamadas a métodos deprecados y similares — sin editar archivo por archivo a mano. - Actualiza dependencias y deja que Flex aplique las recipes nuevas:
composer require symfony/symfony:^8.1 --with-all-dependencies
- Haz grep de lo específico de tu app antes de asumir que la automatización ya lo cubrió:
grep -rln '<?xml' config/
grep -rn "hide_user_not_found\|->get(" config/ src/
grep -rn "getDefaultName\|getDefaultDescription" src/
- Compara tu árbol de
config/contra unsymfony/skeletonfresco en la rama destino. La mayor parte de lo que está en esta lista vive en archivos de configuración, no en código del framework, ycomposer updateno te toca la configuración.
¿Cuándo deberías actualizar?
- Si ya estás en 8.0: muévete a 8.1 de inmediato. No hay costo de cambio de código bajo la promesa de BC, y cada día después del 30 de julio de 2026 es un día sin parches de seguridad.
- Si estás en 7.4, estable, sin presión por funciones nuevas: quédate. Tienes hasta noviembre de 2028 para bug fixes y noviembre de 2029 para seguridad. No hay reloj empujándote a moverte.
- Si estás en 7.4 y quieres lo más nuevo o planeas seguir el ritmo de la rama normal: ve a 8.1 ya, y luego sigue los minors semestrales (8.2 llega en noviembre de 2026). Arregla las deprecaciones de config en XML, config PHP fluida y
hide_user_not_foundantes del salto, no durante. - Si estás en 6.4 o anterior: ya estás sin soporte. Pasa por 7.4, limpia cada deprecación con el bridge de PHPUnit, y luego elige entre LTS y rama normal según qué tanto quieras lo más nuevo contra una ventana de soporte larga y tranquila.
Para seguir leyendo:
- Las novedades de PHP 8.6
- Qué se rompe al actualizar a Laravel 13
- Los mejores IDEs para desarrolladores PHP