PHP

PHP 9: qué se rompe, cómo afecta a Laravel y Symfony y cómo prepararte hoy

Autorangel cruz
Publicado
Lectura12 min de lectura
PHP 9: qué se rompe, cómo afecta a Laravel y Symfony y cómo prepararte hoy

PHP 9 no tiene fecha de lanzamiento, pero ya sabemos casi todo lo que va a romper. En el índice de RFC de php.net hay diez propuestas aprobadas que llevan literalmente "PHP 9.0" en el título, esperando a que se abra la rama. Con eso se puede armar la lista de lo que desaparece.

La otra mitad de la lista viene de PHP 8.6, que sale el 19 de noviembre de 2026. Su RFC de deprecaciones dice en la introducción que lo que se deprecia ahí se elimina en PHP 9.

¿Cuándo sale PHP 9?

No hay calendario. El wiki de php.net publica una página de tareas por versión, y para PHP 8.6 existe con fechas concretas. Para PHP 9.0 no existe ninguna. Lo único publicado es el calendario de 8.6:

Fecha Hito
13 ago 2026 Beta 1, congelación blanda de features
22 sep 2026 Congelación dura de features
24 sep 2026 RC1
5 nov 2026 RC4
19 nov 2026 GA

Los release managers electos de 8.6 son Daniel Scherzer como veterano y Matteo Beccati y Joe Ferguson como novatos.

En septiembre de 2026, PHP 8.6 ya cerró la puerta a nuevas features y PHP 9 sigue sin rama, sin alpha y sin release managers elegidos. Cualquier artículo que te dé una fecha se la está inventando.

Mientras tanto, las ramas soportadas hoy son estas, según la tabla oficial de php.net:

Rama Lanzamiento Fin soporte activo Fin soporte de seguridad
8.2 8 dic 2022 31 dic 2024 31 dic 2026
8.3 23 nov 2023 31 dic 2025 31 dic 2027
8.4 21 nov 2024 31 dic 2026 31 dic 2028
8.5 20 nov 2025 31 dic 2027 31 dic 2029

Si sigues en PHP 8.2, la fecha que te aprieta es el 31 de diciembre de 2026, cuando se acaban los parches de seguridad.

Qué cambia en PHP 9: los RFC ya aprobados

Estos son los cambios que el proyecto ya votó y que solo esperan a que exista la rama de PHP 9. Están todos en la sección "Pending Implementation / Landing" del índice de RFC.

Leer una variable indefinida lanzará un Error

Hoy leer $noDefinida emite un E_WARNING y se comporta como null. En PHP 9 lanzará una excepción Error y la ejecución se detiene.

if ($user->admin) {
    $restricted = false;
}
 
if ($restricted) { // PHP 8.x: Warning. PHP 9: Error.
    die('No tienes permiso para estar aquí');
}

El RFC identifica tres formas de llegar ahí: una rama que no se ejecutó, un typo en el nombre y un contador sin inicializar. Las dos primeras son casi siempre bugs. La solución es compatible hacia atrás: inicializa la variable antes.

$restricted = true;
if ($user->admin) {
    $restricted = false;
}

isset(), empty() y el operador ?? siguen contemplando valores no definidos, así que no les afecta.

Leer una propiedad indefinida, igual

Mismo tratamiento para $obj->propiedadQueNoExiste: hoy es un warning que devuelve null, en PHP 9 es una excepción. Esto muerde sobre todo en código que hidrata objetos desde json_decode() sin validar. Si la clase define __get(), el acceso sigue rutando ahí y no cambia nada.

Se acaba la interpolación con ${}

Deprecada desde PHP 8.2, se elimina en 9.0. Quedan solo dos formas de interpolar:

// Se van
echo "${foo}";
echo "${foo['bar']}";
 
// Se quedan
echo "$foo";
echo "{$foo['bar']}";

++ y -- dejarán de ser mágicos con strings

El RFC de operadores de incremento saneados se implementó por partes: PHP 8.3 añadió str_increment() y str_decrement(), y PHP 9 termina el trabajo. En la versión mayor, $v++ se comportará exactamente igual que $v += 1:

  • Los valores bool y null se convierten primero a entero, en vez de no hacer nada.
  • Los strings no numéricos lanzan TypeError.

Eso significa que el clásico $s = "a9"; $s++; que hoy devuelve "b0" dejará de funcionar. Si dependes de esa secuencia tipo hoja de cálculo, ya tienes reemplazo:

$col = 'az';
$col = str_increment($col); // "ba"

Fuera la interfaz Serializable

El plan de retirada se aprobó en 2020 y la deprecación entró en PHP 8.1. En PHP 9 la interfaz Serializable se elimina y unserialize() rechazará los payloads con el formato de serialización C. El reemplazo son __serialize() y __unserialize(), disponibles desde PHP 7.4.

Fuera las sesiones por GET/POST

Se deprecaron en PHP 8.4 y en PHP 9 desaparecen estos ajustes del INI, junto con la constante SID:

session.use_only_cookies
session.use_trans_sid
session.trans_sid_tags
session.trans_sid_hosts
session.referer_check

La deprecación solo salta si tienes esos valores fuera de su posición por defecto. Con session.use_only_cookies=On y session.use_trans_sid=Off, que es lo normal, no verás nada. La reescritura automática de URLs para colar PHPSESSID es lo que se pierde. Iniciar una sesión con un ID recibido por GET seguirá siendo posible a mano con session_id(), y session.use_cookies no se toca.

Y tres más, en la misma cola

  • Autovivificación sobre false: $arr = false; $arr[] = 2; está deprecado desde 8.1 y se elimina.
  • Tipos implícitamente nullable: function f(string $s = null) está deprecado desde 8.4. Escríbelo como ?string $s = null.
  • Parámetro obligatorio después de uno opcional: pasa de deprecación a error.

Las deprecaciones de PHP 8.6 son la lista de bajas de PHP 9

El RFC "Deprecations for PHP 8.6" lo dice sin rodeos en su introducción: propone deprecar en 8.6 y eliminar en PHP 9. Se votó punto por punto, cada uno con mayoría de dos tercios. Esto es lo que pasó el corte y que, por tanto, deberías dejar de escribir hoy:

Deprecado en 8.6 Votación (sí/no/abst.) Qué usar
return dentro de un bloque finally 39 / 3 / 4 Sacar el return fuera
Funciones llamadas readonly 39 / 1 / 2 Renombrar
let como identificador 24 / 11 / 9 Renombrar
is como identificador 29 / 10 / 6 Renombrar
_ como constante y alias 34 / 4 / 5 Renombrar
is_double(), is_integer(), is_long(), doubleval() 38-40 a favor is_float(), is_int(), floatval()
define() con $case_insensitive 41 / 0 / 3 Constantes normales
strcoll() y el flag SORT_LOCALE_STRING 39 / 2 / 4 y 38 / 2 / 3 Collator de intl
spl_classes() 43 / 0 / 0 Nada, listar a mano
spl_object_hash() 26 / 9 / 11 spl_object_id()
mysqli_get_charset() 43 / 0 / 0 mysqli::$charset
Objetos donde se espera un array (array_walk(), http_build_query(), mb_convert_variables()...) 21-41 a favor Convertir con get_object_vars()

Reservar let e is como palabras clave tiene una razón concreta. El propio RFC conecta esa familia de reservas con la discusión de tipos genéricos: la votación hermana sobre in, out e inout menciona los marcadores de varianza del RFC de genéricos con borrado de límites. PHP está haciendo sitio en la gramática para algo que todavía no existe.

Lo que se salvó: list() empató a 23

La propuesta más ruidosa del paquete era deprecar la construcción list() en favor de [...]. El análisis de impacto que acompañaba al RFC contó 12.275 usos en 5.545 archivos. La votación terminó 23 a favor, 23 en contra y una abstención: rechazada, porque hacían falta dos tercios.

O sea que esto sigue siendo legal y no tienes que tocarlo:

list($a, $b) = [1, 2];

Aun así, la migración automática ya estaba escrita en el propio RFC, por si quieres unificar estilo:

ast-grep run --pattern 'list($$$ARGS)' --rewrite '[$$$ARGS]' --lang php -U

También se cayeron la reserva de in, out e inout como identificadores (8 a favor contra 21), la deprecación de la función _() como alias de gettext() (10 contra 22) y la del filtro dechunk (18 contra 15, que se queda a medio camino de los dos tercios).

Cómo afecta PHP 9 a Laravel

Poco. Laravel siempre ha ido por delante del intérprete en su versión mínima.

Versión PHP Lanzamiento Bug fixes hasta Seguridad hasta
12 8.2 - 8.5 24 feb 2025 13 ago 2026 24 feb 2027
13 8.3 - 8.5 17 mar 2026 Q3 2027 17 mar 2028

Laravel 13 exige PHP 8.3 como mínimo y su rama de desarrollo ya subió el mínimo a PHP 8.4. Con releases mayores anuales alrededor del primer trimestre, para cuando PHP 9 exista habrá al menos una o dos versiones mayores más de Laravel, y el framework llegará con su código ya limpio de deprecaciones.

El trabajo te toca en tu carpeta app/: las variables indefinidas dentro de un if, los $model->campo_que_no_existe que hoy devuelven null en silencio, y los paquetes de terceros sin mantenimiento que arrastras en el composer.lock. Si te interesa esa parte, escribí sobre por qué el composer.lock importa más de lo que parece y sobre las novedades de Laravel 13.

Cómo afecta PHP 9 a Symfony

Symfony es todavía más agresivo con el mínimo de PHP, y tiene además la costumbre de marcar sus propias deprecaciones con un rigor que el resto del ecosistema copia.

  • Symfony 8.1, la rama estable, pide PHP 8.4 o superior.
  • Symfony 7.4, la LTS actual, pide PHP 8.2 y tiene correcciones de bugs hasta noviembre de 2028 y parches de seguridad hasta noviembre de 2029.
  • Symfony 8.2 llega en noviembre de 2026, también sobre PHP 8.4.

