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.

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

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

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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Raus mit dem Alten (wirklich Alten)

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

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

; 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

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

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

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

; 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

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

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

  • 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

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

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

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

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

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: