4 de agosto de 2026

PHP 8.6: todas las novedades, fecha de lanzamiento y qué se rompe al actualizar

Foto de Marco Orta Marco Orta | 15 min de lectura
Compartir
Ilustración 3D del elefante de PHP junto a un signo de interrogación flotando como marcador de posición dentro de una llamada a función
Tabla de Contenidos

    PHP 8.6 sale el 19 de noviembre de 2026, y el 13 de agosto pasó el soft feature freeze: a partir de la beta 1, lo que no esté dentro ya casi no entra. Es decir, la lista de novedades que verás abajo es prácticamente la lista definitiva.

    La estrella es Partial Function Application, una sintaxis que llevaba años en el tintero y que por fin se votó por unanimidad. Pero lo que de verdad te va a dar trabajo no son las funciones nuevas, sino tres cambios silenciosos: las sesiones pasan a ser seguras por defecto, mb_ereg y compañía quedan deprecadas, y return dentro de un constructor empieza a avisar.

    Vamos por partes.

    Calendario: cuándo llega PHP 8.6

    flowchart TB
        A["🧪 Alpha 1 · 2 jul 2026"] --> B["🧪 Alpha 2 · 16 jul"]
        B --> C["🧪 Alpha 3 · 30 jul"]
        C --> D["🧊 Beta 1 + soft feature freeze<br/>13 ago 2026"]
        D --> E["🧊 Beta 2 · 27 ago"]
        E --> F["🧊 Beta 3 · 10 sep"]
        F --> G["🔒 Hard feature freeze<br/>22 sep 2026"]
        G --> H["📦 RC 1 a RC 4<br/>24 sep – 5 nov"]
        H --> I["🚀 GA · 19 nov 2026"]

    Traducido: desde el 13 de agosto una feature nueva necesita justificación para entrar, y desde el 22 de septiembre necesita aprobación explícita de los release managers. Del 24 de septiembre en adelante solo hay corrección de bugs.

    Los release managers de esta versión son Daniel Scherzer (veterano), Matteo Beccati y Joe Ferguson (novatos), elegidos por voto transferible en abril.

    Si quieres probarla hoy sin tocar tu máquina, la vía más limpia es un contenedor con la imagen php:8.6-rc-cli en cuanto salgan las RC — lo tienes explicado en la guía completa de Docker.

    Partial Function Application: la novedad grande

    Esta es la que cambia cómo escribes código. La RFC (Partial Function Application v2) se aprobó 33 votos a favor, 0 en contra, y ya está fusionada desde la alpha 3.

    La idea: puedes llamar a una función dejando huecos, y en vez de ejecutarla, PHP te devuelve una closure con esos huecos como parámetros.

    function add(int $a, int $b, int $c): int {
        return $a + $b + $c;
    }
    
    $add10AndX = add(10, ?, 5);  // Closure: fn(int $b): int
    echo $add10AndX(3);          // 18
    

    El ? es un marcador de posición. No es un valor, es un hueco.

    Antes y después

    El caso típico es pasar una función nativa a array_map cuando el argumento que varía no es el primero. Hoy tienes que envolverla:

    // PHP 8.5 y anteriores
    $strings = ['hello world', 'hello php'];
    $result = array_map(fn ($s) => str_replace('hello', 'hi', $s), $strings);
    

    En 8.6 el wrapper sobra:

    // PHP 8.6
    $replaceHello = str_replace('hello', 'hi', ?);
    $result = array_map($replaceHello, $strings);
    

    O directamente, sin variable intermedia:

    $result = array_map(str_replace('hello', 'hi', ?), $strings);
    

    Los dos marcadores: ? y ...

    • ? consume exactamente un argumento en esa posición.
    • ... consume cero o más argumentos restantes.
    $replaceWhitespace = str_replace(' ', ?, ?);   // 2 parámetros
    $replaceWhitespace('=', $input);
    
    $replaceWhitespace = str_replace(' ', ...);    // el resto, tal cual
    $replaceWhitespace('=', $input);
    

    Si ya usabas la sintaxis de first-class callable de PHP 8.1 (strlen(...)), buena noticia: es exactamente el caso degenerado de PFA. foo(...) sigue significando lo mismo, solo que ahora forma parte de una familia más grande.

    Con argumentos nombrados

    Los marcadores nombrados adoptan el orden en que los escribes, no el orden de la firma original:

    function stuff(int $i1, string $s2, float $f3, Point $p4) {}
    
    $c = stuff(s2: ?, i1: ?, p4: ?);
    // Firma resultante: fn(string $s2, int $i1, Point $p4)
    

    Con una regla dura: no puedes poner un marcador posicional después de uno nombrado. Eso es error fatal.

    Combinado con el operador pipe

    Aquí es donde brilla de verdad. El operador |> llegó en PHP 8.5 (noviembre de 2025) y hasta ahora obligaba a envolver cualquier función con más de un argumento. Con PFA:

    $slug = $input
        |> trim(...)
        |> str_replace(' ', '-', ?)
        |> str_replace(['.', '/', '…'], '', ?)
        |> strtolower(...);
    

    Y no es solo estético: la RFC incluye una optimización de compilación que elimina la closure intermedia para los casos simples (foo(?), foo(1, ?), foo(1, a: ?), foo(1, ...)) cuando se usan con el pipe. No pagas el coste de la abstracción.

    El detalle que te va a morder

    Los argumentos que rellenas se evalúan cuando creas la parcial, no cuando la llamas. Esto es lo contrario de una arrow function:

    $partial = speak(?, getArg());        // getArg() se ejecuta AQUÍ
    $arrow   = fn ($who) => speak($who, getArg());  // getArg() se ejecuta al llamar
    

    Si getArg() lee la hora, consulta la base de datos o depende del estado, el resultado queda congelado en el momento de crear la parcial. Es un comportamiento coherente (estás aplicando argumentos, no diferiéndolos), pero es el error más fácil de cometer las primeras semanas.

    Qué NO puedes hacer

    • Constructores: new Foo(?) no funciona. Error: Cannot create Closure for new expression.
    • compact(), extract(), func_get_arg(), get_defined_vars(): prohibidas, porque dependen del ámbito de la llamada.
    • __get y __set: no aplican, porque no se invocan como métodos. __call y __callStatic sí funcionan.
    • Los métodos estáticos producen closures estáticas (no se pueden rebindear); los de instancia sí se pueden rebindear, pero solo al mismo tipo.

    El parche sobre parámetros opcionales

    Hubo una segunda RFC (25 a favor, 0 en contra) que ajustó un detalle importante antes de que llegara a producción. La regla final:

    • Un parámetro generado por ? siempre es obligatorio, aunque en la función original tuviera valor por defecto.
    • Los parámetros que arrastra ... conservan su opcionalidad.
    function example(mixed $a, string $b = 'default', string $c = 'also optional') {}
    
    example(?, ?);        // fn(mixed $a, string $b)         ← $b ahora es obligatorio
    example('foo', ...);  // fn(string $b = 'default', string $c = 'also optional')
    

    Sin este ajuste, una clase hija que cambiara un valor por defecto podía alterar la firma de una parcial creada sobre la clase padre. Ahora ? significa siempre lo mismo.

    Las adiciones pequeñas y útiles

    clamp()

    Acota un valor entre un mínimo y un máximo. Todos hemos escrito max($min, min($max, $v)) alguna vez, y todos lo hemos escrito mal alguna vez.

    clamp(2, min: 1, max: 3)   // 2
    clamp(0, min: 1, max: 3)   // 1
    clamp(6, min: 1, max: 3)   // 3
    clamp("a", "c", "g")       // "c"
    

    La firma es clamp(mixed $value, mixed $min, mixed $max): mixed y sigue las reglas de comparación normales de PHP, así que también funciona con fechas:

    clamp(
        new \DateTimeImmutable('2025-08-01'),
        new \DateTimeImmutable('2025-08-15'),
        new \DateTimeImmutable('2025-09-15')
    )->format('Y-m-d');  // 2025-08-15
    

    Dos casos borde que conviene tener claros: si $min > $max lanza ValueError, y si pasas NAN como límite también. NAN como valor se devuelve tal cual. Aprobada 23 a 3.

    enum SortDirection

    Un enum en el espacio de nombres global con dos casos, Ascending y Descending:

    enum SortDirection {
        case Ascending;
        case Descending;
    }
    
    $query->orderBy('created_at', SortDirection::Descending);
    

    Seamos honestos sobre qué es y qué no: ninguna función del core lo consume todavía. Lo que aporta es un vocabulario compartido para que frameworks, ORMs y extensiones dejen de inventarse cada uno sus constantes 'ASC' / 'DESC' / SORT_ASC. La RFC menciona scandir() como candidato futuro. Aprobada 22 a 2.

    #[\Override] en constantes de clase

    El atributo #[\Override] de PHP 8.3 ahora vale también para constantes. Si declaras que sobreescribes algo y no sobreescribes nada, PHP te lo dice:

    class Demo {
        #[\Override]
        public const C = 'C';  // Error: no hay nada que sobreescribir
    }
    
    class Base {
        protected const C = 'C';
    }
    
    class Child extends Base {
        #[\Override]
        public const C = 'Changed';  // Correcto
    }
    

    Sirve con constantes públicas y protegidas de clases padre e interfaces; las privadas no cuentan. Funciona en interfaces, enums y clases anónimas. Aprobada 15 a 0.

    Enums depurables

    Hasta ahora __debugInfo() estaba prohibido en enums. En 8.6 se levanta la restricción:

    enum Foo: string {
        case Bar = "Baz";
    
        public function __debugInfo() {
            return [__CLASS__ . '::' . $this->name . ' = ' . $this->value];
        }
    }
    
    var_dump(Foo::Bar);
    

    Cambio pequeño, sin ruptura, y útil si tienes enums con lógica que quieres inspeccionar. Aprobada 16 a 2.

    Errores que muestran los argumentos

    Este me parece de los que más tiempo ahorran depurando. Hoy, un warning de una función nativa te dice qué falló pero no con qué:

    Warning: chmod(): Operation not permitted in /app/chmod.php on line 5
    

    Con la nueva directiva error_include_args activada:

    Warning: chmod('/', 511): Operation not permitted in /app/chmod.php on line 5
    

    Un detalle crítico: viene desactivada por defecto (error_include_args=0). La feature se aprobó 24 a 2, pero en la votación secundaria sobre el valor por defecto ganó el 0 por 18 a 7. Si la quieres, la activas tú.

    Respeta las protecciones que ya existían: honra el atributo #[\SensitiveParameter] para enmascarar contraseñas, respeta zend.exception_string_param_max_len para no inflar los logs, y solo aplica a funciones internas de PHP, no a errores que dispares tú.

    Y además

    • grapheme_strrev() — invertir una cadena respetando grapheme clusters. Es decir, strrev() que no destroza los emojis ni los caracteres compuestos.
    • API de sondeo (Io\Poll) — una API de multiplexación de E/S con clases Context, Watcher y StreamPollHandle, respaldada por epoll en Linux y kqueue en BSD/macOS. Resuelve las dos limitaciones históricas de stream_select(): el techo de descriptores y su complejidad O(n). Si escribes servidores asíncronos en PHP, esta es tu novedad. Aprobada 33 a 1.
    • mysqli_quote_string() — escapado de cadenas sin necesidad de una conexión abierta.
    • Locale::getDisplayKeyword() y getDisplayKeywordValue() — nombres legibles de las claves de locale.
    • Métodos de Reflection isReadable / isWriteable — para saber si una propiedad se puede leer o escribir sin provocar el acceso.
    • Reanudación de sesión TLS en streams — menos handshakes completos en conexiones repetidas.
    • DocComments en parámetros de función — los bloques de documentación ahora se pueden asociar a parámetros individuales y leerlos por Reflection.
    • json_decode() indica la posición del error — el mensaje te dice dónde está el JSON malformado, no solo que lo está.

    Lo que se rompe: revísalo antes de actualizar

    Aquí es donde deberías centrar el esfuerzo. Ninguno de estos cambios es dramático, pero todos son silenciosos.

    1. Sesiones seguras por defecto (el que más va a doler)

    La RFC de valores por defecto seguros de sesión cambia tres directivas:

    DirectivaAntesEn 8.6
    session.use_strict_mode01
    session.cookie_httponly01
    session.cookie_samesite(vacío)Lax

    Es un cambio bueno — son los valores que cualquier auditoría te pide desde hace años — pero cambiar defaults rompe aplicaciones que dependían del comportamiento anterior sin saberlo:

    • use_strict_mode = 1: si compartes IDs de sesión entre subdominios sin llamar antes a session_write_close(), los IDs se rechazan. Los manejadores personalizados que no implementen validateId() no se ven afectados.
    • cookie_httponly = 1: cualquier código que lea la cookie de sesión desde JavaScript con document.cookie deja de funcionar. La recomendación de la RFC es la correcta: usa un token CSRF aparte para eso.
    • cookie_samesite = Lax: los POST cross-site dejan de llevar la cookie de sesión. Chrome y Firefox ya aplicaban Lax implícitamente, así que en la práctica esto solo hace explícito lo que ya pasaba en los navegadores mayoritarios — pero afecta a pasarelas de pago con callback por POST y a integraciones SSO antiguas.

    Las tres votaciones salieron limpias: 27-0, 26-0 y 26-0.

    Si vas a tocar esto, aprovecha y revisa las cabeceras de seguridad de todo el sitio de paso:

    2. Adiós a mb_ereg y todo mbregex

    Oniguruma, la librería de expresiones regulares multibyte detrás de mb_ereg, dejó de tener mantenimiento. PHP responde deprecando 14 funciones en 8.6 y eliminándolas en PHP 9.0:

    mb_ereg, mb_ereg_match, mb_ereg_replace, mb_ereg_replace_callback, mb_ereg_search, mb_ereg_search_getpos, mb_ereg_search_getregs, mb_ereg_search_init, mb_ereg_search_pos, mb_ereg_search_regs, mb_ereg_search_setpos, mb_eregi, mb_eregi_replace y mb_split.

    También desaparecen la constante MB_ONIGURUMA_VERSION y las opciones mbstring.regex_retry_limit y mbstring.regex_stack_limit.

    Tienes dos caminos: migrar a PCRE con el modificador u (preg_match('/…/u', …)), que es lo que deberías hacer en el 95 % de los casos, o instalar la extensión PECL mb_onig que publicó el propio autor de la RFC en Packagist, si tienes patrones con sintaxis específica de Oniguruma que no quieres reescribir.

    Empieza buscando en tu código:

    grep -rn "mb_ereg\|mb_split" --include="*.php" .
    

    Y si vas a reescribir patrones, pruébalos antes de desplegarlos:

    Aprobada 24 a 0, sin oposición.

    3. return con valor en constructores

    A partir de 8.6, devolver un valor desde __construct() o __destruct() — o convertirlos en generadores — emite una deprecación en tiempo de compilación:

    class Foo {
        public function __construct() {
            return 123;  // Deprecated: Returning a value from a constructor is deprecated
        }
    }
    
    class Baz {
        public function __construct() {
            yield 123;   // Deprecated: Making a constructor a Generator is deprecated
        }
    }
    

    Un return; pelado para salir antes sigue siendo perfectamente legal:

    class Bar {
        public function __construct() {
            if (random_int(0, 1)) {
                return;  // Correcto
            }
        }
    }
    

    Como es en tiempo de compilación, lo vas a ver aunque el código no se ejecute nunca. En la primera major después de 8.6 pasa a ser error. Aprobada 39 a 0.

    4. Cambios menores que igual te tocan

    • trim(), ltrim(), rtrim() y chop() ahora eliminan el form feed (\f) por defecto. Si tu código dependía de que \f sobreviviera a un trim(), cambia el comportamiento. Es raro, pero existe.
    • array_filter() lanza ValueError si $mode es inválido, en vez de ignorarlo en silencio. Esto convierte un bug latente en una excepción, que es mejor, pero puede tumbar código en producción que llevaba años pasando un valor incorrecto sin enterarse.
    • Límite en el número de filtros encadenados en php://filter. Endurecimiento de seguridad contra una técnica conocida de explotación por cadenas de filtros.

    Checklist de migración

    Un orden razonable para no llevarte sustos:

    1. Levanta 8.6 en un contenedor en cuanto salga la RC1 (24 de septiembre). No toques tu entorno principal.
    2. Ejecuta tu suite de tests con error_reporting(E_ALL) y captura las deprecaciones. Las de constructor salen solas al compilar.
    3. grep de mb_ereg y mb_split. Decide entre PCRE o la extensión mb_onig y planifica: tienes hasta PHP 9.0, pero cuanto antes lo saques del backlog, mejor.
    4. Revisa las sesiones. Si tu php.ini ya fijaba las tres directivas explícitamente, no te afecta nada. Si dependía de los defaults, prueba login, logout, SSO y cualquier callback POST externo.
    5. Actualiza tus herramientas de análisis estático (PHPStan, Psalm, Rector) antes de escribir sintaxis nueva. PFA es sintaxis nueva; hasta que tu analizador la entienda, vas a tener falsos positivos.
    6. Reserva PFA para código nuevo. No hay ninguna razón para reescribir closures que funcionan.

    ¿Deberías actualizar?

    Si vienes de PHP 8.3 o anterior, sí, y con prisa: 8.3 sale de soporte de seguridad a finales de 2026. Salta directo a 8.6 en cuanto tu stack lo soporte.

    Si estás en 8.4 o 8.5, el salto a 8.6 es de los cómodos: sin cambios de sintaxis obligatorios, sin eliminaciones, solo deprecaciones y tres defaults más seguros. La pregunta no es si actualizar, sino si tus dependencias ya lo soportan — y ese suele ser el verdadero cuello de botella. Laravel, Symfony y compañía tardan entre semanas y un par de meses en declarar compatibilidad formal.

    Mi recomendación práctica: prueba en la RC1 de septiembre, migra mb_ereg mientras tanto, y despliega en producción cuando salga 8.6.1 o 8.6.2. Nunca hay prisa por ser el primero en un .0.

    Conclusión

    PHP 8.6 no es una versión revolucionaria, y eso está bien. Es una versión que cierra huecos: PFA completa lo que empezaron los first-class callables en 8.1 y el operador pipe en 8.5; clamp() y SortDirection quitan código repetido de todos lados; y los defaults de sesión llevan una década siendo la recomendación de todo el mundo menos del propio PHP.

    Lo único que exige trabajo real es mbregex, y tienes hasta PHP 9.0 para hacerlo.

    Para seguir:

    Preguntas frecuentes

    ¿Cuándo sale PHP 8.6? El 19 de noviembre de 2026. El soft feature freeze fue el 13 de agosto y el hard feature freeze es el 22 de septiembre.

    ¿Qué es Partial Function Application? Llamar a una función dejando huecos (? o ...) para que devuelva una closure en vez de ejecutarse. Es el caso general del que foo(...) de PHP 8.1 era un caso particular.

    ¿Rompe compatibilidad? No elimina nada, pero cambia tres defaults de sesión (use_strict_mode, cookie_httponly, cookie_samesite) y deprecia mbregex, return en constructores y el trim del form feed.

    ¿Por qué deprecan mb_ereg? Oniguruma dejó de mantenerse. Se deprecian 14 funciones en 8.6 y se eliminan en 9.0. Migra a PCRE con /u o a la extensión PECL mb_onig.

    ¿Para qué sirve clamp()? Acota un valor entre un mínimo y un máximo, sustituyendo a max($min, min($max, $v)). Funciona con números, cadenas y objetos comparables.

    ¿Puedo usarlo con Laravel? Podrás, pero no el día uno. Prueba en la RC de septiembre y despliega en producción con 8.6.1 o 8.6.2.

    Compartir

    Buscar

    Etiquetas

    Tutorial PHP Laravel IA Desarrollo Web JavaScript Buenas Prácticas Laravel 13 Seguridad Herramientas Claude SEO Expresiones Regulares Manipulación de Texto Frontend