Higher Order Functions in PHP: Funktionen als Werte einsetzen
AI generated
<?php
8.4
PHP · Funktionale Programmierung · Closures
Higher Order Functions in PHP
Funktionen als Werte richtig einsetzen

Higher Order Functions behandeln Funktionen wie jeden anderen Wert: sie werden übergeben, zurückgegeben und zur Laufzeit zusammengesetzt. In PHP 8.4 lassen sich damit Schleifen, Bedingungen und Boilerplate durch klar benannte, wiederverwendbare Bausteine ersetzen, ohne ein zusätzliches Framework zu benötigen.

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

1. Was Higher Order Functions in PHP wirklich bedeuten

Eine Higher Order Function ist eine Funktion, die mindestens eines von zwei Dingen tut: sie nimmt eine andere Funktion als Argument entgegen, oder sie gibt eine Funktion als Rückgabewert zurück. In PHP ist das seit Langem möglich, weil Funktionen als callable, als Closure-Objekt oder seit PHP 8.1 auch als First-Class-Callable-Referenz übergeben werden können. Der Unterschied zu einer gewöhnlichen Funktion liegt also nicht in der Syntax, sondern in der Rolle: Verhalten wird zum Parameter, statt fest im Funktionsrumpf verdrahtet zu sein.

Praktisch bedeutet das: Statt für jede Variante einer Berechnung eine eigene Funktion zu schreiben, schreibt man eine einzige Higher Order Function, die das variable Verhalten als Parameter entgegennimmt. Eine Validierungspipeline, ein Preis-Kalkulator mit austauschbaren Rabattregeln oder eine Middleware-Kette in einem eigenen Micro-Framework sind typische Stellen, an denen Higher Order Functions Code kürzer und gleichzeitig testbarer machen, weil jede einzelne Funktion isoliert geprüft werden kann.

Der Rest dieses Artikels zeigt, wie Higher Order Functions in PHP 8.4 konkret aussehen: von den eingebauten Array-Funktionen über selbst geschriebene Funktionsfabriken bis zur Typisierung von Callables mit PHPStan. Jeder Abschnitt enthält lauffähigen Code, der sich direkt in ein Projekt übernehmen lässt.

2. Funktionen als Werte: callable, Closures, First-Class Callables

PHP kennt drei Wege, eine Funktion als Wert zu behandeln. Der älteste ist der callable-Typehint zusammen mit Strings oder Arrays: 'strtoupper' für eine globale Funktion, [$objekt, 'methode'] für eine Instanzmethode. Diese Form funktioniert, ist aber weder von der IDE noch von PHPStan vollständig auf Existenz und Signatur prüfbar, weil der String erst zur Laufzeit aufgelöst wird. Für Higher Order Functions, die verlässlich in CI-Pipelines geprüft werden sollen, ist das eine Schwachstelle.

Die zweite Form ist die anonyme Funktion, also eine Closure, entweder als function() {} oder als kompaktere Arrow-Function fn() =>. Arrow-Functions erfassen automatisch alle Variablen aus dem umgebenden Scope per Wert, was viele use()-Klauseln überflüssig macht. Die dritte, seit PHP 8.1 verfügbare Form ist die First-Class-Callable-Syntax: strtoupper(...) oder $objekt->methode(...) erzeugt direkt ein typisiertes Closure-Objekt, ohne den Namen als String zu schreiben. Diese Form ist die von PHPStan und der IDE am besten unterstützte und sollte für neue Higher Order Functions bevorzugt werden.


<?php

declare(strict_types=1);

// Three ways to reference the same function as a value

// 1. String callable — no static check possible
$upper1 = 'strtoupper';
echo $upper1('hello');

// 2. Closure with explicit body
$upper2 = function (string $s): string {
    return strtoupper($s);
};
echo $upper2('hello');

// 3. First-class callable syntax (PHP 8.1+) — statically checkable
$upper3 = strtoupper(...);
echo $upper3('hello');

final class PriceFormatter
{
    public function format(float $amount): string
    {
        return number_format($amount, 2, ',', '.') . ' EUR';
    }
}

$formatter = new PriceFormatter();

// First-class callable on an instance method
$formatFn = $formatter->format(...);
echo $formatFn(19.9); // 19,90 EUR

3. array_map, array_filter und array_reduce im Detail

