Was ist neu in PHP 8.5: Pipe Operator, URI Extension und mehr

Drei Jahre. So lange hat die PHP-Community darauf gewartet. Und am 20. November 2025 war es endlich soweit: der Pipe Operator ist da.

Oder: Wie PHP lernte, funktionale Komposition zu lieben

Wenn du die Entwicklung von PHP verfolgst, weißt du, dass jedes Release schrittweise Verbesserungen bringt. Constructor Property Promotion in PHP 8.0 hat uns Millionen Zeilen Boilerplate erspart. Enums in PHP 8.1 haben unseren Code sicherer gemacht. Aber PHP 8.5? Das ist was anderes. Laut Larry Garfield (ja, der Larry Garfield von PHP-FIG) liefert der Pipe Operator allein “eines der besten Preis-Leistungs-Verhältnisse aller Features der letzten Zeit.”

Und er hat recht. Lass mich dir zeigen, warum dieses Release wichtig ist – nicht nur wegen der Headline-Features, sondern wegen dem, was es über PHPs Zukunft aussagt.

Permalink to "Der Pipe Operator: Endlich Links-nach-Rechts-Logik"

Das Problem, mit dem wir alle leben mussten

Permalink to "Das Problem, mit dem wir alle leben mussten"

Schnelltest: Was macht dieser Code?

$result = ucfirst(trim(strtolower($input)));

Du liest ihn von innen nach außen, oder? Dein Gehirn hat das so geparst:

  1. In Kleinbuchstaben umwandeln
  2. Whitespace trimmen
  3. Ersten Buchstaben groß schreiben

Aber so liest man kein Deutsch. Oder Englisch. Oder die meisten menschlichen Sprachen. Wir lesen von links nach rechts, nicht von innen nach außen wie ein durchgeknallter LISP-Interpreter.

Ich habe jahrelang Code so geschrieben und dabei mental verschachtelte Funktionsaufrufe in eine lineare Erzählung umgewandelt. Manchmal habe ich es für die Lesbarkeit aufgeteilt:

$temp1 = strtolower($input);
$temp2 = trim($temp1);
$result = ucfirst($temp2);

Besser? Vielleicht. Aber jetzt verschmutze ich meinen Namespace mit $temp1 und $temp2 Variablen, die nur existieren, um meinen Code lesbar zu machen. Wir haben besseres verdient.

Die Transformation

Permalink to "Die Transformation"

PHP 8.5 führt den Pipe Operator (|>) ein, und plötzlich liest sich Code wie eine Geschichte:

$result = $input
    |> strtolower(...)
    |> trim(...)
    |> ucfirst(...);

Von links nach rechts. Von oben nach unten. So wie Menschen wirklich denken.

Die ...-Syntax ist der Partial Application Operator (aus PHP 8.1), der anzeigt “setze den gepipten Wert hier ein”. Bei Callables, die nur einen Parameter akzeptieren, kannst du ihn komplett weglassen:

$result = $input |> 'strtolower' |> 'trim' |> 'ucfirst';

Real-World-Beispiel: Datenverarbeitungs-Pipeline

Permalink to "Real-World-Beispiel: Datenverarbeitungs-Pipeline"

Hier wird’s praktisch. Stell dir vor, du verarbeitest User-Input für eine Suchanfrage:

// Der alte Weg
function prepareSearchQuery(string $input): string {
    return mb_substr(
        trim(
            preg_replace('/\s+/', ' ',
                htmlspecialchars_decode(
                    strtolower($input)
                )
            )
        ),
        0,
        100
    );
}

// Der neue Weg
function prepareSearchQuery(string $input): string {
    return $input
        |> strtolower(...)
        |> htmlspecialchars_decode(...)
        |> fn($s) => preg_replace('/\s+/', ' ', $s)
        |> trim(...)
        |> fn($s) => mb_substr($s, 0, 100);
}

Fällt dir was auf? Die neue Version liest sich wie ein Rezept. Jeder Schritt ist klar. Jede Transformation ist offensichtlich. Und wenn was kaputt geht (weil um 3 Uhr nachts geht immer was kaputt), kannst du einzelne Schritte auskommentieren zum Debuggen:

return $input
    |> strtolower(...)
    |> htmlspecialchars_decode(...)
    // |> fn($s) => preg_replace('/\s+/', ' ', $s)  // Ist der Regex das Problem?
    |> trim(...)
    |> fn($s) => mb_substr($s, 0, 100);