Symfony publica versión menor cada seis meses, en mayo y noviembre, y mayor cada dos años. Si estás en la LTS 7.4 vas cómodo hasta bien entrada la era de PHP 9; si estás en 6.4, que pide PHP 8.1, las correcciones de bugs se te acaban en noviembre de 2026.

Cómo preparar tu código hoy

Nada de lo que sigue depende de que PHP 9 tenga fecha, y todo se puede hacer esta semana.

Haz que las deprecaciones rompan los tests. Es lo más barato de la lista y lo que antes te da una cifra de cuánto trabajo tienes. En PHPUnit basta con unos atributos en phpunit.xml:

<phpunit
    failOnDeprecation="true"
    failOnDirectDeprecation="true"
    displayDetailsOnTestsThatTriggerDeprecations="true">
</phpunit>

Si no quieres romper la suite todavía, empieza con displayDetailsOnTestsThatTriggerDeprecations a secas y mira el volumen antes de decidir.

Sube PHPStan a nivel 1 como mínimo. Según la documentación oficial, el nivel 0 reporta "always undefined variables" y el nivel 1 añade "possibly undefined variables", que son justo los dos casos del RFC de promoción a error.

vendor/bin/phpstan analyse --level 1 app

Pásale Rector los sets de versión. Rector detecta la versión de PHP de tu composer.json y aplica los sets correspondientes:

use Rector\Config\RectorConfig;
 
return RectorConfig::configure()
    ->withPaths([__DIR__ . '/app'])
    ->withPhpSets();

En proyectos grandes, la propia documentación recomienda withPhpLevel() para subir de escalón en escalón en vez de aplicar todo el paquete de golpe.

Busca a mano los patrones condenados. Son cinco greps:

grep -rn '\${' app/ resources/          # interpolación ${}
grep -rn 'implements Serializable' app/ # interfaz retirada
grep -rn 'is_long\|is_integer\|is_double\|doubleval' app/
grep -rn 'spl_object_hash\|spl_classes' app/
grep -rn 'use_trans_sid\|use_only_cookies' .

No esperes a PHP 9 para actualizar. Saltar de 8.x a 8.y es barato. Saltar a 9.0 también lo será si llegas desde 8.5 o 8.6, y será caro si llegas desde 8.2, porque pagarás de golpe las deprecaciones de cuatro versiones.

Preguntas frecuentes

¿Cuándo sale PHP 9?

No hay fecha anunciada. El proyecto no ha publicado página de calendario para PHP 9.0 ni ha elegido release managers. Lo siguiente confirmado es PHP 8.6, el 19 de noviembre de 2026.

¿PHP 9 va a romper mi aplicación?

Depende de la edad de tu código. Si ya corre en PHP 8.4 o 8.5 sin emitir deprecaciones, la mayoría de los cambios de PHP 9 no te tocan. Lo que más rompe en la práctica es la promoción a error de variables y propiedades indefinidas, porque es un cambio de comportamiento en tiempo de ejecución que ningún composer update arregla por ti.

¿Laravel 13 va a funcionar en PHP 9?

Laravel 13 declara soporte para PHP 8.3 a 8.5. Cuando PHP 9 exista, lo soportará la versión mayor de Laravel que toque en ese momento, no la 13. La política de Laravel es 18 meses de correcciones y 2 años de parches de seguridad por versión mayor.

¿Se elimina list() en PHP 9?

No. La propuesta se votó dentro del paquete de deprecaciones de PHP 8.6 y quedó empatada a 23 votos, así que fue rechazada. list() sigue siendo válido.

¿Qué pasa con ${} en los strings?

Se elimina en PHP 9. Está deprecado desde PHP 8.2 y la sustitución es mecánica: "${foo}" pasa a "{$foo}".

¿Cómo sé si mi código emite deprecaciones?

Configura error_reporting a E_ALL en los entornos de desarrollo y CI, y activa failOnDeprecation en PHPUnit. Si usas Symfony, el symfony/phpunit-bridge ya agrupa y cuenta las deprecaciones por origen.

Lo que hay que llevarse

PHP 9 se puede leer hoy como una lista: diez RFC aprobados esperando rama, más lo que se deprecó en 8.5 y 8.6 con la eliminación prometida en el texto de la votación. Falta la fecha, y es lo único que no cambia el trabajo de preparación: tratar las deprecaciones como errores, subir el nivel del análisis estático y no quedarte dos ramas por detrás en la versión mínima de PHP.

Fuentes

¿Tienes un proyecto en mente?

Trabajo con Laravel, WordPress, SEO técnico y servidores MCP. El primer paso es una llamada de descubrimiento, sin costo ni compromiso, donde me cuentas qué necesitas y te digo con honestidad si puedo ayudarte.

Hablemos