Approfondimento· PHP 8.6 RC4

PHP 8.6 esce il 19 novembre: sessioni più sicure, partial application e cosa controllare nel tuo codice

PHP 8.6 è in calendario per il 19 novembre, dopo la RC4 del 21 ottobre. Cosa cambia davvero: nuovi default delle sessioni, partial function application, readonly con default, clamp() e le deprecazioni da cercare.

2P Ada Strict · La tecnica
Schermata arcade con la scritta PHP 8.6, la sequenza RC4, RC5, GA 19.11.2026 e l'avatar pixel art di Ada Strict
Crediti: Illustrazione: redazione TuttoPHP

PHP 8.6.0 RC4 è uscita il 21 ottobre 2026. Il calendario prevede la RC5, ultima release candidate, per il 5 novembre e la versione stabile per il 19 novembre. La RC4 è di fatto la terza release candidate, perché la RC1 è stata saltata. Contiene anche le correzioni di due vulnerabilità critiche che permettono l’esecuzione remota di codice senza autenticazione (CVE-2026-109618 e CVE-2026-109619, secondo il file NEWS). Dal 22 settembre è in vigore l’hard feature freeze, per cui una nuova funzionalità entra solo con l’approvazione dei release manager: è il momento giusto per capire cosa cambia nel tuo codice.

Perché ti riguarda

  • Tre impostazioni delle sessioni cambiano valore di default. Se la tua applicazione legge il cookie di sessione da JavaScript, riceve POST da un altro sito o usa un gestore di sessione personalizzato, va controllata prima di aggiornare.
  • Arrivano deprecazioni su funzioni molto comuni, come is_integer(), doubleval() e spl_object_hash(). Il codice continua a funzionare, ma emette avvisi di deprecazione.
  • Se stai già provando una build precedente di 8.6, l’annuncio ti chiede di passare subito alla RC4. In produzione, le stesse due vulnerabilità sono corrette nelle versioni 8.5.12, 8.4.27, 8.3.36 e 8.2.35, uscite lo stesso giorno.

Sessioni: tre default più sicuri

È il cambiamento con più impatto pratico. La RFC “Secure Session Configuration Defaults” di Jorg Sowa cambia tre valori, ciascuno approvato senza voti contrari, e il file UPGRADING li elenca tra le incompatibilità:

  • session.use_strict_mode passa da 0 a 1. In modalità strict PHP rifiuta gli ID di sessione non inizializzati, cioè quelli a cui non corrisponde una sessione salvata. È la difesa contro la session fixation, l’attacco in cui qualcuno ti fa usare un ID di sessione scelto da lui.
  • session.cookie_httponly passa da 0 a 1. Il cookie di sessione non è più leggibile da JavaScript tramite document.cookie, ma continua a viaggiare con le richieste.
  • session.cookie_samesite passa da vuoto a "Lax". Il browser non invia più il cookie nelle richieste cross-site, tranne nelle navigazioni di primo livello con metodi sicuri come GET.

Cosa può rompersi:

  • il JavaScript che legge il cookie di sessione;
  • i flussi che si aspettano il cookie su un POST in arrivo da un altro sito (la RFC cita il single sign-on SAML avviato dal service provider);
  • i gestori di sessione personalizzati che accettano ID forniti dall’esterno senza una sessione salvata.

Per i gestori personalizzati UPGRADING indica due strade: implementare validateId() e create_sid(), oppure impostare esplicitamente session.use_strict_mode=0. Se ti serve davvero SameSite=None, imposta anche session.cookie_secure a 1.

La trappola del vecchio php.ini

Un default vale solo se nessuno imposta il valore. I file php.ini-production e php.ini-development di PHP 8.5.12 lo impostano: contengono session.use_strict_mode = 0 e lasciano vuoti session.cookie_httponly e session.cookie_samesite. I modelli della RC4, invece, riportano già i nuovi valori. Se aggiorni PHP e ti porti dietro il vecchio php.ini, la protezione nuova non arriva.

Controlla cosa vale davvero sul tuo server:

<?php
foreach (['use_strict_mode', 'cookie_httponly', 'cookie_samesite'] as $k) {
    echo "session.$k = ";
    var_dump(ini_get("session.$k"));
}

Output su PHP 8.3 senza php.ini (php -n), cioè con i default di oggi:

session.use_strict_mode = string(1) "0"
session.cookie_httponly = string(1) "0"
session.cookie_samesite = string(0) ""

La buona notizia è che non devi aspettare PHP 8.6: puoi fissare questi valori già oggi, nel php.ini oppure all’avvio della sessione.

