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.
Der Pipe Operator: Endlich Links-nach-Rechts-Logik
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:
- In Kleinbuchstaben umwandeln
- Whitespace trimmen
- 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)
neverParameters: 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: