chaque nouveauté, chaque dépréciation, le chemin de migration · tenu à jour depuis les scrutins RFC que nous reflétons en direct · mis à jour le 31 août 2026
PHP 8.6 est la dernière version mineure avant PHP 9. Elle sort le 19 novembre 2026 avec 27 RFC adoptées : application partielle de fonctions, valeurs par défaut pour les propriétés readonly, un vrai type Duration, clamp(), une API de polling qui passe à l’échelle, des erreurs de flux structurées et des cookies de session sécurisés d’office. L’autre moitié, c’est le ménage : 35 scrutins de dépréciation distincts, dont 31 adoptés, préparent les suppressions de PHP 9. Cette page suit la version de la bêta jusqu’à la GA ; chaque décompte ci-dessous est reflété depuis les scrutins publics de wiki.php.net, et chaque exemple de code coché a été exécuté sur la build 8.6 courante.
Un ? à la place d’un argument transforme l’appel en Closure pour le reste. Les placeholders nommés réordonnent les paramètres, ... récupère tout ce qui n’a pas encore été fourni, et la Closure obtenue conserve types, valeurs par défaut et passage par référence pour la réflexion.
C’est la moitié manquante des first-class callables de 8.1 et le partenaire naturel de l’opérateur pipe de 8.5. array_map(str_replace('a', 'b', ?), $list) remplace une ligne de fonction fléchée, et l’analyse statique voit la vraie signature. Une RFC complémentaire a rendu chaque ? obligatoire : l’arité que vous écrivez est celle que vous obtenez.
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}
Une propriété readonly peut désormais déclarer une valeur par défaut ; cette valeur compte comme l’unique initialisation. Avec les propriétés d’interface de 8.4, cela donne des constantes contractuelles par classe sans affectation dans le constructeur.
La RFC readonly d’origine jugeait les valeurs par défaut « pas particulièrement utiles ». Les propriétés d’interface ont changé la donne : une valeur readonly par défaut satisfait un contrat { get; } sans boilerplate. Attention : une affectation dans le constructeur est désormais une modification et lève une erreur, et unset() sur une telle propriété échoue.
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
Les objets dans les constantes existent depuis 8.1, mais toute écriture comme CONST->prop = 1 mourait avec « Cannot use temporary expression in write context ». PHP 8.6 lève cette restriction pour les affectations de propriétés, les opérateurs composés, les incréments, isset(), unset() et le passage par référence.
La liaison de la constante reste immuable ; seul l’état de l’objet change, comme les objets se comportent partout ailleurs. Les constantes tableau ne bougent pas (copy-on-write). Effet de bord : écrire dans la propriété d’un cas d’enum lève désormais l’erreur readonly habituelle au lieu d’une erreur fatale à la compilation.
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)
Une durée « chronomètre », finale et readonly, avec une précision à la nanoseconde : fabriques des secondes aux nanosecondes et depuis des chaînes ISO 8601, add/sub/multiplyBy/divideBy, opérateurs de comparaison et une plage protégée contre le dépassement d’environ 292 ans.
Timeouts, backoff et planification étaient des int dans une unité quelconque ou des flottants fragiles. DateInterval transporte des unités calendaires impossibles à convertir en secondes de façon fiable. Duration est sûre par construction et sert déjà de type de timeout à la nouvelle API de polling ; d’autres API internes suivront. sleep() et consorts prennent toujours un 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)
L’attribut introduit pour les méthodes en 8.3 et étendu aux propriétés en 8.5 couvre désormais les constantes de classe et les cas d’enum. Le compilateur signale une erreur si rien n’est réellement surchargé.
Surcharger une constante parente par accident est un bug silencieux classique. Marquer l’intention sécurise les refactorings : renommez la constante parente et l’enfant ne compile plus au lieu de définir discrètement une nouvelle constante.
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;
}
Les enums peuvent implémenter __debugInfo() ; var_dump() garde l’en-tête enum(Foo::Bar) et ajoute ce que la méthode retourne.
La sortie des enums dans les dumps et les débogueurs était figée et laconique. Les backed enums aux valeurs parlantes, ou les cas qui pointent vers une configuration, peuvent maintenant s’expliquer. __toString() sur les enums a été discuté puis écarté au profit de ce changement plus étroit.
Un commentaire /** … */ placé avant ou après un paramètre est conservé par le moteur et renvoyé par le nouveau ReflectionParameter::getDocComment().
La documentation des paramètres n’a plus à répéter nom et type dans une ligne @param au-dessus de la fonction, où elle se désynchronise sans bruit. Les frameworks pilotés par attributs et les générateurs d’API lisent le texte directement par réflexion.
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 */
Une closure statique qui ne capture aucune variable et ne déclare aucune variable static est créée une fois puis réutilisée. Deux closures de ce type issues du même emplacement source sont désormais le même objet.
Les chemins chauds qui recréent la même closure à chaque itération n’allouent plus. La seconde partie de la RFC, inférer static pour les closures qui n’utilisent jamais $this, a été retirée après le vote à cause d’un cas limite avec les callables sous forme de chaîne ; marquez vos closures static vous-même pour le bénéfice complet.
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) renvoie la valeur si elle est dans les bornes, sinon la borne la plus proche. La fonction accepte tout ce qui est comparable, chaînes et objets DateTime compris, et lève un ValueError si $min dépasse $max ou si une borne vaut NAN.
L’idiome max($min, min($max, $v)) cache deux bugs : des bornes inversées et un NAN qui passe en silence. La version native vérifie les deux et va légèrement plus vite que le ternaire 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 contexte de flux peut basculer la gestion d’erreurs vers des exceptions ou une collecte silencieuse : error_mode (Error, Exception, Silent), error_store et un callback error_handler. Les erreurs arrivent sous forme d’objets StreamError avec un cas de l’enum StreamErrorCode, le nom du wrapper, la sévérité et le chemin concerné.
Jusqu’ici file_get_contents() et consorts signalaient l’échec par des warnings à intercepter avec un gestionnaire d’erreurs et à analyser comme du texte. Avec StreamErrorMode::Exception, une ouverture ratée est une StreamException aux codes lisibles par la machine ; les valeurs par défaut ne bougent pas, rien ne change sans votre demande.
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)
Une API de multiplexage native au-dessus d’epoll, kqueue, event ports, WSAPoll ou poll() : Io\Poll\Context surveille des handles pour les événements Read, Write, Error et HangUp, gère les modes one-shot et edge-triggered et renvoie depuis wait() les watchers déclenchés.
stream_select() est en O(n), plafonné à 1024 descripteurs sur bien des systèmes et ignore les noyaux modernes. Les boucles d’événements comme Revolt, ReactPHP ou AMPHP peuvent tourner sur un backend intégré au lieu d’ext-uv ou ext-event, et les runtimes façon FrankenPHP obtiennent une API interne partagée. Seules les ressources de flux sont pollables dans cette première version.
Uri\Rfc3986\UriBuilder et Uri\WhatWg\UrlBuilder assemblent une URI dans un seul objet et valident au build() ; getUriType() distingue les URI absolues des références de chemin et de réseau, getHostType() indique IPv4, IPv6, domaine ou hôte opaque, et isSpecialScheme() dit si les règles WHATWG des schémas spéciaux s’appliquent.
Les classes URI de 8.5 n’offraient que des withers immuables : un objet alloué par étape et une dépendance à l’ordre des appels. Le builder supprime les deux. L’aide à l’encodage pourcent de la même RFC n’a pas encore atterri dans la bêta.
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 non adossé avec les cas Ascending et Descending, pensé comme le type unique de sens de tri sur lequel PHP, frameworks et bibliothèques convergent.
Doctrine, les query builders et chaque ORM livrent leur propre enum Order. Un type partagé rend les signatures interopérables ; une union avec les anciens drapeaux string ou int garde les API existantes compatibles pendant la migration.
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
Les deux méthodes répondent si une lecture ou une écriture réussirait depuis une portée donnée, éventuellement pour un objet concret. Elles comprennent readonly, la visibilité asymétrique, les hooks et les accesseurs magiques, ce qu’isPublic() n’a jamais fait.
Depuis readonly (8.1) et private(set) (8.4), public ne signifie plus modifiable. Sérialiseurs, hydrateurs et mappers devaient réimplémenter les règles du moteur ; ils les demandent maintenant au moteur.
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)
Les modificateurs < et > à la Perl fonctionnent désormais sur les codes entiers s, l, q, S, L, Q et sur les codes flottants f et d. Les appliquer à un code au boutisme intrinsèque lève un ValueError.
Lire un int32 signé little-endian demandait trois appels unpack() et des décalages de bits. Les parseurs de protocoles binaires et de formats de fichiers tiennent en une chaîne de format. Attention au nommage : unpack('s<value') produit désormais la clé value, pas <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"
Échappe et entoure une chaîne de guillemets simples en une étape, le pendant mysqli de PDO::quote(). Disponible en mysqli::quote_string() et mysqli_quote_string().
real_escape_string() n’a jamais été correct qu’entre guillemets simples ; avec des guillemets doubles et NO_BACKSLASH_ESCAPES, c’était une faille d’injection. La nouvelle fonction supprime le quoting manuel là où les requêtes préparées ne sont pas une option, comme dans les query builders qui doivent produire du SQL complet.
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!'
Les contextes de flux gagnent session_data, session_new_cb, session_cache et les options associées avec un objet Openssl\Session exportable et importable ; psk_client_cb/psk_server_cb activent les clés pré-partagées externes, et early_data ajoute le 0-RTT de TLS 1.3.
Chaque requête PHP recommençait un handshake TLS complet. Persister la session dans Redis ou APCu économise un aller-retour par appel HTTPS sortant, et les serveurs écrits en PHP peuvent adosser leur cache de sessions à n’importe quel stockage.
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() inverse par grappe de graphèmes, les séquences d’emoji et les diacritiques restent intacts. Locale::getDisplayKeyword() et getDisplayKeywordValue() localisent les mots-clés de locale, IntlNumberRangeFormatter formate des plages numériques, SpoofChecker apprend les confusions bidirectionnelles et IntlDatePatternGenerator renvoie des squelettes.
L’extension intl continue de combler ses écarts avec ICU. La plupart de ces ajouts sont petits, mais la paire display-keyword complète une famille incomplète depuis 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);
Trois valeurs INI par défaut changent : session.use_strict_mode=1 rejette les identifiants de session que le serveur n’a pas émis, session.cookie_httponly=1 cache le cookie à JavaScript et session.cookie_samesite=Lax l’empêche de voyager avec les POST inter-sites.
C’est le changement de 8.6 le plus susceptible d’atteindre la production sans être remarqué. Les frameworks fixent ces valeurs depuis des années, le PHP nu souvent pas. Les passages d’identifiant entre sous-domaines, le JavaScript qui lit PHPSESSID et les formulaires inter-sites cassent ; chaque cas a un opt-out explicite dans 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)
Avec error_include_args=1, les warnings levés par les fonctions internes incluent les arguments de l’appel, via la même mécanique que les backtraces d’exceptions : les valeurs #[\SensitiveParameter] restent masquées et zend.exception_string_param_max_len tronque les longues chaînes.
chmod(): Operation not permitted ne dit rien ; chmod('/var/app/cache', 511) dit tout. Le réglage est désactivé par défaut dans la bêta pour des raisons de volume de logs et de données personnelles ; activez-le d’abord en staging, délibérément.
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)
Une URL php://filter de plus de 16 filtres émet désormais E_DEPRECATED ; la limite se relève par contexte via filter.max_filter_count, et stream_filter_append() reste illimité.
Les chaînes de filtres convert.iconv et base64 sont l’outil standard pour transformer une inclusion de fichier local en exécution de code à distance. Le code légitime empile rarement plus de trois filtres : le plafond supprime la classe d’exploit sans interrupteur INI à oublier.
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 avec valeur dans un constructeur ou un destructeur émet une dépréciation à la compilation, tout comme yield qui en ferait un générateur. Un return; nu pour sauter la suite du corps reste légal.
Les constructeurs ne peuvent pas déclarer de type de retour, rien n’empêchait donc return $this; de s’installer ; un audit des 4000 principaux paquets Composer a trouvé 77 instructions de ce genre. PHP 9 transformera la dépréciation en erreur.
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
}
}
}
Toutes les fonctions mbregex (mb_ereg, mb_ereg_replace, mb_split, mb_eregi, …), les réglages INI mbstring.regex_* et MB_ONIGURUMA_VERSION sont dépréciés en 8.6 et prévus pour suppression en 9.0, la bibliothèque Oniguruma n’étant plus maintenue depuis avril 2025.
PCRE avec le modificateur u couvre les cas UTF-8. Le code qui dépend d’expressions régulières multi-octets hors UTF-8 aura besoin du paquet externe mb_onig maintenu par l’auteur de la RFC. Cherchez mb_ereg et mb_split dès maintenant ; 147 occurrences ont été trouvées dans les principaux paquets 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);
Compiler depuis git exige autoconf 2.71, les connexions MySQL persistantes demandent MySQL 5.7.3 ou MariaDB 10.2.4 pour COM_RESET_CONNECTION, et les builds Windows officielles passent à Visual Studio 2026, OpenSSL 4 et libxml2 2.15.
autoconf 2.71 (uniquement pour les builds depuis git ; les archives de release livrent configure)
MySQL ≥ 5.7.3 / MariaDB ≥ 10.2.4 pour les connexions persistantes dans mysqlnd et PDO
Windows : toolchain VS 2026 (VS18), OpenSSL 4, libxml2 2.15.3
PHP_OS_FAMILY gagne la valeur AIX
Moteur & performance
ce qui est devenu plus rapide sans toucher à votre code
Plusieurs motifs compilent désormais vers des opcodes moins coûteux, les builds ZTS ont accéléré partout et le JIT atteint Apple Silicon dans les builds threadées.
printf() avec seulement %s et %d compile en interpolation de chaîne.
array_map() avec un first-class callable ou une application partielle compile en boucle foreach sans Closure intermédiaire.
Les arguments des constructeurs connus (new self()) sont transmis plus efficacement.
La VM TAILCALL est plus rapide et disponible sous Windows avec Clang 19+.
JIT pour les builds ZTS sur Apple Silicon ; accélérations ZTS générales.
str_ends_with(), str_split(), array_fill_keys(), array_intersect(), array_sum()/array_product() sur tableaux d’entiers, array_unshift(), array_walk() et json_encode() en pretty print plus rapides.
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);
Protocoles de sécurité AES192/AES256 pour SNMPv3 quand net-snmp les supporte, snmp_init_mib() pour réinitialiser l’arbre MIB, et de nouveaux contrôles d’analyse et de sortie MIB (snmp_set_mib_option(), snmp_set_output_option(), formats de sortie OID et chaîne).
Dépréciations
35 scrutins distincts dans une seule RFC, chacun tranché séparément à la majorité des 2/3 des voix oui/non
Chaque ligne renvoie à la page du scrutin avec toutes les voix nominatives. Le code déprécié fonctionne encore en 8.6 et émet E_DEPRECATED ; la suppression est prévue pour PHP 9.0. RFC ↗ · Le 8.6 Vote Watch →
Également déprécié, via des RFC séparées et des entrées de changelog
Retourner une valeur depuis __construct() ou __destruct(), et en faire un générateur. RFC ↗
Toute la famille mb_ereg*() / mb_split() et les réglages INI mbstring.regex_* (fin de vie d’Oniguruma). RFC ↗
Plus de 16 filtres dans une seule URL php://filter sans définir filter.max_filter_count. RFC ↗
Déclarer __debugInfo() avec un type de retour array|null ou ?array ; utilisez array.
Les opérateurs de décalage et ** de GMP avec un opérande droit flottant qui perd en précision à la conversion en int.
Ruptures de compatibilité
des comportements qui changent sans période de dépréciation
session.use_strict_mode, session.cookie_httponly et session.cookie_samesite valent par défaut 1, 1 et Lax. Les POST inter-sites et l’accès JavaScript au cookie de session cessent de fonctionner sauf opt-out explicite.
trim(), ltrim() et rtrim() retirent par défaut le caractère de saut de page \f, comme Python, JavaScript, Rust et Go.
unpack() lit un < ou > juste après un code de format comme modificateur de boutisme ; "s<value" produit la clé value au lieu de <value.
?? et empty() sur une propriété magique n’appellent plus __get() dès que __isset() a écrit la propriété dans l’objet ; la valeur écrite est renvoyée directement.
preg_grep() renvoie false au lieu d’un tableau partiel en cas d’erreur d’exécution PCRE, comme les autres fonctions preg_*.
array_intersect() convertit les valeurs en chaînes pendant le parcours plutôt que pendant le tri, ce qui change le nombre et l’ordre des avertissements de conversion et des appels à __toString().
Des dizaines de fonctions lèvent désormais un ValueError pour des octets NUL dans les arguments de chemin, de locale, d’environnement et similaires au lieu de tronquer en silence : file_exists(), is_file(), stat(), setlocale(), getenv(), putenv(), parse_str(), proc_open() et d’autres.
sodium_crypto_pwhash*() lèvent ValueError au lieu de SodiumException pour des arguments opslimit/memlimit hors plage.
Deux closures sans état issues du même emplacement source sont identiques (===) à cause du nouveau cache ; les objets qui formaient un cycle de références avec une closure peuvent être détruits plus tôt.
Les propriétés DOM documentées @readonly (DOMNode::$nodeType et d’autres) sont déclarées public private(set) ; les écritures échouent avec un message d’erreur différent.
Un dépassement mémoire dur appelle désormais abort() au lieu d’exit(1) : code de sortie 134 et éventuellement un core dump.
ce que l’on s’attend à trouver ici et que l’on ne trouvera pas
L’opérateur d’affectation pipe |>= a été rejeté par 14 oui contre 12 non, soit 54 pour cent, loin de la majorité des 2/3. $x = $x |> f(...) reste la façon de l’écrire ; c’est le résultat le plus serré de toute la saison 8.6.
Le pattern matching avec un mot-clé is est encore en discussion et n’a pas été soumis au vote pour 8.6. Les dépréciations des identifiants is et _ dans cette version réservent la syntaxe pour plus tard.
La dépréciation de list() s’est terminée sur une égalité 23 à 23. Une égalité n’est pas une majorité des 2/3, donc list($a, $b) = … reste pleinement pris en charge.
l’ordre qui trouve les problèmes avant vos utilisateurs
Lancez votre suite de tests sur la bêta dès aujourd’hui
L’image officielle php:8.6-rc suit la dernière préversion. Lintez d’abord la base de code, puis exécutez les tests avec les dépréciations transformées en échecs ; la vague de dépréciations de 8.6 touche surtout le code des bibliothèques, vendor/ compte donc autant que 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
Cherchez les remplacements mécaniques
La plupart des dépréciations de 8.6 ont un remplacement un pour un qu’un rechercher-remplacer règle en quelques minutes : is_double(), is_integer(), is_long(), doubleval(), spl_object_hash(), strcoll(), mysqli::stmt_init(). Les jeux de règles Rector et PHP_CodeSniffer pour 8.6 arrivent ; la liste de cette page en est la spécification.
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
Tranchez sur les valeurs par défaut du cookie de session
Si votre application lit le cookie de session en JavaScript, transmet des identifiants de session entre sous-domaines ou reçoit des POST inter-sites dans une session, fixez explicitement les trois directives session.* avant de migrer. Tout le reste gagne la protection gratuitement.
Vérifiez les extensions et les prérequis de plateforme
Les extensions PECL doivent être recompilées à chaque version mineure ; l’API de polling, les erreurs de flux et Duration sont de nouvelles API internes que les auteurs d’extensions peuvent adopter. Les connexions MySQL persistantes exigent MySQL 5.7.3+ ou MariaDB 10.2.4+ ; les utilisateurs de mbregex ont besoin d’un plan pour PHP 9.
Ajoutez 8.6 à la matrice CI, gardez 8.5 au vert
Faites tourner 8.6 en job autorisé à échouer jusqu’à la GA du 19 novembre 2026, puis rendez-le obligatoire. Le contrôle de plateforme de Composer refusera les paquets dont les contraintes s’arrêtent à 8.5 : c’est votre liste de tickets à ouvrir en amont.
La disponibilité générale est prévue le 19 novembre 2026. Avant cela : la bêta 3 le 10 septembre, le gel dur des fonctionnalités le 22 septembre et quatre release candidates du 24 septembre au 5 novembre 2026.
Quelles sont les nouveautés phares de PHP 8.6 ?
L’application partielle de fonctions, les valeurs par défaut des propriétés readonly, l’écriture de propriétés sur les objets dans les constantes, la classe Time\Duration, clamp(), une API de polling native, les erreurs de flux structurées, #[\Override] pour les constantes, les commentaires de documentation sur les paramètres et des cookies de session sécurisés par défaut.
PHP 8.6 a-t-il le pattern matching ?
Non. La RFC de pattern matching est encore en discussion. PHP 8.6 réserve seulement le mot-clé is et le joker _ en les dépréciant comme identifiants.
L’opérateur d’affectation pipe |>= a-t-il été adopté ?
Non. Il a obtenu 14 oui et 12 non, une majorité mais pas les deux tiers requis. L’opérateur pipe |> de PHP 8.5 n’est pas concerné.
list() est-il déprécié dans PHP 8.6 ?
Non. Le scrutin s’est terminé à 23 contre 23, la construction reste. La déstructuration courte [$a, $b] = … reste le style recommandé.
Qu’est-ce qui casse en passant à PHP 8.6 ?
Le changement le plus visible est l’ensemble des sessions sécurisées par défaut : mode strict, HttpOnly et SameSite=Lax. Au-delà, trim() retire les sauts de page, unpack() traite < et > après un code de format comme des modificateurs, et beaucoup de fonctions lèvent ValueError sur des octets NUL. Les dépréciations n’émettent que des avertissements en 8.6.
Quelle version de MySQL ou MariaDB PHP 8.6 exige-t-il ?
Les connexions persistantes exigent MySQL 5.7.3 ou MariaDB 10.2.4, car mysqlnd réinitialise désormais les connexions avec COM_RESET_CONNECTION. Les serveurs plus anciens fonctionnent toujours avec des connexions ordinaires.
Comment essayer PHP 8.6 avant la sortie ?
Utilisez l’image Docker officielle php:8.6-rc-cli, qui suit la dernière bêta ou release candidate, ou téléchargez les sources sur qa.php.net. Chaque exemple coché sur cette page a été exécuté sur la build courante.
Sources et méthode. Liste des fonctionnalités d’après le fichier UPGRADING de php-src et l’index des RFC sur wiki.php.net ; décomptes reflétés depuis les scrutins publics (2/3 des voix oui/non requis, les abstentions ne comptent pas) ; dates issues du calendrier de sortie 8.6. Les exemples marqués « exécuté sur PHP 8.6.0beta2 » ont tourné sur l’image Docker officielle ; leurs commentaires montrent la sortie réelle. Relu par un humain avant publication et mis à jour à chaque bêta.