<?php
session_start([
    'use_strict_mode' => 1,
    'cookie_httponly' => 1,
    'cookie_samesite' => 'Lax',
]);
var_dump(ini_get('session.use_strict_mode'), ini_get('session.cookie_samesite'));

Output su PHP 8.3:

string(1) "1"
string(3) "Lax"

Partial function application

La partial function application (PFA) ti permette di chiamare una funzione lasciando alcuni argomenti in sospeso: invece del risultato ottieni una closure che aspetta quelli mancanti. I segnaposto sono due: ? indica che in quella posizione è atteso esattamente un argomento, ... che ne possono arrivare zero o più.

Prima, per passare str_replace() ad array_map() serviva una arrow function:

<?php
$arr = ['hello world', 'hello PHP'];
$result = array_map(static fn(string $string): string => str_replace('hello', 'hi', $string), $arr);
print_r($result);

Output su PHP 8.3:

Array
(
    [0] => hi world
    [1] => hi PHP
)

Con PHP 8.6 la stessa riga diventa:

<?php
$result = array_map(str_replace('hello', 'hi', ?), $arr);

Esempio tratto dalla RFC, non eseguito: richiede PHP 8.6

La RFC descrive ogni PFA come equivalente a una closure scritta a mano:

<?php
function foo(int $a, int $b, int $c, int $d): int
{
    return $a + $b + $c + $d;
}

$f = foo(1, ?, 3, 4);
$f = static fn(int $b): int => foo(1, $b, 3, 4);

$f = foo(1, ?, 3, ...);
$f = static fn(int $b, int $d): int => foo(1, $b, 3, $d);

Esempio tratto dalla RFC, non eseguito: richiede PHP 8.6

Due dettagli da sapere:

  • Gli argomenti già forniti vengono valutati quando crei la closure, non quando la chiami.
  • La PFA non funziona con new: il compilatore risponde “Cannot create Closure for new expression.”. I metodi factory statici invece sono ammessi.

UPGRADING aggiunge che un array_map() con una callback PFA viene compilato nel foreach equivalente, senza creare closure intermedie.

Proprietà readonly con un valore di default

Fino a oggi una proprietà readonly non poteva avere un default. Su PHP 8.3:

<?php
final class Config
{
    public readonly string $driver = 'mysql';
}
Fatal error: Readonly property Config::$driver cannot have default value in ... on line 4

La RFC “Readonly Property Defaults” toglie il divieto. La spinta viene dalle proprietà nelle interfacce: con un contratto come public string $className { get; } ha senso che un’implementazione dichiari il suo valore fisso direttamente sulla proprietà. Il default conta come inizializzazione, quindi dopo non puoi più riassegnare la proprietà, nemmeno nel costruttore:

<?php
final readonly class Rule
{
    public string $className = SomeParser::class;

    public function __construct(string $className) {
        $this->className = $className; // Error: Cannot modify readonly property Rule::$className
    }
}

Esempio tratto dalla RFC, non eseguito: richiede PHP 8.6

Qui $className è readonly perché lo è la classe. La RFC precisa anche un altro punto: il default su un parametro promosso del costruttore resta un default del parametro, non della proprietà.

clamp(): un valore dentro un intervallo

clamp($value, $min, $max) restituisce $value se è nell’intervallo, altrimenti il limite più vicino. Oggi lo scrivi a mano con min() e max(), nella forma che la RFC indica come equivalente:

<?php
function limita(int|float $value, int|float $min, int|float $max): int|float
{
    return max($min, min($max, $value));
}
var_dump(limita(0, 1, 3), limita(6, 1, 3), limita(2.5, 1, 3));
var_dump(limita(4, 8, 6)); // min > max: nessun errore

Output su PHP 8.3:

int(1)
int(3)
float(2.5)
int(8)

La funzione nativa dà gli stessi risultati nei casi normali, ma non accetta intervalli impossibili. La RFC elenca gli esempi come espressioni, con il risultato in commento:

<?php
clamp(2, min: 1, max: 3) // 2
clamp(0, min: 1, max: 3) // 1
clamp(6, min: 1, max: 3) // 3
clamp(2.5, 1, 3) // 2.5
clamp(4, 8, 6)
// Throws ValueError: clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)

Esempio tratto dalla RFC, non eseguito: richiede PHP 8.6

Se nel tuo progetto esiste già una funzione globale chiamata clamp(), fai attenzione: la RFC avverte che quel nome non sarà più disponibile.

