cada novedad, cada deprecación, el camino de actualización · mantenida al día con las votaciones RFC que reflejamos en directo · actualizado el 31 ago 2026
PHP 8.6 es la última versión menor antes de PHP 9. Llega el 19 de noviembre de 2026 con 27 RFC aprobadas: aplicación parcial de funciones, valores por defecto en propiedades readonly, un tipo Duration de verdad, clamp(), una API de polling escalable, errores de stream estructurados y cookies de sesión seguras de serie. La otra mitad es limpieza: 35 votaciones de deprecación independientes, de las que 31 se aprobaron, preparan lo que PHP 9 eliminará. Esta página acompaña la versión desde la beta hasta la GA; cada recuento de votos se refleja desde las papeletas públicas de wiki.php.net, y cada ejemplo de código marcado con una señal de verificación se ejecutó en la build actual de 8.6.
Un ? en lugar de un argumento convierte la llamada en un Closure para el resto. Los marcadores con nombre reordenan parámetros, ... recoge todo lo que aún no se ha pasado, y el Closure resultante conserva tipos, valores por defecto y paso por referencia para la reflexión.
Es la mitad que faltaba de los first-class callables de 8.1 y el compañero natural del operador pipe de 8.5. array_map(str_replace('a', 'b', ?), $list) sustituye una línea de función flecha, y el análisis estático ve la firma real. Una RFC posterior hizo obligatorio cada marcador ?: la aridad que escribes es la que obtienes.
partial-application.php
// Fix some arguments now, supply the rest later$greet = str_replace('hello', 'hi', ?);
echo$greet('hello world'); // hi world// Placeholders compose with the 8.5 pipe operator$slugs = ['Hello World', 'PHP 8.6']
|> array_map(strtolower(...), ?)
|> array_map(str_replace(' ', '-', ?), ?);
print_r($slugs); // hello-world, php-8.6// "..." keeps the remaining parameters, defaults included$encode = json_encode(?, JSON_THROW_ON_ERROR, ...);
echo$encode(['ok' => true]); // {"ok":true}
Las propiedades readonly ya pueden declarar un valor por defecto; ese valor cuenta como la única inicialización. Junto con las propiedades de interfaz de 8.4, esto da constantes contractuales por clase sin asignaciones en el constructor.
La RFC original de readonly consideraba los valores por defecto «no especialmente útiles». Las propiedades de interfaz cambiaron eso: un readonly con valor por defecto cumple un contrato { get; } sin código repetitivo. Cuidado: una asignación en el constructor ahora es una modificación y lanza un error, y unset() sobre esa propiedad falla.
readonly-defaults.php
interface Ingestor
{
publicstring$name { get; }
publicarray$steps { get; }
}
// Fixed metadata without constructor boilerplate:// the default IS the initialising assignmentfinalreadonlyclass ChangelogIngestor implements Ingestor
{
publicstring$name = 'changelog';
publicarray$steps = [ParseMarkdown::class, ExtractVersions::class];
}
echonew ChangelogIngestor()->name; // changelog
Los objetos en constantes existen desde 8.1, pero cualquier escritura como CONST->prop = 1 moría con «Cannot use temporary expression in write context». PHP 8.6 levanta esa restricción para asignaciones de propiedades, operadores compuestos, incrementos, isset(), unset() y paso por referencia.
El enlace de la constante sigue siendo inmutable; solo cambia el estado del objeto, como se comportan los objetos en cualquier otro sitio. Las constantes array no cambian (copy-on-write). Efecto secundario: escribir en la propiedad de un caso de enum ahora lanza el error readonly habitual en vez de un fatal en compilación.
const-object.php
const REGISTRY = new stdClass();
// PHP 8.5: Fatal error: Cannot use temporary expression in write context// PHP 8.6: the constant still binds the same object, its state may change
REGISTRY->hits = 0;
REGISTRY->hits++;
REGISTRY->hits += 10;
class Config
{
const SETTINGS = REGISTRY;
}
Config::SETTINGS->debug = true;
var_dump(REGISTRY->hits, isset(Config::SETTINGS->debug)); // int(11) bool(true)
Una duración de cronómetro, final y readonly, con precisión de nanosegundos: fábricas desde segundos hasta nanosegundos y desde cadenas ISO 8601, add/sub/multiplyBy/divideBy, operadores de comparación y un rango protegido contra desbordamiento de unos 292 años.
Timeouts, backoff y planificación eran int en alguna unidad o flotantes frágiles. DateInterval arrastra unidades de calendario que no se convierten a segundos de forma fiable. Duration es segura por construcción y ya es el tipo de timeout de la nueva API de polling; vendrán más API internas. sleep() y compañía siguen aceptando int.
duration.php
use Time\Duration;
// Stop-watch time with nanosecond precision, no calendar semantics$timeout = Duration::fromMilliseconds(1_500);
$backoff = $timeout->multiplyBy(2)->add(Duration::fromSeconds(1));
var_dump($backoff->seconds, $backoff->nanoseconds); // int(4) int(0)
var_dump($timeout < $backoff); // bool(true)// ISO 8601 without date parts: hours are the largest unit$limit = Duration::fromIso8601DurationString('PT1H30M');
var_dump(Duration::compare($backoff, $limit)); // int(-1)
El atributo introducido para métodos en 8.3 y ampliado a propiedades en 8.5 cubre ahora constantes de clase y casos de enum. El compilador da error si en realidad no se sobrescribe nada.
Sobrescribir por accidente una constante del padre es un bug silencioso clásico. Marcar la intención hace seguras las refactorizaciones: renombra la constante del padre y el hijo deja de compilar en vez de definir en silencio una constante nueva.
override-constant.php
class Base
{
protectedconst RETRIES = 3;
}
class Worker extends Base
{
#[\Override] // compile error if Base had no RETRIESpublicconst RETRIES = 5;
}
enum Level: intimplements HasDefault
{
#[\Override] // enum cases may override interface constantscaseDefault = 0;
}
Los enums ya pueden implementar __debugInfo(); var_dump() conserva la cabecera enum(Foo::Bar) y añade lo que devuelva el método.
La salida de los enums en volcados y depuradores era fija y escueta. Los backed enums con valores expresivos, o los casos que apuntan a configuración, ahora pueden explicarse. __toString() en enums se debatió y se descartó a favor de este cambio más acotado.
Un comentario /** … */ colocado antes o después de un parámetro lo conserva el motor y lo devuelve el nuevo ReflectionParameter::getDocComment().
La documentación de parámetros ya no tiene que repetir nombre y tipo en una línea @param encima de la función, donde se desincroniza sin que nadie lo note. Los frameworks basados en atributos y los generadores de API leen el texto directamente por reflexión.
param-doccomment.php
/** Search for entries matching a query */function search(
/** Terms to look for in the index */string$query,
/** Maximum number of entries returned */int$limit = 10,
): array {
return [];
}
foreach (new ReflectionFunction('search')->getParameters() as$p) {
echo$p->name, ': ', $p->getDocComment(), PHP_EOL;
}
// query: /** Terms to look for in the index */// limit: /** Maximum number of entries returned */
Un closure static que no captura variables ni declara variables static se crea una vez y se reutiliza. Dos closures así procedentes de la misma posición del código son ahora el mismo objeto.
Las rutas calientes que crean el mismo closure en cada iteración dejan de reservar memoria. La segunda parte de la RFC, inferir static en closures que nunca usan $this, se retiró tras la votación por un caso límite con callables en forma de cadena; marca tus closures como static para obtener todo el beneficio.
closure-cache.php
function handler(): Closure
{
// static, captures nothing, no static variables => cachedreturnstaticfn (int$x): int => $x * 2;
}
// PHP 8.5: two instances. PHP 8.6: the same cached Closure.
var_dump(handler() === handler()); // bool(true)
clamp($value, $min, $max) devuelve el valor si está dentro de los límites y, si no, el límite más cercano. Acepta cualquier cosa comparable, incluidas cadenas y objetos DateTime, y lanza un ValueError si $min es mayor que $max o alguno de los límites es NAN.
El idiom habitual max($min, min($max, $v)) esconde dos bugs: límites intercambiados y un NAN que pasa en silencio. La versión nativa valida ambos y es algo más rápida que el ternario en userland.
clamp.php
var_dump(clamp(150, min: 0, max: 100)); // int(100)
var_dump(clamp(42, 0, 100)); // int(42)
var_dump(clamp(2.5, 1, 3)); // float(2.5)// Any comparable type follows the usual comparison rulesecho clamp(
new DateTimeImmutable('2026-12-24'),
new DateTimeImmutable('2026-11-19'),
new DateTimeImmutable('2026-12-01'),
)->format('Y-m-d'); // 2026-12-01
clamp(4, 8, 6); // ValueError: $min must be smaller than or equal to $max
Un contexto de stream puede cambiar el manejo de errores a excepciones o a recogida silenciosa: error_mode (Error, Exception, Silent), error_store y un callback error_handler. Los errores llegan como objetos StreamError con un caso del enum StreamErrorCode, nombre del wrapper, severidad y la ruta afectada.
Hasta ahora file_get_contents() y compañía avisaban del fallo con warnings que había que capturar con un manejador de errores y analizar como texto. Con StreamErrorMode::Exception, una apertura fallida es una StreamException con códigos legibles por máquina; los valores por defecto no cambian, así que nada se altera si no lo pides.
stream-errors.php
$ctx = stream_context_create([
'stream' => ['error_mode' => StreamErrorMode::Exception],
]);
try {
$config = file_get_contents('/etc/app/missing.ini', false, $ctx);
} catch (StreamException $e) {
foreach ($e->getErrors() as$error) {
echo$error->code->name, ': ', $error->message, PHP_EOL;
// OpenFailed: Failed to open stream: No such file or directory
}
}
// Or stay silent and inspect afterwards$ctx = stream_context_create(['stream' => ['error_mode' => StreamErrorMode::Silent]]);
@file_get_contents('/nope', false, $ctx);
var_dump(stream_last_errors()[0]->code === StreamErrorCode::OpenFailed); // bool(true)
Una API de multiplexación nativa sobre epoll, kqueue, event ports, WSAPoll o poll(): Io\Poll\Context vigila handles para los eventos Read, Write, Error y HangUp, admite los modos one-shot y edge-triggered y devuelve desde wait() los watchers disparados.
stream_select() es O(n), está limitado a 1024 descriptores en muchos sistemas y no sabe nada de los kernels modernos. Los bucles de eventos como Revolt, ReactPHP o AMPHP pueden funcionar sobre un backend integrado en vez de ext-uv o ext-event, y los runtimes tipo FrankenPHP obtienen una API interna compartida. En esta primera versión solo se pueden sondear recursos de stream.
Uri\Rfc3986\UriBuilder y Uri\WhatWg\UrlBuilder montan una URI en un solo objeto y validan en build(); getUriType() distingue URI absolutas de referencias de ruta y de red, getHostType() informa de IPv4, IPv6, dominio u host opaco, e isSpecialScheme() dice si aplican las reglas WHATWG de esquemas especiales.
Las clases URI de 8.5 solo tenían withers inmutables, así que construir una URI desde cero reservaba un objeto por paso y dependía del orden de las llamadas. El builder elimina ambos problemas. El helper de codificación porcentual de la misma RFC todavía no ha aterrizado en la beta.
uri-builder.php
// One object instead of a wither chain that allocates per step$uri = new Uri\Rfc3986\UriBuilder()
->setScheme('https')
->setHost('php-net.pro')
->setPath('/en/php-8.6')
->setQuery('utm_source=dispatch')
->build();
echo$uri->toRawString(); // https://php-net.pro/en/php-8.6?utm_source=dispatchecho$uri->getUriType()->name; // Uri (vs. RelativePathReference ...)$url = Uri\WhatWg\Url::parse('https://[2001:db8::1]/');
echo$url->getHostType()->name; // IPv6
var_dump($url->isSpecialScheme()); // bool(true) for http(s), ws(s), ftp, file
Un enum global sin respaldo con los casos Ascending y Descending, pensado como el único tipo de dirección de ordenación en el que converjan PHP, frameworks y bibliotecas.
Doctrine, los query builders y cada ORM traen su propio enum Order. Un tipo compartido hace interoperables las firmas; una unión con las viejas banderas string o int mantiene compatibles las API existentes mientras migran.
sort-direction.php
// One shared type instead of SORT_ASC, 'DESC', bool $ascending ...function orderBy(string$column, SortDirection $direction = SortDirection::Ascending): string
{
return sprintf('ORDER BY %s %s', $column, match ($direction) {
SortDirection::Ascending => 'ASC',
SortDirection::Descending => 'DESC',
});
}
echo orderBy('created_at', SortDirection::Descending); // ORDER BY created_at DESC
Ambos métodos responden si una lectura o escritura tendría éxito desde un ámbito dado, opcionalmente para un objeto concreto. Entienden readonly, visibilidad asimétrica, hooks y accesores mágicos, cosa que isPublic() nunca hizo.
Desde readonly (8.1) y private(set) (8.4), public ya no significa escribible. Serializadores, hidratadores y mappers tenían que reimplementar las reglas del motor; ahora se las preguntan al motor.
is-writable.php
finalclass Money
{
publicfunction __construct(
publicreadonlyint$amount,
publicprivate(set) string$currency = 'EUR',
) {}
}
$amount = new ReflectionProperty(Money::class, 'amount');
$currency = new ReflectionProperty(Money::class, 'currency');
// isPublic() says "true" for both; the new methods answer the real question
var_dump($amount->isWritable(null)); // bool(false) from global scope
var_dump($currency->isWritable(Money::class)); // bool(true) from inside
var_dump($currency->isReadable(null)); // bool(true)
Los modificadores < y > al estilo Perl funcionan ahora en los códigos enteros s, l, q, S, L, Q y en los códigos flotantes f y d. Aplicarlos a códigos con endianness intrínseco lanza un ValueError.
Leer un int32 con signo little-endian requería tres llamadas a unpack() y desplazamientos de bits. Los parsers de protocolos binarios y formatos de archivo se reducen a una cadena de formato. Ojo con los nombres: unpack('s<value') produce ahora la clave value, no <value.
pack-endianness.php
// Signed little-endian int32 used to need a manual bit dance$bytes = pack('l<', -16909060);
print_r(unpack('l<value', $bytes)); // [value] => -16909060// Modifiers work on s/S, l/L, q/Q and (new) f/d$frame = pack('s>l<d>', -258, 1024, 3.14159);
[$id, $len, $ratio] = array_values(unpack('s>id/l<len/d>ratio', $frame));
pack('a<', 'x'); // ValueError: modifier not allowed on "a"
Escapa y envuelve una cadena en comillas simples en un solo paso, el equivalente en mysqli de PDO::quote(). Disponible como mysqli::quote_string() y mysqli_quote_string().
real_escape_string() solo funcionaba bien entre comillas simples; con comillas dobles y NO_BACKSLASH_ESCAPES era un agujero de inyección. La nueva función elimina el entrecomillado manual donde las sentencias preparadas no son opción, como en query builders que deben renderizar SQL completo.
mysqli-quote.php
// Before: quotes by hand, injection possible under NO_BACKSLASH_ESCAPES$sql = sprintf("SELECT id FROM users WHERE name = '%s'", $mysqli->real_escape_string($name));
// PHP 8.6: escaped AND quoted, always single quotes (like PDO::quote())$sql = sprintf('SELECT id FROM users WHERE name = %s', $mysqli->quote_string($name));
// SELECT id FROM users WHERE name = 'Don\'t!'
Los contextos de stream ganan session_data, session_new_cb, session_cache y opciones relacionadas con un objeto Openssl\Session exportable e importable; psk_client_cb/psk_server_cb habilitan claves precompartidas externas, y early_data añade 0-RTT de TLS 1.3.
Cada petición PHP empezaba con un handshake TLS completo. Persistir la sesión en Redis o APCu ahorra un viaje de ida y vuelta por cada llamada HTTPS saliente, y los servidores escritos en PHP pueden respaldar su caché de sesiones con cualquier almacén.
tls-resume.php
// Skip the full handshake on the next request by persisting the session$stored = $cache->get('tls:api.example.com');
$ctx = stream_context_create(['ssl' => [
'peer_name' => 'api.example.com',
'session_data' => $stored ? Openssl\Session::import($stored) : null,
'session_new_cb' => function ($stream, Openssl\Session $session) use ($cache): void {
$cache->set('tls:api.example.com', $session->export(), $session->getTimeout());
},
]]);
$fp = stream_socket_client('tls://api.example.com:443', context: $ctx);
grapheme_strrev() invierte por clúster de grafemas, así que las secuencias de emoji y las marcas combinantes quedan intactas. Locale::getDisplayKeyword() y getDisplayKeywordValue() localizan palabras clave de locale, IntlNumberRangeFormatter formatea rangos numéricos, SpoofChecker aprende confusiones bidireccionales e IntlDatePatternGenerator devuelve esqueletos.
La extensión intl sigue cerrando huecos frente a ICU. La mayoría son pequeños, pero el par display-keyword completa una familia incompleta desde PHP 5.3.
intl.php
// Reverses grapheme clusters, so family emoji and combining marks surviveecho grapheme_strrev("PHP 👨👩👧 8.6"); // 6.8 👨👩👧 PHP// Localised labels for locale keywordsecho Locale::getDisplayKeyword('calendar', 'de'); // Kalenderecho Locale::getDisplayKeywordValue('ja@calendar=japanese', 'calendar', 'en'); // Japanese Calendar// Number ranges with ICU collapsing rules ("1–5 km")$f = new IntlNumberRangeFormatter('measure-unit/length-kilometer', 'en');
echo$f->format(1, 5);
Cambian tres valores INI por defecto: session.use_strict_mode=1 rechaza identificadores de sesión que el servidor no emitió, session.cookie_httponly=1 oculta la cookie a JavaScript y session.cookie_samesite=Lax impide que viaje con POST entre sitios.
Es el cambio de 8.6 con más probabilidades de llegar a producción sin que nadie lo note. Los frameworks fijan estos valores desde hace años; el PHP a pelo, a menudo no. Se rompen los traspasos de sesión entre subdominios, el JavaScript que lee PHPSESSID y los formularios entre sitios; cada caso tiene un opt-out explícito en php.ini.
php.ini (new built-in defaults)
; PHP 8.6 ships these as built-in defaults (were 0 / 0 / unset)
session.use_strict_mode = 1 ; unknown session IDs are rejected (fixation)
session.cookie_httponly = 1 ; document.cookie can no longer read it
session.cookie_samesite = Lax ; not sent on cross-site POSTs
; Only if you really depend on the old behaviour:; session.cookie_samesite = None (requires session.cookie_secure = 1)
Con error_include_args=1, los warnings de las funciones internas incluyen los argumentos de la llamada, con la misma maquinaria que los backtraces de excepciones: los valores #[\SensitiveParameter] siguen enmascarados y zend.exception_string_param_max_len recorta las cadenas largas.
chmod(): Operation not permitted no dice nada; chmod('/var/app/cache', 511) lo dice todo. El ajuste viene desactivado por defecto en la beta por el tamaño de los logs y los datos personales; actívalo primero en staging, a propósito.
terminal
$ php-d error_include_args=1 -r'chmod("/var/app/cache", 0777);'Warning: chmod('/var/app/cache', 511): Operation not permitted in ...
# #[\SensitiveParameter] values stay masked, long strings are cut# by zend.exception_string_param_max_len; the INI defaults to 0 (off)
Una URL php://filter con más de 16 filtros emite ahora E_DEPRECATED; el límite se puede subir por contexto con filter.max_filter_count, y stream_filter_append() sigue sin límite.
Las cadenas de filtros convert.iconv y base64 son la herramienta estándar para convertir una inclusión de archivo local en ejecución remota de código. El código legítimo rara vez apila más de tres filtros, así que el tope elimina esa clase de exploit sin un interruptor INI que olvidar.
filter-chains.php
// Still fine: two filters
file_get_contents('php://filter/string.toupper|string.rot13/resource=data://text/plain,hello');
// 17+ filters in one URL (the LFI-to-RCE gadget) now emits E_DEPRECATED,// a later version turns it into an error. Raise the cap only on purpose:$ctx = stream_context_create(['php' => ['filter.max_filter_count' => 32]]);
Un return con valor en un constructor o destructor emite una deprecación en compilación, igual que yield, que los convertiría en generadores. Un return; a secas para saltar el resto del cuerpo sigue siendo legal.
Los constructores no pueden declarar tipo de retorno, así que nada impedía que return $this; se colara; una auditoría de los 4000 paquetes principales de Composer encontró 77 sentencias así. PHP 9 convertirá la deprecación en error.
constructor-return.php
class Connection
{
publicfunction __construct()
{
return$this; // Deprecated: Returning a value from a constructor is deprecated
}
publicfunction __destruct()
{
if (!$this->open) {
return; // bare "return;" stays legal
}
}
}
Todas las funciones mbregex (mb_ereg, mb_ereg_replace, mb_split, mb_eregi, …), los ajustes INI mbstring.regex_* y MB_ONIGURUMA_VERSION quedan deprecados en 8.6 y previstos para eliminación en 9.0, porque la biblioteca Oniguruma dejó de mantenerse en abril de 2025.
PCRE con el modificador u cubre los casos UTF-8. El código que depende de expresiones regulares multibyte fuera de UTF-8 necesitará el paquete externo mb_onig que mantiene el autor de la RFC. Busca mb_ereg y mb_split ya; se encontraron 147 apariciones en los principales paquetes de Composer.
mbregex.php
// Deprecated in 8.6, removed in 9.0 (Oniguruma is unmaintained since 2025-04)$parts = mb_split('\s+', $text);
$clean = mb_ereg_replace('[^\w]', '', $text);
// PCRE with the /u modifier covers the same ground$parts = preg_split('/\s+/u', $text);
$clean = preg_replace('/[^\w]/u', '', $text);
Compilar desde git requiere autoconf 2.71, las conexiones MySQL persistentes exigen MySQL 5.7.3 o MariaDB 10.2.4 para COM_RESET_CONNECTION, y las builds oficiales de Windows pasan a Visual Studio 2026, OpenSSL 4 y libxml2 2.15.
autoconf 2.71 (solo para builds desde git; los tarballs de release incluyen configure)
MySQL ≥ 5.7.3 / MariaDB ≥ 10.2.4 para conexiones persistentes en mysqlnd y PDO
Windows: toolchain VS 2026 (VS18), OpenSSL 4, libxml2 2.15.3
Varios patrones compilan ahora a opcodes más baratos, las builds ZTS son más rápidas en general y el JIT llega a Apple Silicon en builds con hilos.
printf() solo con %s y %d compila a interpolación de cadenas.
array_map() con un first-class callable o aplicación parcial compila a un bucle foreach sin Closures intermedios.
Los argumentos a constructores conocidos (new self()) se pasan de forma más eficiente.
La VM TAILCALL es más rápida y está disponible en Windows con Clang 19+.
JIT para builds ZTS en Apple Silicon; mejoras generales de ZTS.
str_ends_with(), str_split(), array_fill_keys(), array_intersect(), array_sum()/array_product() en arrays de enteros, array_unshift(), array_walk() y json_encode() con pretty print más rápidos.
compiled-away.php
// Both lines compile to the same string interpolation, no function call
printf("%s has %d items\n", $name, $count);
echo"$name has $count items\n";
// First-class callables and partials in array_map() become a plain loop:// no intermediate Closure, no userland-callback overhead, JIT-friendly$upper = array_map(strtoupper(...), $names);
$prefixed = array_map(str_replace('_', '-', ?), $keys);
Protocolos de seguridad AES192/AES256 para SNMPv3 donde net-snmp los soporte, snmp_init_mib() para reiniciar el árbol MIB y nuevos controles de análisis y salida MIB (snmp_set_mib_option(), snmp_set_output_option(), formatos de salida OID y cadena).
Deprecaciones
35 votaciones independientes en una sola RFC, cada una decidida por separado con 2/3 de los votos sí/no
Cada fila enlaza a la página de la votación con todos los votos nominales. El código deprecado sigue funcionando en 8.6 y emite E_DEPRECATED; la eliminación está prevista para PHP 9.0. RFC ↗ · El 8.6 Vote Watch →
También deprecado, mediante RFC separadas y entradas del changelog
Devolver un valor desde __construct() o __destruct(), y convertir cualquiera de los dos en generador. RFC ↗
Toda la familia mb_ereg*() / mb_split() y los ajustes INI mbstring.regex_* (fin de vida de Oniguruma). RFC ↗
Más de 16 filtros en una sola URL php://filter sin definir filter.max_filter_count. RFC ↗
Declarar __debugInfo() con tipo de retorno array|null o ?array; usa array.
Los operadores de desplazamiento y ** de GMP con un operando derecho flotante que pierde precisión al convertirse a int.
Cambios incompatibles
comportamiento que cambia sin periodo de deprecación
session.use_strict_mode, session.cookie_httponly y session.cookie_samesite pasan por defecto a 1, 1 y Lax. Los POST entre sitios y el acceso JavaScript a la cookie de sesión dejan de funcionar salvo opt-out explícito.
trim(), ltrim() y rtrim() eliminan por defecto el carácter de salto de página \f, como Python, JavaScript, Rust y Go.
unpack() interpreta un < o > justo después de un código de formato como modificador de endianness; "s<value" produce la clave value en vez de <value.
?? y empty() sobre una propiedad mágica ya no llaman a __get() cuando __isset() ha escrito la propiedad en el objeto; se devuelve directamente el valor escrito.
preg_grep() devuelve false en vez de un array parcial ante un error de ejecución de PCRE, en línea con las demás funciones preg_*.
array_intersect() convierte los valores a cadena durante el recorrido en vez de durante la ordenación, lo que cambia el número y el orden de los avisos de conversión y de las llamadas a __toString().
Decenas de funciones lanzan ahora un ValueError ante bytes NUL en argumentos de ruta, locale, entorno y similares en vez de truncar en silencio: file_exists(), is_file(), stat(), setlocale(), getenv(), putenv(), parse_str(), proc_open() y más.
sodium_crypto_pwhash*() lanzan ValueError en vez de SodiumException con argumentos opslimit/memlimit fuera de rango.
Dos closures sin estado de la misma posición del código son idénticos (===) por la nueva caché; los objetos que antes formaban un ciclo de referencias con un closure pueden destruirse antes.
Las propiedades DOM documentadas como @readonly (DOMNode::$nodeType y otras) se declaran public private(set); las escrituras fallan con otro mensaje de error.
Un agotamiento de memoria duro llama ahora a abort() en vez de exit(1): código de salida 134 y posiblemente un volcado de memoria.
lo que la gente espera encontrar aquí y no encontrará
El operador de asignación pipe |>= fue rechazado con 14 síes frente a 12 noes, un 54 por ciento y muy lejos de la mayoría de 2/3. $x = $x |> f(...) sigue siendo la forma de escribirlo; es el resultado más ajustado de toda la temporada de 8.6.
El pattern matching con una palabra clave is sigue en discusión y no llegó a votarse para 8.6. Las deprecaciones de los identificadores is y _ en esta versión reservan la sintaxis para más adelante.
el orden que encuentra los problemas antes que tus usuarios
Ejecuta hoy tu suite de tests en la beta
La imagen oficial php:8.6-rc sigue la última preversión. Primero pasa el linter a la base de código y luego ejecuta los tests con las deprecaciones convertidas en fallos; la ola de deprecaciones de 8.6 afecta sobre todo a código de bibliotecas, así que vendor/ importa tanto como src/.
terminal
# Lint every file with the 8.6 parser, then run the suite with deprecations as failuresdocker run --rm-v"$PWD":/app -w /app php:8.6-rc-cli \
sh -c'find src tests -name "*.php" -print0 | xargs -0 -n1 -P4 php -l | grep -v "No syntax errors"'docker run --rm-v"$PWD":/app -w /app php:8.6-rc-cli \
php -d error_reporting=E_ALL vendor/bin/phpunit --fail-on-deprecation
Busca los reemplazos mecánicos
La mayoría de las deprecaciones de 8.6 tienen un reemplazo uno a uno que un buscar y reemplazar resuelve en minutos: is_double(), is_integer(), is_long(), doubleval(), spl_object_hash(), strcoll(), mysqli::stmt_init(). Los conjuntos de reglas de Rector y PHP_CodeSniffer para 8.6 están en camino; la lista de esta página es la especificación.
terminal
# The one-to-one replacements, including vendor/grep-rnE'\b(is_double|is_integer|is_long|doubleval|strcoll|metaphone|spl_classes|spl_object_hash|mysqli_get_charset|mysqli_stmt_init|mb_ereg[a-z_]*|mb_split|mb_eregi)\s*\(' \
--include='*.php' src vendor
# Identifiers that are reserved from 8.6 ongrep-rnE'\b(function|class|interface|enum|const)\s+(let|is|_|readonly)\b'--include='*.php' src
Decide sobre los valores por defecto de la cookie de sesión
Si tu aplicación lee la cookie de sesión desde JavaScript, pasa identificadores de sesión entre subdominios o recibe POST entre sitios dentro de una sesión, fija explícitamente las tres directivas session.* antes de actualizar. Todo lo demás recibe la protección gratis.
Las extensiones PECL hay que recompilarlas en cada versión menor; la API de polling, los errores de stream y Duration son nuevas API internas que los autores de extensiones pueden adoptar. Las conexiones MySQL persistentes necesitan MySQL 5.7.3+ o MariaDB 10.2.4+; quien use mbregex necesita un plan para PHP 9.
Añade 8.6 a la matriz de CI y mantén 8.5 en verde
Ejecuta 8.6 como job con fallo permitido hasta la GA del 19 de noviembre de 2026 y después hazlo obligatorio. La comprobación de plataforma de Composer rechazará los paquetes cuyas restricciones se paren en 8.5: esa es tu lista de incidencias que abrir upstream.
La disponibilidad general está prevista para el 19 de noviembre de 2026. Antes: la beta 3 el 10 de septiembre, la congelación dura de funcionalidades el 22 de septiembre y cuatro release candidates del 24 de septiembre al 5 de noviembre de 2026.
¿Cuáles son las novedades principales de PHP 8.6?
Aplicación parcial de funciones, valores por defecto en propiedades readonly, escritura de propiedades en objetos dentro de constantes, la clase Time\Duration, clamp(), una API de polling nativa, errores de stream estructurados, #[\Override] para constantes, comentarios de documentación en parámetros y cookies de sesión seguras por defecto.
¿Tiene PHP 8.6 pattern matching?
No. La RFC de pattern matching sigue en discusión. PHP 8.6 solo reserva la palabra clave is y el comodín _ deprecándolos como identificadores.
¿Se aprobó el operador de asignación pipe |>=?
No. Obtuvo 14 votos a favor y 12 en contra, mayoría pero no los dos tercios necesarios. El operador pipe |> de PHP 8.5 no se ve afectado.
¿Está list() deprecado en PHP 8.6?
No. La votación terminó 23 a 23, así que la construcción se queda. La desestructuración corta [$a, $b] = … sigue siendo el estilo recomendado.
¿Qué se rompe al actualizar a PHP 8.6?
Lo más visible son las sesiones seguras por defecto: modo estricto, HttpOnly y SameSite=Lax. Además, trim() elimina saltos de página, unpack() trata < y > tras un código de formato como modificadores y muchas funciones lanzan ValueError ante bytes NUL. Las deprecaciones solo emiten avisos en 8.6.
¿Qué versión de MySQL o MariaDB necesita PHP 8.6?
Las conexiones persistentes requieren MySQL 5.7.3 o MariaDB 10.2.4 porque mysqlnd reinicia ahora las conexiones con COM_RESET_CONNECTION. Los servidores más antiguos siguen funcionando con conexiones normales.
¿Cómo puedo probar PHP 8.6 antes del lanzamiento?
Usa la imagen Docker oficial php:8.6-rc-cli, que sigue la última beta o release candidate, o descarga el código fuente de qa.php.net. Cada ejemplo de esta página marcado con una señal de verificación se ejecutó en la build actual.
Fuentes y método. Lista de funcionalidades según el archivo UPGRADING de php-src y el índice de RFC de wiki.php.net; recuentos reflejados desde las votaciones públicas (se requieren 2/3 de los votos sí/no, las abstenciones no cuentan); fechas del calendario de publicación de 8.6. Los ejemplos marcados «ejecutado en PHP 8.6.0beta2» se corrieron en la imagen Docker oficial; sus comentarios muestran la salida real. Revisado por una persona antes de publicarse y actualizado con cada beta.