Funktionale Komposition in PHP: Funktionen mit compose() kombinieren
AI generated
<?php
8.4
PHP · Funktionale Programmierung · Closures
Funktionale Komposition in PHP
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.

18 Min. Lesezeit compose · Closures · Function Composition PHP 8.2 · 8.3 · 8.4

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

11. FAQ: Funktionale Komposition in PHP

1Was ist funktionale Komposition?
Das Zusammensetzen zweier Funktionen zu einer neuen Funktion, ausgedrückt als compose(f, g)(x) = f(g(x)).
2Gibt es eine eingebaute compose-Funktion?
Nein, PHP hat keine native compose-Funktion, sie lässt sich aber mit wenigen Zeilen selbst schreiben.
3In welcher Reihenfolge läuft compose()?
Rechts nach links, wie f ∘ g. compose(f, g)(x) führt zuerst g aus und übergibt das Ergebnis an f.
4Mehr als zwei Funktionen komponieren?
Mit einer variadischen Version, die per array_reduce beliebig viele Closures zu einer einzigen zusammenfaltet.
5Wie macht man es typsicher?
Mit PHPStan @template-Typen, die sicherstellen, dass Ausgabe- und Eingabetyp der komponierten Funktionen zusammenpassen.
6Was ist Point-Free-Stil?
Eine Funktion wird nur durch Komposition bestehender Funktionen definiert, ohne eigene benannte Parameter.
7Unterschied zu Pipe-Simulation?
Komposition liest rechts nach links und bindet fest in eine neue Funktion, Pipe-Simulation schickt Werte links nach rechts.
8Unterschied zum Decorator-Pattern?
Decorator führt Code vor und nach dem Aufruf aus, Komposition verkettet Ausgabe direkt zu Eingabe der nächsten Funktion.
9Validierungsregeln kombinieren?
Mit einer allOf()-Funktion, die mehrere Prädikate entgegennimmt und nur bei allen erfüllten Regeln true zurückgibt.
10Wann wird es unübersichtlich?
Ab vier bis fünf verschachtelten Ebenen ohne erklärende Zwischenwerte, dann ist Pipe-Simulation meist lesbarer.