lundi 31 août 2026
FR

Abonnez-vous dans votre lecteur de flux — choisissez vos rubriques :

https://php-net.pro/fr/news/feed.atom

php-net.pro · le journal des devs

The Dev Dispatch

The Dev Dispatch · guide de version

PHP 8.6

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.

27RFC implémentées
31sur 35 scrutins de dépréciation adoptés
1 765voix des contributeurs core
80jours avant la GA · 19 nov. 2026

Build actuelle: PHP 8.6.0beta2 · 27 août 2026 Notes de version & discussion → Builds QA sur qa.php.net ↗

Langage

syntaxe et modèle objet

Application partielle de fonctions

RFC: partial_function_application_v2, partial_function_application_optional_placeholder

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}
✓ exécuté sur PHP 8.6.0beta2

Valeurs par défaut pour les propriétés readonly

RFC: readonly_property_defaults 24–0 adopté

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
{
    public string $name { get; }
    public array $steps { get; }
}

// Fixed metadata without constructor boilerplate:
// the default IS the initialising assignment
final readonly class ChangelogIngestor implements Ingestor
{
    public string $name = 'changelog';
    public array $steps = [ParseMarkdown::class, ExtractVersions::class];
}

echo new ChangelogIngestor()->name;      // changelog
✓ exécuté sur PHP 8.6.0beta2

Écriture de propriétés sur des objets tenus par des constantes

RFC: const_object_property_write 17–2 adopté

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)
✓ exécuté sur PHP 8.6.0beta2

Time\Duration

RFC: duration_class 35–1 adopté 30 multiplyBy, divideBy, neg… · 2 mul, divBy, neg, abs tranché

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)
✓ exécuté sur PHP 8.6.0beta2

#[\Override] sur les constantes de classe

RFC: override_constants

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
{
    protected const RETRIES = 3;
}

class Worker extends Base
{
    #[\Override]                 // compile error if Base had no RETRIES
    public const RETRIES = 5;
}

enum Level: int implements HasDefault
{
    #[\Override]                 // enum cases may override interface constants
    case Default = 0;
}
✓ exécuté sur PHP 8.6.0beta2

__debugInfo() sur les enums

RFC: debugable-enums

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.

enum-debuginfo.php
enum Status: string
{
    case Active = 'active';

    public function __debugInfo(): array
    {
        return [self::class . '::' . $this->name . ' = ' . $this->value];
    }
}

var_dump(Status::Active);
// enum(Status::Active) (1) {
//   [0]=>
//   string(23) "Status::Active = active"
// }
✓ exécuté sur PHP 8.6.0beta2

Commentaires de documentation sur les paramètres

RFC: parameter-doccomments

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 */
✓ exécuté sur PHP 8.6.0beta2

Les closures sans état sont mises en cache

RFC: closure-optimizations

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 => cached
    return static fn (int $x): int => $x * 2;
}

// PHP 8.5: two instances. PHP 8.6: the same cached Closure.
var_dump(handler() === handler());       // bool(true)
✓ exécuté sur PHP 8.6.0beta2

Bibliothèque standard

nouvelles fonctions, classes et API

clamp()

RFC: clamp_v2

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 rules
echo 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
✓ exécuté sur PHP 8.6.0beta2

Erreurs de flux structurées

RFC: stream_errors

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)
✓ exécuté sur PHP 8.6.0beta2

API de polling (Io\Poll)

RFC: poll_api

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.

poll-server.php
use Io\Poll\{Context, Event};
use Time\Duration;

$poll = new Context();                              // epoll / kqueue / WSAPoll
$server = stream_socket_server('tcp://127.0.0.1:8080');
stream_set_blocking($server, false);
$poll->add(new StreamPollHandle($server), [Event::Read], 'server');

while ($watchers = $poll->wait(Duration::fromSeconds(1))) {
    foreach ($watchers as $w) {
        if ($w->getData() === 'server') {
            $client = stream_socket_accept($w->getHandle()->getStream(), 0);
            $poll->add(new StreamPollHandle($client), [Event::Read], 'client');
        } elseif ($w->hasTriggered(Event::Read)) {
            $stream = $w->getHandle()->getStream();
            fwrite($stream, 'echo: ' . fread($stream, 8192));
        }
    }
}
✓ exécuté sur PHP 8.6.0beta2