Die drei eingebauten Higher Order Functions von PHP, array_map, array_filter und array_reduce, decken die überwiegende Mehrheit alltäglicher Array-Transformationen ab. array_map wendet eine Funktion auf jedes Element an und behält die Array-Struktur bei. array_filter behält nur Elemente, für die die übergebene Funktion true zurückgibt, wobei ein häufig übersehener Punkt ist, dass ohne zweiten Parameter der Wert geprüft wird, mit ARRAY_FILTER_USE_KEY oder ARRAY_FILTER_USE_BOTH aber auch der Schlüssel zur Verfügung steht.

array_reduce ist die vielseitigste dieser drei Higher Order Functions, weil sie ein Array auf einen einzigen Wert beliebigen Typs reduziert, egal ob Summe, Objekt oder ein neu aufgebautes Array. Der dritte Parameter, der Startwert, ist bei leeren Arrays entscheidend: fehlt er, liefert array_reduce bei einem leeren Array null zurück, was zu schwer auffindbaren Bugs führt, wenn der Aufrufer einen anderen Typ erwartet.


<?php

declare(strict_types=1);

final class Order
{
    public function __construct(
        public readonly string $sku,
        public readonly int $quantity,
        public readonly float $unitPrice,
    ) {
    }
}

$orders = [
    new Order('SKU-1', 2, 19.90),
    new Order('SKU-2', 1, 49.00),
    new Order('SKU-3', 5, 4.50),
];

// array_map: transform each order into its line total
$lineTotals = array_map(
    fn (Order $o): float => $o->quantity * $o->unitPrice,
    $orders,
);

// array_filter: keep only orders above a threshold
$expensiveOrders = array_filter(
    $orders,
    fn (Order $o): bool => $o->unitPrice > 10.0,
);

// array_reduce: fold all line totals into a single sum
$grandTotal = array_reduce(
    $lineTotals,
    fn (float $carry, float $total): float => $carry + $total,
    0.0, // explicit initial value avoids null on empty arrays
);

echo number_format($grandTotal, 2); // 82.40

4. Eigene Higher Order Functions schreiben

Eingebaute Funktionen decken einfache Fälle ab, aber der eigentliche Wert von Higher Order Functions zeigt sich erst, wenn eine Funktion selbst eine neue, spezialisierte Funktion zurückgibt. Ein typisches Beispiel ist eine Funktionsfabrik für Validierungsregeln: statt für jede Regel eine eigene benannte Funktion zu schreiben, erzeugt eine generische Higher Order Function passende Closures zur Laufzeit, parametrisiert über die konkreten Grenzwerte.

Dieses Muster reduziert Duplikation erheblich, weil die Grundlogik der Prüfung an einer einzigen Stelle steht, während die eigentlichen Regeln nur noch aus einem Aufruf mit unterschiedlichen Parametern bestehen. Wichtig ist, den Rückgabetyp der erzeugenden Funktion explizit als Closure zu deklarieren, damit PHPStan und die IDE die Signatur der erzeugten Higher Order Function kennen, statt nur callable als generischen Typ zu sehen.


<?php

declare(strict_types=1);

/**
 * Factory: returns a validation function bound to the given range.
 *
 * @return Closure(int): bool
 */
function withinRange(int $min, int $max): Closure
{
    return function (int $value) use ($min, $max): bool {
        return $value >= $min && $value <= $max;
    };
}

$isValidAge = withinRange(0, 120);
$isValidQuantity = withinRange(1, 999);

var_dump($isValidAge(35));      // true
var_dump($isValidQuantity(0));  // false

/**
 * Higher order function returning a memoized version of a callable.
 *
 * @return Closure(int): int
 */
function memoize(Closure $fn): Closure
{
    $cache = [];

    return function (int $arg) use ($fn, &$cache): int {
        if (!array_key_exists($arg, $cache)) {
            $cache[$arg] = $fn($arg);
        }

        return $cache[$arg];
    };
}

$slowSquare = function (int $n): int {
    usleep(50_000); // simulate expensive work
    return $n * $n;
};

$fastSquare = memoize($slowSquare);
echo $fastSquare(12); // computed once
echo $fastSquare(12); // returned from cache instantly

5. Decorator-Komposition: Verhalten um Funktionen legen

Ein weiterer starker Anwendungsfall für Higher Order Functions ist die Dekoration bestehender Funktionen mit zusätzlichem Verhalten, ohne deren Code anzufassen. Ein Logging-Wrapper, ein Retry-Mechanismus oder eine Zeitmessung lassen sich als Higher Order Functions schreiben, die eine beliebige Funktion entgegennehmen und eine neue Closure zurückgeben, die vor und nach dem Aufruf zusätzliche Aktionen ausführt.