Die Fallstricke (weil es immer Fallstricke gibt)

Permalink to "Die Fallstricke (weil es immer Fallstricke gibt)"

Einschränkung 1: Nur Ein-Parameter-Funktionen

Permalink to "Einschränkung 1: Nur Ein-Parameter-Funktionen"

Jedes Callable in der Pipe muss genau einen required Parameter akzeptieren. Das hier schlägt fehl:

$result = $value |> str_replace('foo', 'bar', ...);  // Error!

Du musst es wrappen:

$result = $value |> fn($s) => str_replace('foo', 'bar', $s);

Einschränkung 2: Arrow Functions brauchen Klammern

Permalink to "Einschränkung 2: Arrow Functions brauchen Klammern"

Das ist subtil, aber wichtig:

// Falsch
$result = $value |> fn($x) => $x * 2;

// Richtig
$result = $value |> (fn($x) => $x * 2)(...);

Kommt in PHP 8.6: Function Composition

Permalink to "Kommt in PHP 8.6: Function Composition"

Der coolere Geschwisterteil des Pipe Operators steht schon auf der Roadmap. Function Composition wird es dir erlauben, neue Funktionen aus der Pipeline zu erstellen:

// PHP 8.6 (geplant)
$normalizer = strtolower(...) |> trim(...) |> ucfirst(...);
$result1 = $normalizer('  HELLO  ');
$result2 = $normalizer('  WORLD  ');

Das ist keine Science Fiction. Das ist nächstes Jahr.

Die URI Extension: URL-Parsing, das nicht lügt

Permalink to "Die URI Extension: URL-Parsing, das nicht lügt"

Eine dreißig Jahre alte Peinlichkeit

Permalink to "Eine dreißig Jahre alte Peinlichkeit"

Hand aufs Herz: PHPs parse_url() Funktion ist ein Security-Albtraum verpackt in Bequemlichkeit.

Probier mal dieses Experiment. Öffne dein Terminal und führe aus:

var_dump(parse_url('https://user:pass@example.com:80/path'));

Sieht gut aus, oder? Jetzt probier das:

var_dump(parse_url('http://foo@evil.com@example.com/'));

Was hast du bekommen? Hast du erwartet, dass host evil.com oder example.com ist? Glückwunsch, du hast gerade entdeckt, warum parse_url() nicht für sicherheitskritischen Code verwendet werden sollte.

Das Problem? PHPs parse_url() folgt keinem Standard. Nicht RFC 3986. Nicht WHATWG. Es ist ein Homebrew-Parser, der gebildete Vermutungen anstellt, und manchmal liegen diese Vermutungen falsch.

Die neue standardkonforme Lösung

Permalink to "Die neue standardkonforme Lösung"

PHP 8.5 führt eine komplette URI Extension mit zwei verschiedenen Implementierungen ein:

RFC 3986 (traditionelle Webstandards):

use Uri\Rfc3986\Uri;

$uri = Uri::fromString('https://user:pass@example.com:443/path?query=value#fragment');

echo $uri->getScheme();    // "https"
echo $uri->getAuthority(); // "user:pass@example.com:443"
echo $uri->getHost();      // "example.com"
echo $uri->getPath();      // "/path"
echo $uri->getQuery();     // "query=value"
echo $uri->getFragment();  // "fragment"

WHATWG (moderner Browser-Standard):

use Uri\WhatWg\Url;

$url = Url::fromString('https://example.com/path/../other');

echo $url->getPathname();  // "/other" (automatisch normalisiert!)
echo $url->getOrigin();    // "https://example.com"

Warum das wichtig ist: Security und Konsistenz

Permalink to "Warum das wichtig ist: Security und Konsistenz"

Die neue URI Extension nutzt kampferprobte Bibliotheken:

  • uriparser für RFC 3986 Compliance
  • Lexbor für WHATWG Compliance

Diese Bibliotheken treiben Browser und Server an. Sie wurden gefuzzt, auditiert und von Security-Forschern auf Herz und Nieren getestet. Sie raten nicht; sie folgen Spezifikationen.

Hier ist der Security-Gewinn:

// Alter Weg: mehrdeutiges Parsing
$parts = parse_url('http://foo@evil.com@example.com/');
// Was ist der Host? Hängt von der PHP-Version und Laune ab.

// Neuer Weg: eindeutig
$uri = Uri\Rfc3986\Uri::fromString('http://foo@evil.com@example.com/');
// Wirft eine Exception oder parst gemäß Spec. Kein Raten.

Die Entwicklungsgeschichte

Permalink to "Die Entwicklungsgeschichte"

Das war kein schnelles Feature. Die PHP Foundation hat fast ein Jahr daran gearbeitet, mit über 150 E-Mails auf PHP Internals, die das Design diskutiert haben. Sollte es prozedural oder objektorientiert sein? Welchem Standard folgen? Wie mit Edge Cases umgehen?

Das Ergebnis? Beide Standards, gebündelt mit PHP. Keine externen Packages. Keine Dependency Hell. Einfach PHP 8.5 installieren und du bist bereit, URLs korrekt zu parsen.

Clone With: Readonly Properties funktionieren endlich

Permalink to "Clone With: Readonly Properties funktionieren endlich"

Das Readonly-Clone-Problem

Permalink to "Das Readonly-Clone-Problem"

Erinnerst du dich, als PHP 8.1 readonly Properties eingeführt hat? Wir haben alle gefeiert. Immutability! Type Safety! Keine versehentlichen Mutationen mehr!

Dann haben wir versucht, sie tatsächlich zu nutzen:

readonly class User {
    public function __construct(
        public string $name,
        public string $email,
        public int $loginCount = 0,
    ) {}
}

$user = new User('Alice', 'alice@example.com');

// Wie inkrementiere ich loginCount?
// Kann readonly Property nicht direkt ändern...
$newUser = new User(
    $user->name,
    $user->email,
    $user->loginCount + 1
);

Das ist… verbose. Und es wird schlimmer mit 10 Properties. Bibliotheken wie symfony/serializer haben “Wither”-Patterns implementiert:

$newUser = $user
    ->withLoginCount($user->loginCount + 1);

Aber du musstest diese Methoden manuell schreiben. Für jede Property. Für jede Klasse.

Der Fix: Clone-With-Syntax

Permalink to "Der Fix: Clone-With-Syntax"

PHP 8.5 führt clone with ein:

readonly class User {
    public function __construct(
        public string $name,
        public string $email,
        public int $loginCount = 0,
    ) {}
}

$user = new User('Alice', 'alice@example.com');
$updated = clone($user, ['loginCount' => $user->loginCount + 1]);

Eine Zeile. Sauber. Offensichtlich. Und es respektiert alle deine Property Hooks und Type Checks:

readonly class User {
    public function __construct(
        public string $name,
        public string $email,
        public int $loginCount = 0,
    ) {}

    public string $normalizedName {
        get => strtolower($this->name);
        set($value) => strtolower(trim($value));
    }
}

// Property Hooks werden beim Clone noch ausgeführt
$updated = clone($user, [
    'normalizedName' => '  BOB  '  // Automatisch getrimmt und kleingeschrieben
]);

Production-Tipp: Validierung gilt weiterhin

Permalink to "Production-Tipp: Validierung gilt weiterhin"

Geh nicht davon aus, dass clone with deine Business Logic umgeht. Type Checks, Property Hooks und Constructor Logic gelten alle noch:

readonly class User {
    public function __construct(
        public string $email,
    ) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email');
        }
    }
}

$user = new User('valid@example.com');

// Das wirft InvalidArgumentException
$invalid = clone($user, ['email' => 'not-an-email']);

Deine Datenintegrität bleibt intakt.

#[\NoDiscard]: Silent Failures abfangen

Permalink to "#[\NoDiscard]: Silent Failures abfangen"

Das Partial-Success-Problem

Permalink to "Das Partial-Success-Problem"

Hier ist ein Szenario, das mich öfter erwischt hat, als mir lieb ist:

function writeToCache(string $key, mixed $value): bool {
    $success = $this->redis->set($key, $value);

    if (!$success) {
        $this->logger->warning("Cache write failed for key: {$key}");
    }

    return $success;
}

// Später im Code...
writeToCache('user:123', $userData);  // Hat das funktioniert? Who knows!

Die Funktion gibt einen Boolean zurück, der Erfolg oder Misserfolg anzeigt. Aber wenn du den Return Value ignorierst, wirst du nie wissen, dass dein Cache-Write fehlgeschlagen ist. Die Warning wird geloggt, aber deine Anwendung läuft fröhlich weiter.

Exceptions helfen hier nicht, weil das nicht exceptional ist – Cache-Failures passieren. Sie sind zu erwarten. Aber sie einfach zu ignorieren? Das ist ein Bug, der in Production auftauchen wird.

