Montag, 31. August 2026
DE

Abonniere die Zeitung im Feed-Reader — wähle deine Sparten:

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

php-net.pro · die Dev-News-Zeitung

The Dev Dispatch

The Dev Dispatch · Release-Leitfaden

PHP 8.6

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.

27RFCs implementiert
31von 35 Deprecation-Abstimmungen angenommen
1.765Stimmen der Core-Contributor
80Tage bis GA · 19.11.2026

Aktueller Build: PHP 8.6.0beta2 · 27.08.2026 Release-Notes & Diskussion → QA-Builds auf qa.php.net ↗

Sprache

Syntax und Objektmodell

Partial Function Application

RFCs: partial_function_application_v2, partial_function_application_optional_placeholder

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}
✓ lief auf PHP 8.6.0beta2

Default-Werte für readonly-Properties

RFC: readonly_property_defaults 24–0 angenommen

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
{
    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
✓ lief auf PHP 8.6.0beta2

Property-Schreibzugriffe auf Objekte in Konstanten

RFC: const_object_property_write 17–2 angenommen

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)
✓ lief auf PHP 8.6.0beta2

Time\Duration

RFC: duration_class 35–1 angenommen 30 multiplyBy, divideBy, neg… · 2 mul, divBy, neg, abs entschieden

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)
✓ lief auf PHP 8.6.0beta2

#[\Override] auf Klassenkonstanten

RFC: override_constants

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
{
    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;
}
✓ lief auf PHP 8.6.0beta2

__debugInfo() auf Enums

RFC: debugable-enums

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.

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"
// }
✓ lief auf PHP 8.6.0beta2

Doc-Comments an Parametern

RFC: parameter-doccomments

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 */
✓ lief auf PHP 8.6.0beta2

Zustandslose Closures werden gecacht

RFC: closure-optimizations

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 => 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)
✓ lief auf PHP 8.6.0beta2

Standardbibliothek

neue Funktionen, Klassen und APIs

clamp()

RFC: clamp_v2

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 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
✓ lief auf PHP 8.6.0beta2

Strukturierte Stream-Fehler

RFC: stream_errors

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)
✓ lief auf PHP 8.6.0beta2

Polling-API (Io\Poll)

RFC: poll_api

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.

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));
        }
    }
}
✓ lief auf PHP 8.6.0beta2

ext/uri: Builder und Typerkennung

RFC: uri_followup

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=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
✓ lief auf PHP 8.6.0beta2

enum SortDirection

RFC: sort_direction_enum

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
✓ lief auf PHP 8.6.0beta2

ReflectionProperty::isReadable() und isWritable()

RFC: isreadable-iswriteable

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
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)
✓ lief auf PHP 8.6.0beta2

Endianness-Modifier für pack()/unpack()

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

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"
✓ lief auf PHP 8.6.0beta2

mysqli::quote_string()

RFC: mysqli_quote_string

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

TLS-Session-Resumption, PSK und 0-RTT

RFC: tls_session_resumption_api

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

Intl: grapheme_strrev() und mehr

RFCs: grapheme_strrev, getdisplaykeyword_and_getdisplaykeywordvalue

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

Kleinere Ergänzungen, die man kennen sollte

Das Changelog ist lang; das sind die Einträge, die am ehesten in Anwendungscode auftauchen.

  • json_last_error_msg() und JsonException nennen jetzt den Offset des fehlerhaften Tokens.
  • curl_getinfo() meldet size_delivered, und CURLOPT_SEEKFUNCTION lässt libcurl einen gestreamten Request-Body bei Redirects zurückspulen.
  • PDO_PGSQL: Pdo\Pgsql::ATTR_CHUNK_SIZE holt Zeilen in Blöcken (libpq 17).
  • Stream-Sockets akzeptieren die Context-Optionen so_keepalive, tcp_keepidle, tcp_keepintvl, tcp_keepcnt, so_linger und so_reuseaddr.
  • openssl_sign()/openssl_verify() nehmen ein $salt_length für RSA-PSS; openssl_x509_parse() listet kritische Extensions.
  • ZipArchive::openString() und closeString() arbeiten auf Archiven im Speicher.
  • gmp_prevprime() und das seitenkanalresistente gmp_powm_sec().
  • Referenz-Zuweisung in eine WeakMap verlangt nicht mehr, dass der Schlüssel existiert.
  • ini_get_all() liefert je Direktive builtin_default_value.
  • Der CLI-Entwicklungsserver beantwortet die HTTP-Methode QUERY.
  • array_filter() bekommt die explizite Konstante ARRAY_FILTER_USE_VALUE; finfo_file() arbeitet auf entfernten Streams.

Sicherheit & Betrieb

Defaults, die das Verhalten in Produktion ändern

Sichere Session-Defaults

RFC: session_security_defaults

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)
✓ lief auf PHP 8.6.0beta2

Funktionsargumente in Fehlermeldungen

RFC: display_error_function_args

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)
✓ lief auf PHP 8.6.0beta2

php://filter-Ketten auf 16 gedeckelt

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

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 aus __construct()/__destruct() ist deprecated

RFC: deprecate-return-value-from-construct

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
{
    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
        }
    }
}
✓ lief auf PHP 8.6.0beta2

mb_ereg*() deprecated: Oniguruma ist End-of-Life

RFC: eol-oniguruma

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

Mindestversionen und Plattformen

RFC: min_supported_versions_php_8_6 27–2 angenommen 26–0 angenommen

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
  • Windows: VS-2026-Toolchain (VS18), OpenSSL 4, libxml2 2.15.3
  • PHP_OS_FAMILY kennt den Wert AIX

Engine & Performance

was schneller wurde, ohne dass du Code anfasst

Performance

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

SNMP

RFC: snmp_improvements_2026

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 →

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

Abgelehnt: das bleibt vier Vorschläge verfehlten die 2/3-Hürde und bleiben unangetastet

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.

php-src UPGRADING ↗

Nicht in PHP 8.6

was man hier erwartet und nicht finden wird

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.

RFC ↗ 14–12 abgelehnt

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.

RFC ↗

Die Deprecation von list() endete 23 zu 23. Ein Patt ist keine 2/3-Mehrheit, also bleibt list($a, $b) = … voll unterstützt.

RFC ↗ 23–23 abgelehnt

in, out und inout als Bezeichner zu reservieren scheiterte 8 zu 21; Code mit diesen Namen ist nicht betroffen.

RFC ↗ 8–21 abgelehnt

Upgrade-Leitfaden

die Reihenfolge, die Probleme findet, bevor deine Nutzer es tun
  1. 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 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. 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 on
    grep -rnE '\b(function|class|interface|enum|const)\s+(let|is|_|readonly)\b' --include='*.php' src
  3. Ü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.

    ↑ Sichere Session-Defaults

  4. Extensions und Plattform-Anforderungen prüfen

    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.

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

    .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

Unsere Berichte zu PHP 8.6

die Geschichte, wie sie passiert ist: RFC-Abstimmungen, Betas und die Deprecation-Debatte

Der 8.6 Vote Watch: alle 42 Abstimmungen auf der Zeitachse →

Häufige Fragen

Wann erscheint PHP 8.6?
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.

zurück zur Titelseite