Dieses Muster ist eng mit Middleware-Ketten verwandt, wie man sie aus HTTP-Frameworks kennt, funktioniert aber genauso gut für einzelne Domänenfunktionen. Der entscheidende Vorteil gegenüber Vererbung: mehrere Dekorationen lassen sich beliebig kombinieren, indem eine Higher Order Function das Ergebnis einer anderen als Eingabe erhält, ohne dass eine Klassenhierarchie angepasst werden muss.


<?php

declare(strict_types=1);

/**
 * Wraps a callable with retry logic on exception.
 *
 * @return Closure(int): string
 */
function withRetry(Closure $fn, int $maxAttempts = 3): Closure
{
    return function (int $arg) use ($fn, $maxAttempts): string {
        $lastException = null;

        for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
            try {
                return $fn($arg);
            } catch (RuntimeException $e) {
                $lastException = $e;
            }
        }

        throw $lastException;
    };
}

/**
 * Wraps a callable with basic timing output.
 *
 * @return Closure(int): string
 */
function withTiming(Closure $fn): Closure
{
    return function (int $arg) use ($fn): string {
        $start = microtime(true);
        $result = $fn($arg);
        $elapsedMs = (microtime(true) - $start) * 1000;
        echo sprintf("call took %.2fms\n", $elapsedMs);

        return $result;
    };
}

function fetchRemotePrice(int $productId): string
{
    if (random_int(0, 4) === 0) {
        throw new RuntimeException('Timeout while fetching price');
    }

    return sprintf('price-for-%d', $productId);
}

// Composing two higher order functions around one base function
$resilientFetch = withTiming(withRetry(fetchRemotePrice(...)));
echo $resilientFetch(42);

6. Closures und Variablenbindung: use(), Referenzen, Fallen

Die use()-Klausel entscheidet, ob eine Closure den Wert einer äußeren Variable zum Zeitpunkt der Definition kopiert, oder ob sie über eine Referenz an denselben Speicherplatz gebunden bleibt. Standardmäßig erfasst use($var) den Wert einmalig per Kopie: ändert sich $var danach im äußeren Scope, sieht die Higher Order Function davon nichts. Erst use(&$var) koppelt die Closure an dieselbe Speicherzelle, sodass spätere Änderungen sichtbar werden.

Diese Feinheit ist eine häufige Fehlerquelle bei Schleifen, die mehrere Closures erzeugen: wird die Schleifenvariable per Referenz erfasst, teilen sich alle erzeugten Higher Order Functions denselben letzten Wert, statt jeweils ihren eigenen Schleifendurchlauf-Wert zu behalten. Arrow-Functions umgehen dieses Problem für einfache Lesezugriffe automatisch, weil sie implizit per Wert erfassen, unterstützen aber keine Erfassung per Referenz, was sie für Zähler oder Akkumulatoren ungeeignet macht.


<?php

declare(strict_types=1);

// Common bug: capturing the loop variable by reference
$byReferenceClosures = [];
foreach ([1, 2, 3] as $number) {
    $byReferenceClosures[] = function () use (&$number): int {
        return $number; // all closures share the same $number
    };
}

foreach ($byReferenceClosures as $closure) {
    echo $closure(); // prints 3, 3, 3 — not 1, 2, 3
}

// Correct fix: capture by value inside the loop body
$byValueClosures = [];
foreach ([1, 2, 3] as $number) {
    $byValueClosures[] = function () use ($number): int {
        return $number; // each closure keeps its own copy
    };
}

foreach ($byValueClosures as $closure) {
    echo $closure(); // prints 1, 2, 3 as expected
}

// Arrow functions capture by value automatically — same safe result
$arrowClosures = array_map(
    fn (int $number): Closure => fn (): int => $number,
    [1, 2, 3],
);

7. Performance-Aspekte von Closures gegenüber Schleifen

Der Overhead einer Higher Order Function gegenüber einer manuell geschriebenen foreach-Schleife ist in der Praxis meist vernachlässigbar, aber nicht null. Jeder Aufruf einer Closure durchläuft einen zusätzlichen Funktionsaufruf-Mechanismus, und array_map mit mehreren Arrays erzeugt intern zusätzliche Iterationslogik gegenüber einer einzigen, handgeschriebenen Schleife. Bei wenigen tausend Elementen ist dieser Unterschied im Millisekundenbereich und in den seltensten Fällen der tatsächliche Engpass einer Anwendung.