ext/uri : builders et détection de type

RFC: uri_followup

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=dispatch
echo $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
✓ exécuté sur PHP 8.6.0beta2

enum SortDirection

RFC: sort_direction_enum

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
✓ exécuté sur PHP 8.6.0beta2

ReflectionProperty::isReadable() et isWritable()

RFC: isreadable-iswriteable

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
final class Money
{
    public function __construct(
        public readonly int $amount,
        public private(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)
✓ exécuté sur PHP 8.6.0beta2

Modificateurs de boutisme pour pack()/unpack()

RFC: pack-unpack-endianness-signed-integers-support, pack-unpack-float-endianness-modifier

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"
✓ exécuté sur PHP 8.6.0beta2

mysqli::quote_string()

RFC: mysqli_quote_string

É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!'

Reprise de session TLS, PSK et 0-RTT

RFC: tls_session_resumption_api

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);

Intl : grapheme_strrev() et plus

RFC: grapheme_strrev, getdisplaykeyword_and_getdisplaykeywordvalue

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 survive
echo grapheme_strrev("PHP 👨‍👩‍👧 8.6");        // 6.8 👨‍👩‍👧 PHP

// Localised labels for locale keywords
echo Locale::getDisplayKeyword('calendar', 'de');                 // Kalender
echo 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);

Ajouts plus petits à connaître

Le changelog est long ; voici les entrées les plus susceptibles d’apparaître dans du code applicatif.

  • json_last_error_msg() et JsonException indiquent désormais l’offset du token fautif.
  • curl_getinfo() rapporte size_delivered, et CURLOPT_SEEKFUNCTION laisse libcurl rembobiner un corps de requête streamé lors des redirections.
  • PDO_PGSQL : Pdo\Pgsql::ATTR_CHUNK_SIZE récupère les lignes par blocs avec libpq 17.
  • Les sockets de flux acceptent les options de contexte so_keepalive, tcp_keepidle, tcp_keepintvl, tcp_keepcnt, so_linger et so_reuseaddr.
  • openssl_sign()/openssl_verify() prennent un $salt_length pour RSA-PSS ; openssl_x509_parse() liste les extensions critiques.
  • ZipArchive::openString() et closeString() travaillent sur des archives en mémoire.
  • gmp_prevprime() et gmp_powm_sec(), résistant aux canaux auxiliaires.
  • L’affectation par référence dans une WeakMap n’exige plus que la clé existe.
  • ini_get_all() expose builtin_default_value par directive.
  • Le serveur de développement CLI répond à la méthode HTTP QUERY.
  • array_filter() gagne la constante explicite ARRAY_FILTER_USE_VALUE ; finfo_file() fonctionne sur les flux distants.

Sécurité & exploitation

des valeurs par défaut qui changent le comportement en production

Sessions sécurisées par défaut

RFC: session_security_defaults

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)
✓ exécuté sur PHP 8.6.0beta2

Arguments de fonction dans les messages d’erreur

RFC: display_error_function_args

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)
✓ exécuté sur PHP 8.6.0beta2

Chaînes php://filter plafonnées à 16

RFC: limit-maximum-number-of-filter-chains

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]]);

return depuis __construct()/__destruct() est déprécié

RFC: deprecate-return-value-from-construct

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
{
    public function __construct()
    {
        return $this;   // Deprecated: Returning a value from a constructor is deprecated
    }

    public function __destruct()
    {
        if (!$this->open) {
            return;     // bare "return;" stays legal
        }
    }
}
✓ exécuté sur PHP 8.6.0beta2

mb_ereg*() déprécié : Oniguruma en fin de vie

RFC: eol-oniguruma

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);

Versions minimales et plateformes

RFC: min_supported_versions_php_8_6 27–2 adopté 26–0 adopté

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

Performance

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);

SNMP

RFC: snmp_improvements_2026

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 →

Déprécié en 8.6À utiliser à la placeScrutin
is_double() is_float() 40–2 adopté
is_integer() is_int() 39–2 adopté
is_long() is_int() 39–2 adopté
doubleval() floatval() 38–2 adopté
strcoll() Collator::compare() 39–2 adopté
SORT_LOCALE_STRING Collator::sort() / asort() 38–2 adopté
metaphone() userland phonetic library 20–8 adopté
spl_classes() ReflectionExtension::getClassNames() 43–0 adopté
spl_object_hash() spl_object_id() 26–9 adopté
ArrayIterator::asort() / ksort() / setFlags() … ArrayObject or native arrays 42–1 adopté
SplFileObject::fgetcsv() / fputcsv() fgetcsv() / fputcsv() on a stream 25–5 adopté
mysqli::stmt_init() mysqli::prepare() 40–0 adopté
mysqli_get_charset() mysqli_character_set_name() 43–0 adopté
define($n, $v, $case_insensitive) define($n, $v) 41–0 adopté
is_subclass_of("Cls", …, allow_string: false) class_exists() + allow_string: true 37–0 adopté
is_a("Cls", …, allow_string: false) class_exists() + allow_string: true 38–0 adopté
ReflectionProperty::setValue($foreignObject, …) pass an instance of the declaring class 43–1 adopté
ReflectionMethod::invoke($obj) on static methods invoke(null, …) 39–3 adopté
array_walk($object, …) array_walk(get_object_vars($object), …) 41–3 adopté
deflate_init($enc, $optionsObject) get_object_vars($optionsObject) 39–4 adopté
zlib.* filter with object params get_object_vars() 38–4 adopté
bzip2.* filter with object params get_object_vars() 38–4 adopté
mb_convert_variables(…, $object) get_object_vars() 39–2 adopté
http_build_query($object) http_build_query(get_object_vars($object)) 21–10 adopté
session handler without create_sid() / validateId() implement both methods 26–2 adopté
return inside finally {} return after the try/finally 39–3 adopté
function readonly() rename the function 39–1 adopté
const namespace = … rename the constant 29–8 adopté
let as class / function / constant name rename (reserved for a future keyword) 24–11 adopté
is as class / function / constant name rename (reserved for pattern matching) 29–10 adopté
const _ / use Foo as _ rename (reserved as wildcard) 34–4 adopté

Rejeté : ceux-là restent quatre propositions ont manqué le seuil des 2/3 et restent intactes

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

php-src UPGRADING ↗

Pas dans PHP 8.6

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.

RFC ↗ 14–12 rejeté

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.

RFC ↗

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.

RFC ↗ 23–23 rejeté

Réserver in, out et inout comme identifiants a échoué 8 contre 21 ; le code qui les utilise comme noms n’est pas concerné.

RFC ↗ 8–21 rejeté

Guide de migration

l’ordre qui trouve les problèmes avant vos utilisateurs
  1. 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 failures
    docker 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
  2. 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 on
    grep -rnE '\b(function|class|interface|enum|const)\s+(let|is|_|readonly)\b' --include='*.php' src
  3. 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.

    ↑ Sessions sécurisées par défaut

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

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

    .github/workflows/ci.yml
    jobs:
      test:
        strategy:
          fail-fast: false
          matrix:
            php: ['8.4', '8.5', '8.6']
        continue-on-error: ${{ matrix.php == '8.6' }}   # required after GA
        steps:
          - uses: shivammathur/setup-php@v2
            with:
              php-version: ${{ matrix.php }}
          - run: composer install --ignore-platform-req=php+
          - run: vendor/bin/phpunit --fail-on-deprecation

Nos articles sur PHP 8.6

l’histoire telle qu’elle s’est déroulée : votes RFC, bêtas et débat sur les dépréciations

Le 8.6 Vote Watch : les 42 scrutins sur une frise chronologique →

Questions fréquentes

Quand PHP 8.6 sort-il ?
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.

retour à la une