Funktionen mit compose() kombinieren statt verschachteln
Funktionale Komposition setzt kleine, einzeln getestete Funktionen zu einer neuen Funktion zusammen, statt Aufrufe tief ineinander zu verschachteln. Mit einer eigenen compose()-Funktion lässt sich dieses Muster in PHP 8.4 typsicher und ohne zusätzliche Abhängigkeit umsetzen.
Inhaltsverzeichnis
- 1. Was funktionale Komposition in PHP bedeutet
- 2. Das Verschachtelungsproblem: warum f(g(h(x))) schlecht skaliert
- 3. Eine eigene compose()-Funktion bauen
- 4. Komposition mit beliebig vielen Funktionen: variadische Parameter
- 5. Typsicherheit mit PHPStan-Templates absichern
- 6. Point-Free-Stil: Funktionen ohne benannte Argumente definieren
- 7. Praxisbeispiel: Validierungsketten aus kleinen Prädikaten
- 8. Abgrenzung zu Pipe-Operator-Simulation und Decorator-Pattern
- 9. Komposition im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was funktionale Komposition in PHP bedeutet
Funktionale Komposition bezeichnet das Zusammensetzen zweier oder mehrerer Funktionen zu einer neuen Funktion, bei dem das Ergebnis der einen Funktion automatisch zum Eingabewert der nächsten wird. Mathematisch ausgedrückt: aus f und g entsteht eine neue Funktion h mit h(x) = f(g(x)). In PHP lässt sich dieses Konzept nicht über eine eingebaute Sprachsyntax abbilden, wohl aber über eine selbst geschriebene compose-Funktion, die Closures als Werte entgegennimmt und eine neue Closure zurückgibt.
Der praktische Reiz von funktionaler Komposition liegt darin, kleine, einzeln benannte und einzeln testbare Funktionen zu größeren Verarbeitungsschritten zusammenzusetzen, ohne dass jede Kombination als eigene, separat geschriebene Funktion existieren muss. Eine Funktion, die einen String normalisiert, trimmt und in Kleinbuchstaben umwandelt, kann aus drei einzelnen Bausteinen komponiert werden, statt als eigene, monolithische Funktion geschrieben zu werden, die alle drei Schritte in einem Funktionsrumpf vermischt.
Dieser Artikel zeigt, wie eine eigene compose-Funktion in PHP 8.4 aussieht, wie sie sich mit PHPStan-Templates typsicher machen lässt und wo die Grenze zur Pipe-Operator-Simulation verläuft, die ein verwandtes, aber unterschiedliches Muster darstellt.
2. Das Verschachtelungsproblem: warum f(g(h(x))) schlecht skaliert
Ohne funktionale Komposition werden mehrere aufeinanderfolgende Transformationen häufig als verschachtelte Funktionsaufrufe geschrieben: trim(strtolower(str_replace(' ', '-', $input))). Dieser Code ist korrekt, aber von innen nach außen zu lesen, was der tatsächlichen Ausführungsreihenfolge widerspricht und mit jeder weiteren Transformation schwerer nachvollziehbar wird. Bei fünf oder sechs verschachtelten Aufrufen verliert man schnell den Überblick, welcher Aufruf zuerst und welcher zuletzt ausgeführt wird.
Ein zweites Problem der Verschachtelung ist die fehlende Wiederverwendbarkeit der Zwischenschritte als benannte Einheit. Wird dieselbe Kombination aus drei Transformationen an mehreren Stellen im Code gebraucht, muss entweder der komplette verschachtelte Ausdruck kopiert werden, oder es entsteht eine zusätzliche Hilfsfunktion, die aber wieder von Hand die Verschachtelung nachbildet. Funktionale Komposition löst genau dieses Problem, indem die Kombination selbst zu einem benannten, wiederverwendbaren Wert wird.
<?php
declare(strict_types=1);
// Nested calls: correct, but read inside-out and hard to extend
function slugifyNested(string $input): string
{
return trim(strtolower(str_replace(' ', '-', $input)));
}
echo slugifyNested(' Mein Blog Beitrag '); // mein-blog-beitrag
// The same logic, but each step is a separately testable named function
function replaceSpaces(string $s): string
{
return str_replace(' ', '-', $s);
}
function toLower(string $s): string
{
return strtolower($s);
}
function trimWhitespace(string $s): string
{
return trim($s);
}
3. Eine eigene compose()-Funktion bauen
Eine minimale compose-Funktion nimmt zwei Closures entgegen und gibt eine neue Closure zurück, die zuerst die zweite und dann die erste Funktion mit deren Ergebnis aufruft. Diese Reihenfolge, von rechts nach links, entspricht der mathematischen Notation f ∘ g und ist die klassische Konvention in funktionalen Sprachen. Wichtig ist, den Rückgabetyp explizit als Closure zu deklarieren, damit die komponierte Funktion selbst wieder als Baustein für weitere funktionale Komposition verwendet werden kann.
Der entscheidende Vorteil gegenüber der verschachtelten Schreibweise: die komponierte Funktion existiert jetzt als eigener, benannter Wert, der einer Variable zugewiesen, weitergegeben oder erneut mit anderen Funktionen kombiniert werden kann. Der Aufrufer muss die innere Struktur der Komposition nicht mehr kennen, sondern behandelt compose($f, $g) wie jede andere Funktion mit klar definierter Signatur.
<?php
declare(strict_types=1);
/**
* Compose two functions right to left: compose(f, g)(x) === f(g(x))
*
* @return Closure(string): string
*/
function compose(Closure $f, Closure $g): Closure
{
return function (string $x) use ($f, $g): string {
return $f($g($x));
};
}
$replaceSpaces = fn (string $s): string => str_replace(' ', '-', $s);
$toLower = fn (string $s): string => strtolower($s);
$trimWhitespace = fn (string $s): string => trim($s);
// Composition reads right to left: trim, then lowercase, then replace
$normalizeStep1 = compose($toLower, $replaceSpaces);
$slugify = compose($trimWhitespace, $normalizeStep1);
echo $slugify(' Mein Blog Beitrag '); // mein-blog-beitrag
// The composed function is itself a reusable, named value
$anotherSlug = $slugify(' Zweiter Artikel Titel ');
echo $anotherSlug; // zweiter-artikel-titel
4. Komposition mit beliebig vielen Funktionen: variadische Parameter
Eine compose-Funktion, die nur zwei Argumente akzeptiert, wird bei mehr als zwei Transformationsschritten schnell unhandlich, weil jeder zusätzliche Schritt eine weitere verschachtelte compose-Anweisung erfordert. Die praktische Lösung ist eine variadische Version, die eine beliebige Anzahl von Funktionen entgegennimmt und sie intern mit array_reduce zu einer einzigen Closure zusammenfaltet. Diese Version behält die rechts-nach-links-Reihenfolge bei, verarbeitet aber jede Anzahl von Funktionen ohne manuelle Verschachtelung.
Ein wichtiges Detail bei der variadischen Implementierung: die Reihenfolge der Faltung entscheidet, ob die erste oder die letzte übergebene Funktion zuerst ausgeführt wird. array_reduce von links beginnend über die Argumentliste erzeugt eine Komposition, bei der die letzte Funktion in der Parameterliste zuerst auf den Eingabewert angewendet wird, was der klassischen mathematischen Konvention f ∘ g ∘ h entspricht.
<?php
declare(strict_types=1);
/**
* Compose any number of functions right to left.
*
* @param Closure(mixed): mixed ...$fns
* @return Closure(mixed): mixed
*/
function composeAll(Closure ...$fns): Closure
{
return array_reduce(
$fns,
function (?Closure $carry, Closure $fn): Closure {
if ($carry === null) {
return $fn;
}
return fn (mixed $x): mixed => $carry($fn($x));
},
null,
);
}
$double = fn (int $n): int => $n * 2;
$addTen = fn (int $n): int => $n + 10;
$square = fn (int $n): int => $n ** 2;
// Executes right to left: square first, then addTen, then double
$pipeline = composeAll($double, $addTen, $square);
echo $pipeline(3); // double(addTen(square(3))) = double(addTen(9)) = double(19) = 38
5. Typsicherheit mit PHPStan-Templates absichern
Eine generische compose-Funktion, die mit mixed arbeitet, verliert für PHPStan jede Information über die tatsächlichen Ein- und Ausgabetypen der komponierten Funktion. Mit PHPStan-Template-Typen über @template lässt sich das beheben: die Signatur beschreibt, dass der Ausgabetyp der inneren Funktion mit dem Eingabetyp der äußeren Funktion übereinstimmen muss, und PHPStan meldet einen Fehler, sobald zwei nicht kompatible Funktionen komponiert werden.
Diese Typisierung kostet zur Laufzeit nichts, weil PHPDoc-Templates reine Entwicklungszeit-Metadaten sind. Der Gewinn zeigt sich beim Refactoring: ändert sich die Signatur einer der beteiligten Funktionen, meldet PHPStan sofort jede Stelle, an der die funktionale Komposition jetzt nicht mehr typkompatibel ist, statt den Fehler erst zur Laufzeit als falschen Rückgabewert sichtbar zu machen.
<?php
declare(strict_types=1);
/**
* Type-safe composition with PHPStan templates.
*
* @template TA
* @template TB
* @template TC
* @param Closure(TB): TC $f
* @param Closure(TA): TB $g
* @return Closure(TA): TC
*/
function composeTyped(Closure $f, Closure $g): Closure
{
return fn (mixed $x): mixed => $f($g($x));
}
/** @var Closure(string): int $stringLength */
$stringLength = strlen(...);
/** @var Closure(int): bool $isEven */
$isEven = fn (int $n): bool => $n % 2 === 0;
// PHPStan infers Closure(string): bool for the composed function
$hasEvenLength = composeTyped($isEven, $stringLength);
var_dump($hasEvenLength('test')); // true, "test" has 4 characters
6. Point-Free-Stil: Funktionen ohne benannte Argumente definieren
Point-Free-Stil bezeichnet das Schreiben von Funktionen, ohne ihre Argumente explizit zu benennen, indem die Funktion allein durch Komposition bestehender Funktionen definiert wird. Statt fn (string $s): string => trim(strtolower($s)) zu schreiben, wobei $s nur als Durchlaufvariable dient, definiert man die Funktion direkt als compose($trimWhitespace, $toLower), ganz ohne einen einzigen benannten Parameter im eigenen Code.
Der Vorteil dieses Stils liegt in der Kompaktheit und darin, dass die Funktion als reine Beschreibung ihrer Bestandteile lesbar bleibt: was passiert, wird durch die Namen der komponierten Bausteine ausgedrückt, nicht durch eine explizite Parametervariable. Der Nachteil zeigt sich bei komplexeren Transformationen: Point-Free-Code kann schnell schwer lesbar werden, wenn zu viele Ebenen von Komposition ineinander verschachtelt sind, ohne dass ein Zwischenschritt einen erklärenden Namen bekommt. In PHP-Teams ist ein moderater Einsatz von Point-Free-Stil meist die pragmatischste Wahl.
7. Praxisbeispiel: Validierungsketten aus kleinen Prädikaten
Ein konkretes Einsatzgebiet für funktionale Komposition sind Validierungsregeln, bei denen mehrere unabhängige Prüfungen zu einer Gesamtregel kombiniert werden sollen. Statt eine einzelne, lange Validierungsfunktion mit vielen if-Verzweigungen zu schreiben, definiert man kleine Prädikate, jedes für genau eine Regel zuständig, und komponiert sie zu einer zusammengesetzten Prüfung, die erst dann true liefert, wenn alle Einzelregeln erfüllt sind.
Dieses Muster unterscheidet sich von der reinen mathematischen Komposition f(g(x)), weil hier nicht das Ergebnis der einen Funktion zur Eingabe der nächsten wird, sondern mehrere Prädikate über denselben Eingabewert mit einem logischen Und verknüpft werden. Trotzdem ist es dieselbe Grundidee: kleine, einzeln benannte und getestete Bausteine werden zu einer größeren, wiederverwendbaren Einheit zusammengesetzt, ohne dass die Gesamtregel als monolithischer Codeblock existiert.
<?php
declare(strict_types=1);
/**
* Combine any number of predicates into a single predicate (logical AND).
*
* @param Closure(string): bool ...$predicates
* @return Closure(string): bool
*/
function allOf(Closure ...$predicates): Closure
{
return function (string $value) use ($predicates): bool {
foreach ($predicates as $predicate) {
if (!$predicate($value)) {
return false;
}
}
return true;
};
}
$hasMinLength = fn (string $s): bool => strlen($s) >= 8;
$hasDigit = fn (string $s): bool => (bool) preg_match('/\d/', $s);
$hasUppercase = fn (string $s): bool => (bool) preg_match('/[A-Z]/', $s);
$isStrongPassword = allOf($hasMinLength, $hasDigit, $hasUppercase);
var_dump($isStrongPassword('weak')); // false
var_dump($isStrongPassword('Str0ngPass')); // true
8. Abgrenzung zu Pipe-Operator-Simulation und Decorator-Pattern
Funktionale Komposition wird oft mit Pipe-Operator-Simulation verwechselt, weil beide Muster mehrere Funktionen aneinanderreihen. Der Unterschied liegt in der Leserichtung und dem Zeitpunkt der Bindung: compose(f, g) bindet die Reihenfolge fest in eine neue, wiederverwendbare Funktion und liest sich von rechts nach links, während eine Pipe-Simulation typischerweise einen konkreten Wert direkt von links nach rechts durch eine Kette von Funktionen schickt, ohne notwendigerweise eine neue, eigenständige Funktion zu erzeugen.
Auch zum Decorator-Pattern gibt es eine Abgrenzung: ein Decorator wie withRetry oder withTiming verändert das Verhalten einer Funktion, indem er zusätzlichen Code vor und nach dem eigentlichen Aufruf ausführt, während funktionale Komposition den Ausgabewert der einen Funktion direkt zur Eingabe der nächsten macht, ohne zusätzliches Verhalten um den Aufruf herum einzuführen. Beide Muster lassen sich kombinieren, sind aber konzeptionell unterschiedlich motiviert.
9. Komposition im direkten Vergleich
Je nach Situation ist Verschachtelung, funktionale Komposition oder eine Pipe-artige Struktur die bessere Wahl. Die folgende Tabelle ordnet typische Szenarien ein.
| Szenario | Verschachtelt | Funktionale Komposition | Empfehlung |
|---|---|---|---|
| Zwei bis drei Transformationsschritte | f(g(x)) |
compose(f, g) |
Beide vertretbar, Komposition benannter |
| Kombination mehrfach wiederverwendet | Code-Duplikation bei jedem Aufruf | Einmal komponierte, benannte Variable | Klar Komposition bevorzugen |
| Validierungsregeln kombinieren | Ein großer if-Block | allOf($p1, $p2, $p3) |
Komposition, klar erweiterbar |
| Sehr viele, sich oft ändernde Schritte | Unlesbar bei > 4 Ebenen | Point-Free schwer lesbar ohne Namen | Pipe-Simulation mit Zwischenwerten |
| Zusätzliches Verhalten um Aufruf | Nicht direkt abbildbar | Nicht das richtige Muster | Decorator-Pattern (withRetry) |
Die Grundregel: funktionale Komposition eignet sich besonders für kurze, klar benannte Ketten aus zwei bis vier Funktionen, die als eigenständiger, wiederverwendbarer Wert existieren sollen. Bei sehr langen Ketten oder häufig wechselnden Zwischenschritten ist eine Pipe-Simulation mit lesbaren Zwischenwerten oft übersichtlicher als tief verschachtelte Komposition.
Mironsoft
PHP-Architektur, Code-Reviews und funktionale Muster im Team-Alltag
Verschachtelter Code, der schwer lesbar geworden ist?
Wir prüfen bestehenden PHP-Code auf tief verschachtelte Aufrufe und zeigen, wo funktionale Komposition kleine, testbare Bausteine zu klaren, wiederverwendbaren Funktionen zusammensetzt.
Code-Review
Analyse auf tief verschachtelte Aufrufe und fehlende Wiederverwendbarkeit
Refactoring
Verschachtelte Transformationen durch typsichere Komposition ersetzen
Schulung
Funktionale Komposition praxisnah im Team einführen und dokumentieren
10. Zusammenfassung
Funktionale Komposition setzt kleine, einzeln benannte Funktionen zu einer neuen, wiederverwendbaren Funktion zusammen, statt sie tief ineinander zu verschachteln. Eine eigene compose()-Funktion, die zwei Closures entgegennimmt und eine neue Closure zurückgibt, lässt sich in PHP 8.4 ohne zusätzliche Abhängigkeit umsetzen und mit einer variadischen Version auf beliebig viele Funktionen erweitern. PHPStan-Templates machen diese Komposition typsicher, ohne zur Laufzeit einen einzigen zusätzlichen Check zu kosten.
Wichtig bleibt die Abgrenzung zu verwandten Mustern: Pipe-Operator-Simulation schickt einen konkreten Wert von links nach rechts durch eine Kette, während Decorator-Pattern zusätzliches Verhalten um einen Aufruf herum legen, statt Ausgabe zu Eingabe zu verketten. Wer diese Unterschiede kennt, wählt für jede Situation das passende funktionale Muster, statt ein einziges Muster auf alle Probleme anzuwenden.
Funktionale Komposition in PHP — Das Wichtigste auf einen Blick
Definition
compose(f, g)(x) === f(g(x)), zwei Funktionen ergeben eine neue, wiederverwendbare Funktion.
Variadische Version
array_reduce faltet beliebig viele Closures zu einer einzigen Komposition zusammen.
Typsicherheit
PHPStan-Templates prüfen, dass Ausgabetyp der inneren und Eingabetyp der äußeren Funktion übereinstimmen.
Abgrenzung
Unterscheidet sich von Pipe-Simulation (Leserichtung) und Decorator-Pattern (zusätzliches Verhalten).