2 de septiembre de 2026

Symfony 8: qué se rompe al actualizar desde 7.4 y cómo decidir entre LTS y la rama normal

Foto de Marco Orta Marco Orta | 14 min de lectura
Compartir
Una cinta cromada pulida curvándose en la oscuridad, cuyo extremo final se hace añicos en fragmentos afilados con luz turquesa
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 enEstado hoy (sep 2026)Qué hacer
    Symfony 8.0Sin mantenimiento desde el 30 jul 2026 — ni fixes ni parches de seguridadSube 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 2029Quédate, o brinca a 8.1 por las funciones nuevas. No hay urgencia de ningún lado
    Symfony 6.4 (LTS) o anterior6.4: bug fixes hasta nov 2026 y seguridad hasta nov 2027. De las anteriores solo 5.4 LTS recibe parches, y solo de seguridad (hasta feb 2029)Pasa por 7.4 primero, sin esperar a que se acabe la ventana de 6.4. 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ónTipoLanzamientoPHP mínimoBug fixes hastaSeguridad hasta
    7.4LTS27 nov 20258.2.0nov 2028nov 2029
    8.0Normal27 nov 20258.4.0jul 2026jul 2026
    8.1Normal29 may 20268.4.0ene 2027ene 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() y TokenInterface::eraseCredentials() se eliminaron. Usa __serialize() para controlar qué sobrevive en el token en su lugar.
    • Los listeners del firewall tienen que extender AbstractListener o implementar FirewallListenerInterface. Registrar un callable simple como listener ya no funciona.
    • hide_user_not_found desapareció; usa expose_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.

    7. Form: UrlType deja de adivinar un protocolo

    // Symfony <= 7.4: default_protocol usa 'http' por default
    // escribir "example.com" en el campo guarda "http://example.com"
    
    // Symfony >= 8.0: default_protocol usa null por default
    // escribir "example.com" guarda "example.com" — sin protocolo agregado
    

    Es de las pocas rupturas de 8.0 que no truenan. Desde 7.1, no configurar default_protocol emitía una deprecación; si nadie la leyó, en 8.0 el valor por default cambia sin avisar. 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. Para conservar el comportamiento anterior, declara 'default_protocol' => 'http' en el campo.


    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.

    8. 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.

    9. ParameterBag::getInt() / getBoolean() empiezan a lanzar excepción

    En dirección contraria al punto anterior: 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

    1. 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.
    2. Corre el reporte de deprecaciones y arregla lo que es tuyo:
    php bin/phpunit --display-deprecations
    

    Para que la suite falle solo por las deprecaciones de tu propio código, y no por las de paquetes de terceros que todavía no controlas, no busques el viejo SYMFONY_DEPRECATIONS_HELPER=weak_vendors: ese modo se eliminó en symfony/phpunit-bridge 5.0, y en PHPUnit 10 o superior el bridge ni siquiera registra su manejador de deprecaciones. Lo vigente es configurarlo en phpunit.dist.xml, como lo trae la recipe de Flex:

    <phpunit failOnDeprecation="true">
        <source ignoreSuppressionOfDeprecations="true" ignoreIndirectDeprecations="true">
            <include>
                <directory>src</directory>
            </include>
            <deprecationTrigger>
                <function>trigger_deprecation</function>
            </deprecationTrigger>
        </source>
    </phpunit>
    

    ignoreIndirectDeprecations descarta las deprecaciones que dispara código de terceros — útil para priorizar qué es de verdad accionable este sprint. Si tu suite sigue en PHPUnit 9 con simple-phpunit, el equivalente es SYMFONY_DEPRECATIONS_HELPER='max[self]=0'.

    1. Automatiza los renombres mecánicos con Rector. rector/rector-symfony trae 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.
    2. Actualiza dependencias y deja que Flex aplique las recipes nuevas:
    composer require symfony/symfony:^8.1 --with-all-dependencies
    
    1. 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/
    
    1. Compara tu árbol de config/ contra un symfony/skeleton fresco 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, y composer update no 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_found antes del salto, no durante.
    • Si estás en 6.4 o anterior: 6.4 LTS deja de recibir bug fixes en noviembre de 2026 y parches de seguridad en noviembre de 2027; las anteriores ya no reciben nada, salvo 5.4 LTS, que conserva solo los de seguridad hasta febrero de 2029. Pasa por 7.4, limpia cada deprecación con 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:

    Preguntas frecuentes

    ¿Symfony 8.0 todavía tiene soporte?

    No. Symfony 8.0 fue una versión normal (no LTS) con una ventana de soporte de ocho meses que cubría tanto bug fixes como parches de seguridad. Se lanzó el 27 de noviembre de 2025 y su soporte terminó el 30 de julio de 2026. Quien siga corriéndolo está en código sin mantenimiento y debería actualizar a 8.1.

    ¿Cuál es la diferencia entre Symfony 7.4 y Symfony 8.0?

    Salieron el mismo día, el 27 de noviembre de 2025, con funcionalidades idénticas. Symfony 7.4 es la versión LTS y conserva las capas de deprecación acumuladas desde 7.0, así que el código que usa APIs deprecadas sigue funcionando. Symfony 8.0 eliminó todas esas APIs deprecadas. Symfony 7.4 requiere PHP 8.2; Symfony 8.0 requiere PHP 8.4.

    ¿Qué cambio incompatible causa más fallas silenciosas en Symfony 8?

    La configuración en XML. Se deprecó en Symfony 7.4 como una línea de log que nadie suele monitorear, y luego se eliminó por completo en Symfony 8.0. Los equipos que actualizan sin limpiar primero las deprecaciones de 7.4 se topan con una falla dura en 8.0 que parece no tener relación con nada que hayan cambiado. Las cookies de autenticación remember-me son un segundo caso silencioso: su formato cambió y las cookies viejas dejan de autenticar sin ningún error, solo forzando un nuevo inicio de sesión.

    ¿Actualizar de Symfony 8.0 a 8.1 exige cambios de código?

    No. Symfony 8.1 es un minor sobre 8.0 y cae bajo la promesa de retrocompatibilidad. Las consideraciones principales son de comportamiento: Messenger ya no lanza excepción al fallar el decode de un mensaje, ParameterBag::getInt/getBoolean ahora lanzan excepción con input malo en vez de forzarlo en silencio, y el placeholder de un ChoiceType requerido se renderiza como hidden. El cambio de UrlType, que deja de agregar http:// a las URLs sin esquema, no es de 8.1: llegó con 8.0, así que si ya estás en 8.0 ya lo tienes.

    ¿Qué versión de PHP necesita Symfony 8?

    Symfony 8.0 y 8.1 requieren ambos PHP 8.4.0 o superior. Symfony 7.4 LTS solo requiere PHP 8.2.0 o superior, que es una de las razones por las que los equipos que todavía no pueden subir de versión de PHP eligen quedarse en 7.4 en vez de saltar a 8.x.

    ¿Debería actualizar a Symfony 8.1 o quedarme en 7.4 LTS?

    Si necesitas las funciones de PHP 8.4 o lo más nuevo de Symfony y puedes seguir el ritmo semestral de lanzamientos, ve a 8.1. Si quieres una ventana de soporte larga y tranquila sin perseguir cada minor, quédate en 7.4: recibe bug fixes hasta noviembre de 2028 y parches de seguridad hasta noviembre de 2029, sin fecha límite que te empuje.

    Compartir

    Buscar

    Etiquetas

    Migración IA PHP JavaScript Laravel Tutorial Desarrollo Web Upgrade Buenas Prácticas Seguridad SEO Backend Claude Laravel 13 TypeScript