Da kommt #[\NoDiscard] ins Spiel

Permalink to "Da kommt #[\NoDiscard] ins Spiel"

PHP 8.5 leiht sich von C++s [[nodiscard]] und Rusts #[must_use]:

#[\NoDiscard]
function writeToCache(string $key, mixed $value): bool {
    $success = $this->redis->set($key, $value);

    if (!$success) {
        $this->logger->warning("Cache write failed for key: {$key}");
    }

    return $success;
}

// Das löst jetzt eine Warning aus
writeToCache('user:123', $userData);

// Warning: Return value of writeToCache() should not be discarded

// Das ist okay
if (!writeToCache('user:123', $userData)) {
    // Handle cache failure
}

Wann du es verwenden solltest

Permalink to "Wann du es verwenden solltest"

Markiere Funktionen mit #[\NoDiscard], wenn:

  • Return Values partiellen Erfolg/Misserfolg anzeigen
  • Das Ignorieren des Return Values zu silent Bugs führt
  • Die Operation nicht wichtig genug für Exceptions ist, aber zu wichtig zum Ignorieren

Verwende es nicht für:

  • Void Functions (offensichtlich)
  • Funktionen, wo Return Values wirklich optional sind
  • Getter-Methoden

Production-Beispiel: Database Transactions

Permalink to "Production-Beispiel: Database Transactions"
class DatabaseTransaction {
    #[\NoDiscard]
    public function commit(): bool {
        try {
            $this->pdo->commit();
            return true;
        } catch (PDOException $e) {
            $this->logger->error('Commit failed', ['error' => $e->getMessage()]);
            return false;
        }
    }
}

// Schlecht: schlägt still fehl
$transaction->commit();

// Gut: explizit behandelt
if (!$transaction->commit()) {
    throw new RuntimeException('Failed to commit transaction');
}

Vorbereitung auf PHP 9.0: Die Deprecations

Permalink to "Vorbereitung auf PHP 9.0: Die Deprecations"

Raus mit dem Alten (wirklich Alten)

Permalink to "Raus mit dem Alten (wirklich Alten)"

Einige dieser Deprecations sind überfällig. Wie, zwei-Jahrzehnte-überfällig.

Alternative Semikolon-Syntax für Switch/Case

Permalink to "Alternative Semikolon-Syntax für Switch/Case"

Diese Syntax stammt aus PHP/FI 2.0 (1997):

// Deprecated in PHP 8.5
switch ($value):
    case 1:
        echo "One";
        break;
    case 2:
        echo "Two";
        break;
endswitch;

Wie viele von euch haben das überhaupt jemals in Production gesehen? Jemand? Genau.

Das disable_classes INI Setting

Permalink to "Das disable_classes INI Setting"
; Deprecated - mach das nicht
disable_classes = PDO,mysqli

Das sollte User davon abhalten, bestimmte Klassen zu instanziieren. In der Praxis hat es Autoloading kaputtgemacht, Static Analyzer verwirrt und null Security-Vorteile gebracht.

Konstanten-Neudeklaration

Permalink to "Konstanten-Neudeklaration"
define('FOO', 'bar');
define('FOO', 'baz');  // Deprecated: redefining constant

Ja, das war erlaubt. Nein, du hättest es nie tun sollen.

Was es nicht ins Release geschafft hat

Permalink to "Was es nicht ins Release geschafft hat"

Mehrere RFCs wurden vorgeschlagen, aber abgelehnt:

  • Readonly hooks: Hooks für readonly Properties (zu komplex)
  • Nested classes: Inner Classes wie in Java (nicht genug Use Cases)
  • never Parameters: Type Hint für Parameter, der nie verwendet werden darf (zu nischig)
  • Optional interfaces: Interfaces, die existieren können oder auch nicht (verwirrend)

PHPs Evolution ist bedacht. Features brauchen starke Begründung, klare Use Cases und minimale Gotchas.

Die kleineren Gewinne, die sich summieren

Permalink to "Die kleineren Gewinne, die sich summieren"

array_first() und array_last()

Permalink to "array_first() und array_last()"

Endlich brauchen wir kein reset() und end() mehr:

// Alter Weg
$first = reset($array);
$last = end($array);

// Neuer Weg
$first = array_first($array);
$last = array_last($array);

Beide geben null für leere Arrays zurück statt false. Ein ===-Check weniger zum Erinnern.

Fatal Error Backtraces standardmäßig