Kritisch wird es erst bei sehr großen Datenmengen in heißen Codepfaden, etwa beim Verarbeiten von Millionen Datensätzen in einem Batch-Import. Dort lohnt sich ein Benchmark mit echten Produktionsdaten, bevor eine Higher Order Function zugunsten einer klassischen Schleife verworfen wird. Der JIT-Compiler in PHP 8.4 optimiert einfache Closure-Aufrufe zunehmend gut, sodass vorzeitige Optimierung häufig mehr Lesbarkeit kostet, als sie an Laufzeit einspart.


<?php

declare(strict_types=1);

$numbers = range(1, 1_000_000);

// Variant A: higher order function
$start = microtime(true);
$squaredHof = array_map(fn (int $n): int => $n * $n, $numbers);
$hofMs = (microtime(true) - $start) * 1000;

// Variant B: manual loop with pre-sized array
$start = microtime(true);
$squaredLoop = [];
foreach ($numbers as $n) {
    $squaredLoop[] = $n * $n;
}
$loopMs = (microtime(true) - $start) * 1000;

printf("array_map: %.2fms, foreach: %.2fms\n", $hofMs, $loopMs);
// Typical result: both within the same order of magnitude on PHP 8.4

8. Callables typisieren: PHPStan, Templates und Static Analysis

Ohne präzise Typisierung degeneriert eine Higher Order Function für statische Analyse zu einem Black-Box-callable, dessen Parameter und Rückgabewert PHPStan nicht prüfen kann. Die PHPDoc-Notation Closure(int, string): bool beschreibt exakt Parametertypen und Rückgabetyp einer Closure und lässt PHPStan ab Level 5 aufwärts falsche Aufrufe erkennen, etwa wenn eine Higher Order Function versehentlich mit vertauschten Argumenttypen aufgerufen wird.

Für generische Higher Order Functions, die mit beliebigen Eingabetypen arbeiten sollen, bietet PHPStan Template-Typen über @template T an. Eine generische pipe-Funktion kann so deklariert werden, dass der Eingabetyp der ersten Funktion mit dem erwarteten Eingabetyp des Aufrufers übereinstimmen muss, ohne dass zur Laufzeit ein einziger zusätzlicher Typecheck nötig wäre. Diese Templates sind reine Entwicklungszeit-Metadaten und haben keinerlei Laufzeit-Overhead.


<?php

declare(strict_types=1);

/**
 * Generic higher order function with template types for static analysis.
 *
 * @template TInput
 * @template TOutput
 * @param Closure(TInput): TOutput $fn
 * @param list<TInput> $items
 * @return list<TOutput>
 */
function mapTyped(Closure $fn, array $items): array
{
    return array_map($fn, $items);
}

/** @var Closure(int): string $intToString */
$intToString = fn (int $n): string => (string) $n;

// PHPStan infers list<string> here — a type mismatch would be flagged
$strings = mapTyped($intToString, [1, 2, 3]);

/**
 * @param Closure(int, int): bool $comparator
 */
function sortWith(array &$items, Closure $comparator): void
{
    usort($items, $comparator);
}

$values = [5, 1, 4, 2, 3];
sortWith($values, fn (int $a, int $b): int => $a <=> $b);

9. Higher Order Functions im direkten Vergleich

Nicht jede Stelle im Code profitiert gleichermaßen von Higher Order Functions. Die folgende Tabelle stellt gängige Alternativen gegenüber und zeigt, wann die funktionale Variante klar im Vorteil ist und wann eine klassische Schleife oder Klasse vorzuziehen bleibt.

Aufgabe Klassischer Ansatz Higher Order Function Empfehlung
Array transformieren foreach mit manuellem Push array_map(fn (...) => ..., $arr) HOF, klarer Intent
Wiederholtes Verhalten dekorieren Vererbung, abstrakte Basisklasse Closure-Wrapper (withRetry) HOF, keine Klassenhierarchie nötig
Sehr großer Datensatz, Millionen Zeilen foreach mit Generator array_map (materialisiert Array) Schleife/Generator, weniger Speicher
Komplexe Zustandsmaschine Klasse mit Methoden Verschachtelte Closures Klasse, bessere Lesbarkeit bei vielen Zuständen
Validierungsregeln parametrisieren Viele einzelne benannte Funktionen Funktionsfabrik (withinRange) HOF, weniger Duplikation

Die Grundregel: Higher Order Functions lohnen sich überall dort, wo Verhalten variiert, aber die umgebende Struktur gleich bleibt. Sobald mehrere zusammengehörige Zustände und Übergänge verwaltet werden müssen, ist eine Klasse mit klar benannten Methoden oft lesbarer als eine Kette verschachtelter Closures.

Mironsoft

PHP-Architektur, Code-Reviews und moderne Sprachfeatures im Team-Alltag

PHP-Code, der Verhalten klar von Struktur trennt?

Wir prüfen bestehenden PHP-Code auf unnötige Duplikation und zeigen, wo Higher Order Functions, saubere Closures und typisierte Callables Boilerplate reduzieren, ohne die Lesbarkeit zu opfern.

Code-Review

PHPStan-Analyse und manuelle Prüfung auf Callable-Typisierung und Closure-Fallen

Refactoring

Duplizierte Schleifen und Bedingungen durch klare Higher Order Functions ersetzen

Schulung

Funktionale Muster in PHP praxisnah im Team einführen und dokumentieren

10. Zusammenfassung

Higher Order Functions in PHP behandeln Funktionen als Werte: sie werden übergeben, zurückgegeben und zur Laufzeit kombiniert. Die eingebauten Funktionen array_map, array_filter und array_reduce decken die häufigsten Array-Transformationen ab, während eigene Funktionsfabriken wie withinRange oder Decorator-Wrapper wie withRetry variables Verhalten ohne Klassenhierarchie kapseln. Die First-Class-Callable-Syntax seit PHP 8.1 macht diese Muster statisch prüfbar, ohne auf fehleranfällige String-Callables zurückzugreifen.

Entscheidend für sauberen Code ist die bewusste Wahl zwischen Kopie und Referenz bei use(), eine präzise PHPDoc-Typisierung mit Closure(...) für PHPStan und ein realistischer Blick auf Performance: bei den meisten Anwendungen ist der Overhead einer Higher Order Function gegenüber einer manuellen Schleife irrelevant. Wer diese Punkte beachtet, bekommt mit Higher Order Functions kürzeren, besser testbaren und klar strukturierten PHP-Code.

Higher Order Functions in PHP — Das Wichtigste auf einen Blick

Definition

Eine Funktion, die andere Funktionen als Parameter annimmt oder als Rückgabewert liefert, ohne Verhalten fest zu verdrahten.

Eingebaute Bausteine

array_map, array_filter, array_reduce decken die häufigsten Array-Transformationen ab.

Typisierung

First-Class-Callable-Syntax plus PHPDoc Closure(int): bool für vollständige PHPStan-Prüfung.

Fallstricke

use(&$var) in Schleifen teilt sich einen Speicherplatz. Bei Bedarf immer per Wert erfassen.

11. FAQ: Higher Order Functions in PHP

1Was ist eine Higher Order Function genau?
Eine Funktion, die eine Funktion als Parameter nimmt oder eine Funktion zurückgibt, statt Verhalten fest zu verdrahten.
2Sind array_map/filter/reduce HOFs?
Ja, alle drei nehmen eine Funktion entgegen und sind die am häufigsten genutzten eingebauten Higher Order Functions in PHP.
3callable-String vs. First-Class-Callable?
Der String wird erst zur Laufzeit aufgelöst. strtoupper(...) erzeugt sofort ein typisiertes, statisch prüfbares Closure-Objekt.
4use(&$var) vs. use($var)?
Referenz nur, wenn spätere Änderungen sichtbar sein sollen. In Schleifen führt das oft zu geteiltem letzten Wert statt individueller Kopien.
5Sind HOFs langsamer als Schleifen?
Meist vernachlässigbar. Erst bei Millionen Elementen in heißen Pfaden lohnt sich ein echter Benchmark vor dem Verzicht.
6Wie typisiere ich Closure-Parameter?
Mit PHPDoc wie @param Closure(int, string): bool $fn. PHPStan ab Level 5 prüft Aufrufe gegen diese Signatur.
7Was ist eine Funktionsfabrik?
Eine HOF, die selbst eine Closure zurückgibt, parametrisiert über eigene Argumente, zum Beispiel withinRange(0, 120).
8Kann ich mehrere HOFs kombinieren?
Ja, Decorator-Wrapper lassen sich ineinander verschachteln, etwa withTiming(withRetry(fn(...))), ohne Klassenhierarchie.
9array_reduce bei leerem Array?
Ohne Startwert liefert array_reduce dann null. Ein expliziter dritter Parameter vermeidet unerwartete Typfehler zuverlässig.
10Wann sind HOFs die falsche Wahl?
Bei komplexen Zustandsmaschinen mit vielen Zuständen ist eine Klasse mit benannten Methoden meist lesbarer.