#[\Override] anche sulle costanti

L’attributo #[\Override] dichiara che un metodo deve sovrascrivere un metodo del genitore. Se non lo fa, per esempio per un refuso nel nome, PHP si ferma con un errore fatale. Su PHP 8.3 funziona già sui metodi. Con PHP 8.6 vale anche per le costanti di classe, compresi i case delle enum:

<?php
class Base {
    protected const C = 'C';
}

class Child extends Base {
    #[\Override] // no error, override is validated
    public const C = 'Changed';
}

Esempio tratto dalla RFC, non eseguito: richiede PHP 8.6

Attenzione se la tua libreria deve girare anche su versioni precedenti. Lo stesso codice, su PHP 8.3, non parte proprio:

Fatal error: Attribute "Override" cannot target class constant (allowed targets: method) in ... on line 7

Incompatibilità e deprecazioni da cercare

trim() e il form feed. trim(), ltrim() e rtrim() ora rimuovono anche il form feed (\f), e la RFC lo dichiara come modifica non retrocompatibile. Su PHP 8.3:

<?php
$str = "\fHello World\f";
var_dump(strlen(trim($str)));
echo json_encode(trim($str)), PHP_EOL;
int(13)
"\fHello World\f"

Secondo la RFC, su PHP 8.6 trim($str) restituisce string(11) "Hello World". Se ti serve conservare il form feed, passa la lista dei caratteri in modo esplicito: trim($str, " \n\r\t\v\0").

Le deprecazioni. Una deprecazione non rompe niente subito: emette un avviso E_DEPRECATED e annuncia una rimozione futura. Le più probabili nel codice di tutti i giorni:

  • is_double(), is_long(), is_integer() e doubleval(): sono alias, usa is_float(), is_int() e floatval();
  • spl_object_hash(): usa spl_object_id();
  • return dentro un blocco finally;
  • un valore restituito da __construct() o __destruct() (un return; senza valore resta lecito).

Deprecazioni da cercare (fonte: UPGRADING di PHP 8.6.0RC4)

Cerca nel codice Cosa usare invece
is_double() is_float()
is_long(), is_integer() is_int()
doubleval() floatval()
spl_object_hash() spl_object_id()
return dentro finally ristrutturare il codice
return con valore in __construct()/__destruct() togliere il valore; return; resta lecito

Il caso del finally merita un esempio, perché oggi l’eccezione sparisce senza lasciare traccia:

<?php
function getConfig(): array {
    try {
        throw new RuntimeException('config mancante');
    } finally {
        return [];
    }
}
var_dump(getConfig());

Output su PHP 8.3, senza alcun avviso:

array(0) {
}

Per le funzioni deprecate basta una ricerca nel progetto:

grep -rnE '\b(is_double|is_long|is_integer|doubleval|spl_object_hash)\s*\(' src/

UPGRADING elenca molte altre incompatibilità. Sono soprattutto funzioni che ora lanciano ValueError su argomenti non validi o che contengono byte NUL: leggi le sezioni delle estensioni che usi.

Come provare la RC senza rischi

L’annuncio è esplicito: “Please DO NOT use this version in production”. Una release candidate serve a trovare i problemi prima della stabile. Un percorso ragionevole:

  1. Scarica l’ultima release candidate dalla pagina delle pre-release di php.net, che offre i sorgenti e le build per Windows, e verifica il checksum SHA-256.
  2. Installala in un ambiente separato: un job di CI dedicato, un container o una macchina di test, mai il server di produzione.
  3. Lancia la test suite con error_reporting=E_ALL e raccogli gli avvisi di deprecazione.
  4. Controlla le sessioni con lo snippet qui sopra e prova a mano i flussi di login, soprattutto quelli che coinvolgono un altro sito.
  5. Se trovi un bug, segnalalo su GitHub Issues di php-src: è proprio quello che chiede l’annuncio.

Trappole comuni

  • Pensare che i nuovi default delle sessioni si applichino da soli: se il tuo php.ini imposta i vecchi valori, vincono quelli.
  • Scrivere session.cookie_samesite = None senza virgolette nel php.ini: il modello della RC4 avverte che none senza virgolette viene letto come false.
  • Prendere alla lettera gli esempi con sleep() nella RFC della nuova classe Time\Duration: la RFC precisa che non modifica le funzioni esistenti per accettare oggetti Duration.
  • Contare su un polyfill di clamp() protetto da function_exists(): su PHP 8.6 entra in gioco la funzione nativa, che lancia ValueError se $min è maggiore di $max.

Fonti