Permalink to "Fatal Error Backtraces standardmäßig"
; Jetzt standardmäßig aktiviert
fatal_error_backtraces = 1

Wenn PHP um 3 Uhr nachts crasht, weißt du jetzt tatsächlich warum. Das hätte schon vor Jahren Standard sein sollen.

OPcache immer dabei

Permalink to "OPcache immer dabei"

OPcache ist nicht mehr optional. Es ist in jedem PHP 8.5 Build kompiliert. Weil mal ehrlich, wer betreibt PHP ohne OPcache in 2025?

CHIPS/Partitioned Cookies

Permalink to "CHIPS/Partitioned Cookies"

Privacy-erhaltende Cookies für Chrome und Safari:

setcookie('session', $value, [
    'partitioned' => true,  // Verhindert Cross-Site-Tracking
    'secure' => true,
    'httponly' => true,
    'samesite' => 'None',
]);

Die Privacy deiner User wurde gerade standardmäßig besser.

Die Timeline: Support und Upgrades

Permalink to "Die Timeline: Support und Upgrades"
  • Released: 20. November 2025
  • Active Support: Bis 31. Dezember 2027 (2 Jahre aktive Entwicklung)
  • Security Updates: Bis 31. Dezember 2029 (4 Jahre insgesamt)

Wenn du noch auf PHP 7.4 bist (End of Life: 28. November 2022), bist du fast drei Jahre ohne Support. Plane deine Upgrades. Allein der Pipe Operator ist es wert.

Was das für deinen Code bedeutet

Permalink to "Was das für deinen Code bedeutet"

Kurzfristig: Fang an, Pipes zu verwenden

Permalink to "Kurzfristig: Fang an, Pipes zu verwenden"

Der Pipe Operator wird transformieren, wie du Datenverarbeitungs-Code schreibst. Fang klein an:

// Ersetze das
$result = ucfirst(trim($input));

// Mit dem
$result = $input |> trim(...) |> ucfirst(...);

Dann erweitere es zu komplexeren Pipelines. Dein Code wird es dir danken.

Mittelfristig: Nutze URI Parsing

Permalink to "Mittelfristig: Nutze URI Parsing"

Wenn du URL-Validierung, Switching oder Security-Checks machst, migriere zur neuen URI Extension:

// Sicherheitskritisches URL-Parsing
try {
    $uri = Uri\Rfc3986\Uri::fromString($userInput);

    // Garantiert nach RFC 3986
    if ($uri->getHost() === 'trusted-domain.com') {
        // Sicher weiterzumachen
    }
} catch (Exception $e) {
    // Ungültige URI - ablehnen
}

Langfristig: Kümmere dich um Deprecations

Permalink to "Langfristig: Kümmere dich um Deprecations"

Lass deine Test Suite mit hochgedrehtem Error Reporting laufen:

error_reporting(E_ALL | E_DEPRECATED);

Fixe Deprecation Warnings jetzt, bevor sie in PHP 9.0 zu Errors werden.

Das große Ganze

Permalink to "Das große Ganze"

PHP 8.5 geht nicht nur um Features. Es geht um Philosophie.

Der Pipe Operator zeigt, dass PHP bereit ist, von funktionalen Sprachen zu lernen, ohne selbst eine zu werden. Die URI Extension demonstriert ein Commitment zu Standards und Security über Rückwärtskompatibilität. Das #[\NoDiscard] Attribut beweist, dass PHP sich darum kümmert, Bugs zu verhindern, nicht nur sie zu fixen.

Und die abgelehnten RFCs? Sie zeigen Zurückhaltung. Nicht jede Idee schafft es ins Release. Features müssen sich ihren Platz verdienen.

Das ist eine reife Sprache, die sich selbstbewusst weiterentwickelt.

Probier es selbst

Permalink to "Probier es selbst"

PHP 8.5 ist jetzt verfügbar. Installiere es, spiel mit dem Pipe Operator, zerstöre deine URL-Parsing-Annahmen und fang an, dich auf PHP 9.0 vorzubereiten.

Und wenn du deine erste Daten-Pipeline schreibst, die sich von links nach rechts wie echte menschliche Sprache liest? Teile sie. Schreib darüber. Zeige der Welt, dass PHP nicht mehr die Sprache von 2005 ist – es ist die Sprache, die es 2035 sein wird.

Was ist dein erster Use Case für den Pipe Operator? Ich würde gerne hören, was du damit baust.


Ressourcen: