jedes Feature, jede Deprecation, der Upgrade-Pfad · gepflegt aus den RFC-Abstimmungen, die wir live spiegeln · aktualisiert 31.08.2026
PHP 8.6 ist das letzte Minor-Release vor PHP 9. Es erscheint am 19. November 2026 mit 27 angenommenen RFCs: Partial Function Application, Default-Werte für readonly-Properties, ein echter Duration-Typ, clamp(), eine skalierbare Polling-API, strukturierte Stream-Fehler und sichere Session-Cookies ab Werk. Die andere Hälfte ist Aufräumen: 35 einzelne Deprecation-Abstimmungen, davon 31 angenommen, bereiten vor, was PHP 9 entfernen wird. Diese Seite begleitet das Release von der Beta bis zum GA. Jede Stimmenzahl unten ist von den öffentlichen Ballots auf wiki.php.net gespiegelt, jedes Code-Beispiel mit Häkchen wurde auf dem aktuellen 8.6-Build ausgeführt.
Ein ? an Stelle eines Arguments macht aus dem Aufruf eine Closure für den Rest. Benannte Platzhalter ordnen Parameter um, ... sammelt alles noch nicht Übergebene ein, und die erzeugte Closure behält Typen, Defaults und Referenz-Flags für Reflection.
Das ist die fehlende Hälfte der First-Class-Callables aus 8.1 und der natürliche Partner des Pipe-Operators aus 8.5. array_map(str_replace('a', 'b', ?), $list) ersetzt eine Zeile Arrow-Function-Boilerplate, und statische Analyse sieht die echte Signatur. Ein Folge-RFC hat jeden ?-Platzhalter zum Pflichtparameter gemacht: die Stelligkeit, die du schreibst, ist die, die du bekommst.
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}
Readonly-Properties dürfen jetzt einen Default deklarieren; der Default gilt als die einzige Initialisierung. Zusammen mit Interface-Properties aus 8.4 ergibt das vertragliche Konstanten pro Klasse ohne Konstruktor-Zuweisungen.
Der ursprüngliche readonly-RFC nannte Defaults „nicht besonders nützlich“. Interface-Properties haben das geändert: Ein readonly-Default erfüllt einen { get; }-Vertrag ohne Boilerplate. Vorsicht: Eine Zuweisung im Konstruktor ist jetzt eine Änderung und wirft, und unset() auf so einer Property ist ein Fehler.
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
Objekte in Konstanten gibt es seit 8.1, aber jeder Schreibzugriff wie CONST->prop = 1 starb mit „Cannot use temporary expression in write context“. PHP 8.6 hebt das für Property-Zuweisungen, Compound-Operatoren, Inkremente, isset(), unset() und Referenz-Übergabe auf.
Die Bindung der Konstante bleibt unveränderlich, nur der Objektzustand ändert sich, genau wie Objekte sich überall sonst verhalten. Array-Konstanten bleiben unverändert (Copy-on-Write). Nebenwirkung: Ein Schreibzugriff auf die Property eines Enum-Case wirft jetzt den normalen readonly-Fehler statt eines Compile-Fatals.
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)
Eine finale, readonly Stoppuhr-Dauer mit Nanosekunden-Präzision: Fabriken von Sekunden bis Nanosekunden und aus ISO-8601-Strings, add/sub/multiplyBy/divideBy, Vergleichsoperatoren und ein überlaufsicherer Bereich von rund 292 Jahren.
Timeouts, Backoff und Scheduling waren bisher int-in-irgendeiner-Einheit oder fragile Floats. DateInterval schleppt Kalendereinheiten mit, die sich nicht zuverlässig in Sekunden umrechnen lassen. Duration ist per Konstruktion einheitensicher und bereits der Timeout-Typ der neuen Polling-API; weitere interne APIs werden folgen. sleep() und Verwandte nehmen weiterhin 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)
Das Attribut, das 8.3 für Methoden einführte und 8.5 auf Properties erweiterte, gilt jetzt auch für Klassenkonstanten und Enum-Cases. Der Compiler meldet einen Fehler, wenn nichts überschrieben wird.
Eine Elternkonstante versehentlich zu überschreiben ist ein klassischer stiller Bug. Die markierte Absicht macht Refactorings sicher: Wird die Elternkonstante umbenannt, kompiliert das Kind nicht mehr, statt leise eine neue Konstante zu definieren.
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;
}
Enums dürfen jetzt __debugInfo() implementieren; var_dump() behält den enum(Foo::Bar)-Kopf und hängt an, was die Methode zurückgibt.
Enum-Ausgaben in Dumps und Debuggern waren fix und knapp. Backed Enums mit sprechenden Werten oder Cases, die auf Konfiguration zeigen, können sich jetzt selbst erklären. __toString() auf Enums wurde diskutiert und zugunsten dieser engeren Änderung verworfen.
Ein /** … */-Kommentar vor oder hinter einem Parameter wird von der Engine aufbewahrt und über das neue ReflectionParameter::getDocComment() zurückgegeben.
Parameter-Dokumentation muss Name und Typ nicht mehr in einer @param-Zeile über der Funktion wiederholen, wo sie unbemerkt auseinanderläuft. Attribut-getriebene Frameworks und API-Generatoren holen den Text direkt aus der Reflection.
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 */
Eine statische Closure, die keine Variablen einfängt und keine static-Variablen deklariert, wird einmal erzeugt und wiederverwendet. Zwei solche Closures aus derselben Quellzeile sind jetzt identische Objekte.
Hot Paths, die pro Iteration dieselbe Closure bauen, allozieren nicht mehr. Der zweite Teil des RFC, static für Closures ohne $this-Nutzung automatisch abzuleiten, wurde nach der Abstimmung wegen eines Randfalls mit Callable-Strings zurückgezogen; markiere Closures selbst als static, um den vollen Effekt zu bekommen.
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) liefert den Wert, wenn er innerhalb der Grenzen liegt, sonst die nächste Grenze. Es akzeptiert alles Vergleichbare, auch Strings und DateTime-Objekte, und wirft einen ValueError, wenn $min größer als $max oder eine Grenze NAN ist.
Das übliche max($min, min($max, $v)) versteckt zwei Bugs: vertauschte Grenzen und NAN, das leise durchrutscht. Die native Version prüft beides und ist etwas schneller als der Userland-Ternary.
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
Ein Stream-Context kann die Fehlerbehandlung auf Exceptions oder stilles Sammeln umstellen: error_mode (Error, Exception, Silent), error_store und ein error_handler-Callback. Fehler kommen als StreamError-Wertobjekte mit StreamErrorCode-Enum, Wrapper-Name, Schweregrad und betroffenem Pfad.
Bis jetzt meldeten file_get_contents() und Co. Fehler über Warnings, die man mit einem Error-Handler abfangen und als Text parsen musste. Mit StreamErrorMode::Exception ist ein fehlgeschlagenes Öffnen eine StreamException mit maschinenlesbaren Codes; die Defaults bleiben unangetastet, es ändert sich nur, was du anforderst.
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)
Eine native Multiplexing-API über epoll, kqueue, Event Ports, WSAPoll oder poll(): Io\Poll\Context überwacht Handles auf Read-, Write-, Error- und HangUp-Events, kennt One-Shot- und Edge-Triggered-Modus und gibt aus wait() die ausgelösten Watcher zurück.
stream_select() ist O(n), auf vielen Systemen bei 1024 Deskriptoren gedeckelt und weiß nichts von modernen Kerneln. Event-Loops wie Revolt, ReactPHP oder AMPHP können jetzt auf einem eingebauten Backend laufen statt auf ext-uv oder ext-event, und Runtimes im FrankenPHP-Stil bekommen eine gemeinsame interne API. In dieser ersten Version sind nur Stream-Ressourcen pollbar.
Uri\Rfc3986\UriBuilder und Uri\WhatWg\UrlBuilder bauen eine URI in einem Objekt und validieren bei build(); getUriType() unterscheidet absolute URIs von Pfad- und Netzwerk-Referenzen, getHostType() meldet IPv4, IPv6, Domain oder opaken Host, und isSpecialScheme() sagt, ob die WHATWG-Sonderregeln greifen.
Die URI-Klassen aus 8.5 kannten nur unveränderliche Wither, also ein Objekt pro Schritt und Abhängigkeit von der Aufrufreihenfolge. Der Builder beseitigt beides. Der Percent-Encoding-Helfer aus demselben RFC ist in der Beta noch nicht gelandet.
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
Ein globales, nicht-backed Enum mit den Cases Ascending und Descending, gedacht als der eine Sortierrichtungs-Typ, auf den PHP, Frameworks und Bibliotheken sich einigen.
Doctrine, Query-Builder und jedes ORM bringen ein eigenes Order-Enum mit. Ein gemeinsamer Typ macht Signaturen interoperabel; eine Union mit den alten String- oder Int-Flags hält bestehende APIs kompatibel, während sie migrieren.
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
Beide Methoden beantworten, ob ein Lese- oder Schreibzugriff aus einem bestimmten Scope gelingen würde, wahlweise für ein konkretes Objekt. Sie verstehen readonly, asymmetrische Sichtbarkeit, Hooks und magische Accessoren, was isPublic() nie konnte.
Seit readonly (8.1) und private(set) (8.4) heißt public nicht mehr beschreibbar. Serializer, Hydratoren und Mapper mussten die Regeln der Engine nachbauen; jetzt fragen sie die Engine.
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)
Die Perl-Modifier < und > funktionieren jetzt auf den Integer-Codes s, l, q, S, L, Q und den Float-Codes f und d. Auf Codes mit fester Byte-Reihenfolge werfen sie einen ValueError.
Ein vorzeichenbehafteter Little-Endian-int32 brauchte bisher drei unpack()-Aufrufe und Bit-Shifts. Parser für Binärprotokolle und Dateiformate werden zu einem Format-String. Achtung bei Namen: unpack('s<value') liefert jetzt den Schlüssel value, nicht <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"
Escapt und setzt einen String in einem Schritt in einfache Anführungszeichen, das mysqli-Gegenstück zu PDO::quote(). Verfügbar als mysqli::quote_string() und mysqli_quote_string().
real_escape_string() funktionierte nur innerhalb einfacher Anführungszeichen korrekt; mit doppelten und NO_BACKSLASH_ESCAPES war es ein Injection-Loch. Die neue Funktion entfernt das manuelle Quoting dort, wo Prepared Statements keine Option sind, etwa in Query-Buildern, die komplettes SQL rendern müssen.
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!'
Stream-Contexts bekommen session_data, session_new_cb, session_cache und verwandte Optionen mit einem exportier- und importierbaren Openssl\Session-Objekt; psk_client_cb/psk_server_cb erlauben externe Pre-Shared Keys, und early_data bringt TLS-1.3-0-RTT.
Jeder PHP-Request begann bisher mit einem frischen TLS-Handshake. Die Session in Redis oder APCu abzulegen spart pro ausgehendem HTTPS-Aufruf einen Roundtrip, und in PHP geschriebene Server können ihren Session-Cache mit jedem Store hinterlegen.
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() kehrt nach Grapheme-Clustern um, Emoji-Sequenzen und kombinierende Zeichen bleiben intakt. Locale::getDisplayKeyword() und getDisplayKeywordValue() lokalisieren Locale-Keywords, IntlNumberRangeFormatter formatiert Zahlenbereiche, SpoofChecker lernt bidirektionale Verwechslungen und IntlDatePatternGenerator liefert Skeletons.
Die intl-Erweiterung schließt weiter Lücken zu ICU. Das meiste ist klein, aber das Display-Keyword-Paar vervollständigt eine Familie, die seit PHP 5.3 unvollständig war.
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);
Drei INI-Defaults ändern sich: session.use_strict_mode=1 verwirft Session-IDs, die der Server nicht vergeben hat, session.cookie_httponly=1 versteckt das Cookie vor JavaScript und session.cookie_samesite=Lax hält es aus Cross-Site-POSTs heraus.
Das ist die 8.6-Änderung, die am ehesten unbemerkt in Produktion ankommt. Frameworks setzen diese Werte seit Jahren, reines PHP oft nicht. Session-Übergaben zwischen Subdomains, JavaScript, das PHPSESSID liest, und Cross-Site-Formulare brechen; für jeden Fall gibt es ein explizites Opt-out in der 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)
Mit error_include_args=1 enthalten Warnings eingebauter Funktionen die Argumente des Aufrufs, über dieselbe Mechanik wie Exception-Backtraces: #[\SensitiveParameter]-Werte bleiben maskiert, zend.exception_string_param_max_len kürzt lange Strings.
chmod(): Operation not permitted sagt nichts; chmod('/var/app/cache', 511) schon. Die Einstellung ist in der Beta wegen Log-Größe und PII-Bedenken standardmäßig aus, also bewusst zuerst im Staging aktivieren.
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)
Eine php://filter-URL mit mehr als 16 Filtern erzeugt jetzt E_DEPRECATED; das Limit lässt sich pro Context über filter.max_filter_count anheben, stream_filter_append() bleibt unbegrenzt.
Ketten aus convert.iconv- und base64-Filtern sind das Standardwerkzeug, um eine Local File Inclusion zu Remote Code Execution zu machen. Legitimer Code stapelt selten mehr als drei Filter, also entfernt die Deckelung die Exploit-Klasse ohne einen INI-Schalter, den man vergessen könnte.
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 mit Wert in Konstruktor oder Destruktor erzeugt eine Compile-Zeit-Deprecation, ebenso yield, das beide zu Generatoren machen würde. Ein nacktes return; zum Überspringen des Rests bleibt erlaubt.
Konstruktoren können keinen Rückgabetyp deklarieren, also hielt nichts return $this; auf; ein Audit der Top-4000-Composer-Pakete fand 77 solcher Statements. PHP 9 macht aus der Deprecation einen Fehler.
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
}
}
}
Alle mbregex-Funktionen (mb_ereg, mb_ereg_replace, mb_split, mb_eregi, …), die INI-Einstellungen mbstring.regex_* und MB_ONIGURUMA_VERSION sind in 8.6 deprecated und für 9.0 zur Entfernung vorgesehen, weil die Oniguruma-Bibliothek seit April 2025 nicht mehr gepflegt wird.
PCRE mit u-Modifier deckt UTF-8-Fälle ab. Code, der auf Multibyte-Regex jenseits von UTF-8 baut, braucht das externe mb_onig-Paket des RFC-Autors. Jetzt nach mb_ereg und mb_split greppen; in den Top-Composer-Paketen fanden sich 147 Vorkommen.
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);
Builds aus Git brauchen autoconf 2.71, persistente MySQL-Verbindungen verlangen MySQL 5.7.3 oder MariaDB 10.2.4 für COM_RESET_CONNECTION, und die offiziellen Windows-Builds wechseln auf Visual Studio 2026, OpenSSL 4 und libxml2 2.15.
autoconf 2.71 (nur für Builds aus Git; Release-Tarballs liefern configure mit)
MySQL ≥ 5.7.3 / MariaDB ≥ 10.2.4 für persistente Verbindungen in mysqlnd und PDO
Mehrere Muster kompilieren jetzt zu günstigeren Opcodes, ZTS-Builds wurden durchgehend schneller, und der JIT erreicht Apple Silicon in Thread-Builds.
printf() nur mit %s und %d kompiliert zu String-Interpolation.
array_map() mit First-Class-Callable oder Partial Application kompiliert zu einer foreach-Schleife ohne Zwischen-Closures.
Argumente an bekannte Konstruktoren (new self()) werden effizienter übergeben.
Die TAILCALL-VM ist schneller und unter Windows mit Clang 19+ verfügbar.
JIT für ZTS-Builds auf Apple Silicon; allgemeine ZTS-Beschleunigung.
Schnellere str_ends_with(), str_split(), array_fill_keys(), array_intersect(), array_sum()/array_product() auf Int-Arrays, array_unshift(), array_walk(), json_encode() mit Pretty Print.
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);
AES192/AES256-Sicherheitsprotokolle für SNMPv3, wo net-snmp sie unterstützt, snmp_init_mib() zum Zurücksetzen des MIB-Baums sowie neue Steuerung von MIB-Parsing und Ausgabe (snmp_set_mib_option(), snmp_set_output_option(), OID- und String-Ausgabeformate).
Deprecations
35 einzelne Abstimmungen in einem RFC, jede für sich mit 2/3 der Ja/Nein-Stimmen entschieden
Jede Zeile verlinkt auf die Ballot-Seite mit allen namentlichen Stimmen. Deprecated Code läuft in 8.6 weiter und erzeugt E_DEPRECATED; die Entfernung ist für PHP 9.0 geplant. RFC ↗ · Der 8.6 Vote Watch →
Ebenfalls deprecated, über eigene RFCs und Changelog-Einträge
Rückgabe eines Werts aus __construct() oder __destruct(), sowie beide als Generator. RFC ↗
Die gesamte mb_ereg*()-/mb_split()-Familie und die INI-Einstellungen mbstring.regex_* (Oniguruma End-of-Life). RFC ↗
Mehr als 16 Filter in einer php://filter-URL ohne gesetztes filter.max_filter_count. RFC ↗
__debugInfo() mit Rückgabetyp array|null oder ?array; array verwenden.
GMP-Shift- und **-Operatoren mit Float-Operand rechts, der bei der Umwandlung in int Präzision verliert.
Breaking Changes
Verhalten, das sich ohne Deprecation-Phase ändert
session.use_strict_mode, session.cookie_httponly und session.cookie_samesite stehen standardmäßig auf 1, 1 und Lax. Cross-Site-Formulare und JavaScript-Zugriff auf das Session-Cookie funktionieren nur noch mit explizitem Opt-out.
trim(), ltrim() und rtrim() entfernen standardmäßig auch den Seitenvorschub \f, wie Python, JavaScript, Rust und Go.
unpack() liest ein < oder > direkt nach einem Format-Code als Endianness-Modifier; "s<value" liefert den Schlüssel value statt <value.
?? und empty() auf einer magischen Property rufen __get() nicht mehr, sobald __isset() die Property ins Objekt geschrieben hat; der geschriebene Wert wird direkt zurückgegeben.
preg_grep() liefert bei einem PCRE-Ausführungsfehler false statt eines Teil-Arrays, konsistent mit den anderen preg_*-Funktionen.
array_intersect() wandelt Werte beim Durchlaufen statt beim Sortieren in Strings um; Anzahl und Reihenfolge von Konvertierungswarnungen und __toString()-Aufrufen ändern sich.
Dutzende Funktionen werfen bei NUL-Bytes in Pfad-, Locale-, Umgebungs- und ähnlichen Argumenten jetzt einen ValueError statt still abzuschneiden: file_exists(), is_file(), stat(), setlocale(), getenv(), putenv(), parse_str(), proc_open() und mehr.
sodium_crypto_pwhash*() werfen bei opslimit/memlimit außerhalb des erlaubten Bereichs ValueError statt SodiumException.
Zwei zustandslose Closures aus derselben Quellzeile sind wegen des neuen Closure-Cache identisch (===); Objekte, die bisher mit einer Closure in einem Referenzzyklus hingen, werden möglicherweise früher destruiert.
DOM-Properties, die als @readonly dokumentiert waren (DOMNode::$nodeType und andere), sind jetzt public private(set); Schreibversuche scheitern mit anderer Fehlermeldung.
Ein hartes Out-of-Memory ruft jetzt abort() statt exit(1): Exit-Code 134 und möglicherweise ein Core-Dump.
Der Pipe-Zuweisungsoperator |>= wurde mit 14 Ja gegen 12 Nein abgelehnt, 54 Prozent und deutlich unter der 2/3-Mehrheit. $x = $x |> f(...) bleibt die Schreibweise; das Ergebnis ist das knappste der ganzen 8.6-Saison.
Pattern Matching mit einem is-Schlüsselwort ist weiter in Diskussion und kam für 8.6 nicht zur Abstimmung. Die Deprecations von is und _ als Bezeichner reservieren die Syntax für eine spätere Version.
die Reihenfolge, die Probleme findet, bevor deine Nutzer es tun
Testsuite heute auf der Beta laufen lassen
Das offizielle Image php:8.6-rc folgt der jeweils neuesten Vorabversion. Erst die Codebasis linten, dann die Tests mit Deprecations als Fehlschlag laufen lassen; die 8.6-Deprecation-Welle trifft vor allem Bibliothekscode, vendor/ zählt also genauso wie 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
Nach den mechanischen Ersetzungen greppen
Die meisten 8.6-Deprecations haben einen 1:1-Ersatz, den Suchen-und-Ersetzen in Minuten erledigt: is_double(), is_integer(), is_long(), doubleval(), spl_object_hash(), strcoll(), mysqli::stmt_init(). Rector- und PHP_CodeSniffer-Regelsätze für 8.6 sind unterwegs; die Liste auf dieser Seite ist die Spezifikation.
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
Über die Session-Cookie-Defaults entscheiden
Liest deine Anwendung das Session-Cookie per JavaScript, reicht Session-IDs zwischen Subdomains weiter oder empfängt Cross-Site-POSTs in eine Session, setze die drei session.*-Direktiven vor dem Upgrade explizit. Alles andere bekommt den Schutz geschenkt.
PECL-Extensions müssen für jedes Minor neu gebaut werden; Polling-API, Stream-Fehler und Duration sind neue interne APIs, die Extension-Autoren übernehmen können. Persistente MySQL-Verbindungen brauchen MySQL 5.7.3+ oder MariaDB 10.2.4+; mbregex-Nutzer brauchen einen Plan für PHP 9.
8.6 in die CI-Matrix, 8.5 grün halten
Lass 8.6 bis zum GA am 19. November 2026 als allowed-to-fail-Job laufen und schalte ihn dann auf Pflicht. Composers Plattform-Check verweigert Pakete, deren Constraints bei 8.5 enden; das ist deine Liste der Upstream-Issues.
Die General Availability ist für den 19. November 2026 geplant. Davor: Beta 3 am 10. September, der Hard Feature Freeze am 22. September und vier Release Candidates vom 24. September bis 5. November 2026.
Was sind die wichtigsten Features von PHP 8.6?
Partial Function Application, Default-Werte für readonly-Properties, Property-Schreibzugriffe auf Objekte in Konstanten, die Klasse Time\Duration, clamp(), eine native Polling-API, strukturierte Stream-Fehler, #[\Override] für Konstanten, Doc-Comments an Parametern und sichere Session-Cookie-Defaults.
Hat PHP 8.6 Pattern Matching?
Nein. Der Pattern-Matching-RFC ist noch in Diskussion. PHP 8.6 reserviert nur das Schlüsselwort is und den Platzhalter _, indem es sie als Bezeichner deprecated.
Wurde der Pipe-Zuweisungsoperator |>= angenommen?
Nein. Er bekam 14 Ja- und 12 Nein-Stimmen, eine Mehrheit, aber nicht die nötigen zwei Drittel. Der Pipe-Operator |> aus PHP 8.5 ist davon nicht betroffen.
Ist list() in PHP 8.6 deprecated?
Nein. Die Abstimmung endete 23 zu 23, das Konstrukt bleibt. Die kurze Array-Destrukturierung [$a, $b] = … bleibt der empfohlene Stil.
Was bricht beim Upgrade auf PHP 8.6?
Am sichtbarsten sind die sicheren Session-Defaults: Strict Mode, HttpOnly und SameSite=Lax. Außerdem entfernt trim() Seitenvorschübe, unpack() behandelt < und > nach einem Format-Code als Modifier, und viele Funktionen werfen bei NUL-Bytes ValueError. Deprecations erzeugen in 8.6 nur Hinweise.
Welche MySQL- oder MariaDB-Version braucht PHP 8.6?
Persistente Verbindungen verlangen MySQL 5.7.3 oder MariaDB 10.2.4, weil mysqlnd Verbindungen jetzt mit COM_RESET_CONNECTION zurücksetzt. Ältere Server funktionieren weiter mit normalen Verbindungen.
Wie kann ich PHP 8.6 vor dem Release ausprobieren?
Mit dem offiziellen Docker-Image php:8.6-rc-cli, das der jeweils neuesten Beta oder dem Release Candidate folgt, oder mit dem Quellcode von qa.php.net. Jedes Beispiel auf dieser Seite mit Häkchen lief auf dem aktuellen Build.
Quellen und Methode. Feature-Liste aus der UPGRADING-Datei von php-src und dem RFC-Index auf wiki.php.net; Stimmenzahlen von den öffentlichen Ballots gespiegelt (2/3 der Ja/Nein-Stimmen nötig, Enthaltungen zählen nicht); Termine aus dem 8.6-Release-Plan. Beispiele mit „lief auf PHP 8.6.0beta2“ wurden auf dem offiziellen Docker-Image ausgeführt; die Kommentare zeigen die echte Ausgabe. Vor Veröffentlichung von einem Menschen geprüft, mit jeder Beta aktualisiert.