Pure Functions in PHP: Referenzielle Transparenz als Fundament sauberen Codes
AI generated
<?php
8.4
PHP · Funktionale Programmierung · Softwarearchitektur
Pure Functions in PHP
Referenzielle Transparenz als Fundament sauberen Codes

Eine Pure Function liefert bei gleichen Argumenten immer dasselbe Ergebnis und verändert nichts außerhalb ihres eigenen Rückgabewerts. Dieses Prinzip der referenziellen Transparenz macht PHP-Code vorhersagbar, leicht testbar und parallelisierbar, ganz ohne zusätzliches Framework oder neue Sprachsyntax.

17 Min. Lesezeit Pure Functions · Seiteneffekte · Functional Core PHP 8.2 · 8.3 · 8.4

1. Was Pure Functions und referenzielle Transparenz bedeuten

Eine Pure Function erfüllt zwei Bedingungen gleichzeitig: sie liefert bei denselben Argumenten immer dasselbe Ergebnis, und sie verändert außerhalb ihres eigenen Rückgabewerts nichts, keine Datenbank, keine globale Variable, keine Eigenschaft eines übergebenen Objekts. Diese zweite Bedingung wird als Freiheit von Seiteneffekten bezeichnet und ist der eigentliche Kern jeder Pure Function. Die Konsequenz aus beiden Bedingungen zusammen nennt man referenzielle Transparenz: ein Aufruf einer Pure Function kann jederzeit durch sein Ergebnis ersetzt werden, ohne das Verhalten des restlichen Programms zu ändern.

In PHP ist dieses Konzept keine neue Spracherweiterung, sondern eine Disziplin beim Schreiben von Funktionen und Methoden. Jede gewöhnliche PHP-Funktion kann pur oder unpur sein, je nachdem, ob sie auf externen Zustand zugreift oder ihn verändert. Der Unterschied zwischen einer Funktion, die einen Steuerbetrag berechnet, und einer Funktion, die zusätzlich eine Log-Zeile schreibt, ist genau dieser: die erste ist eine Pure Function mit referenzieller Transparenz, die zweite nicht mehr, obwohl beide dieselbe Kernberechnung enthalten.

Der praktische Nutzen zeigt sich überall dort, wo Code korrekt, testbar und nebenläufig sicher sein muss: Preisberechnungen, Validierungsregeln, Datentransformationen und Formatierungslogik lassen sich fast immer als Pure Functions schreiben. Dieser Artikel zeigt, wie man solche Funktionen erkennt, schreibt und von unvermeidbaren Seiteneffekten sauber trennt, mit lauffähigem PHP-8.4-Code in jedem Abschnitt.

2. Merkmale einer Pure Function im Detail

Das erste Merkmal, Determinismus, bedeutet, dass eine Pure Function niemals von etwas außerhalb ihrer Parameter abhängen darf. Eine Funktion, die intern date('Y'), rand() oder eine globale Konstante abfragt, ist nicht mehr deterministisch, weil ihr Ergebnis vom Aufrufzeitpunkt oder Zufall abhängt statt ausschließlich von den übergebenen Argumenten. Das zweite Merkmal, Seiteneffektfreiheit, verbietet jede Veränderung außerhalb der Funktion: kein Schreiben in eine Datei, keine Datenbankabfrage, keine Mutation eines als Referenz übergebenen Arrays, keine Änderung einer statischen Eigenschaft.

Ein Detail, das häufig übersehen wird: auch das Werfen einer Exception kann als Seiteneffekt gelten, wenn sie den normalen Kontrollfluss durch eine Ausnahme statt durch einen regulären Rückgabewert modelliert. Streng funktionale Sprachen kodieren Fehlerfälle deshalb über Rückgabetypen wie Result oder Either statt über Exceptions. In PHP ist dieser Purismus selten praktikabel, aber das Bewusstsein dafür hilft, zwischen erwartbaren Validierungsfehlern, die als Wert zurückgegeben werden sollten, und wirklich außergewöhnlichen Zuständen zu unterscheiden, für die eine Exception angemessen bleibt.


<?php

declare(strict_types=1);

// IMPURE: depends on external state (current time) and has no
// deterministic output for the same $birthYear across calls
function calculateAgeImpure(int $birthYear): int
{
    return (int) date('Y') - $birthYear;
}

// PURE: same inputs always produce the same output, no hidden dependency
function calculateAge(int $birthYear, int $currentYear): int
{
    return $currentYear - $birthYear;
}

echo calculateAge(1990, 2026); // always 36, regardless of when it runs

// IMPURE: mutates the array passed in, a hidden side effect for the caller
function addDiscountImpure(array &$prices, float $percent): void
{
    foreach ($prices as $key => $price) {
        $prices[$key] = $price * (1 - $percent / 100);
    }
}

// PURE: returns a new array, leaves the original untouched
function withDiscount(array $prices, float $percent): array
{
    return array_map(
        fn (float $price): float => $price * (1 - $percent / 100),
        $prices,
    );
}

3. Referenzielle Transparenz konkret: Ausdruck durch Ergebnis ersetzen

Referenzielle Transparenz lässt sich mit einem einfachen Gedankenexperiment prüfen: kann ein Funktionsaufruf im Quelltext durch sein berechnetes Ergebnis ersetzt werden, ohne dass sich irgendetwas am Verhalten des Programms ändert? Bei einer Pure Function wie calculateAge(1990, 2026) ist die Antwort immer ja, der Ausdruck kann direkt durch 36 ersetzt werden. Bei einer Funktion, die zusätzlich eine Log-Zeile schreibt oder einen Zähler erhöht, verschwindet dieser Effekt beim Ersetzen durch den reinen Wert, das Substitutionsmodell bricht.

Dieses Substitutionsprinzip ist mehr als eine akademische Übung, es ist die Grundlage für sicheres Refactoring. Wenn ein Compiler oder ein Entwickler weiß, dass ein Ausdruck referenziell transparent ist, kann er ihn verschieben, einmal statt mehrfach auswerten, oder das Ergebnis zwischenspeichern, ohne das Programmverhalten zu riskieren. Genau diese Eigenschaft nutzen spätere Abschnitte über Memoization und Caching aus: nur bei referenzieller Transparenz ist ein Cache-Treffer garantiert korrekt, weil identische Eingaben zwangsläufig identische Ausgaben bedeuten.


<?php

declare(strict_types=1);

final class Money
{
    public function __construct(
        public readonly int $cents,
        public readonly string $currency,
    ) {
    }

    // PURE: referentially transparent, can be replaced by its result
    public function add(Money $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException('Currency mismatch');
        }

        return new self($this->cents + $other->cents, $this->currency);
    }
}

$a = new Money(1000, 'EUR');
$b = new Money(250, 'EUR');

// This expression...
$total = $a->add($b);

// ...can always be substituted by its concrete result
$totalAgain = new Money(1250, 'EUR');

var_dump($total->cents === $totalAgain->cents); // true, every single time

4. Seiteneffekte erkennen und isolieren

Vollständig reiner Code ist in einer realen Anwendung unmöglich, weil jede Anwendung irgendwann mit einer Datenbank, dem Dateisystem, der Uhrzeit oder externen Diensten sprechen muss. Der praktikable Weg ist nicht, Seiteneffekte zu eliminieren, sondern sie an die Ränder der Architektur zu verschieben. Dieses Muster heißt Functional Core, Imperative Shell: die Geschäftslogik, die Entscheidungen trifft und Werte berechnet, bleibt als Pure Function im Kern, während eine dünne äußere Schicht die eigentlichen Seiteneffekte wie Datenbankzugriffe oder HTTP-Aufrufe ausführt.

Praktisch bedeutet das: eine Funktion berechnet, ob ein Rabatt gewährt wird und wie hoch er ist, rein basierend auf übergebenen Werten, ohne selbst eine Datenbank abzufragen. Der Aufrufer, die Imperative Shell, lädt die benötigten Daten vorher, übergibt sie an die Pure Function und persistiert das Ergebnis danach. Diese Trennung macht den Kern der Geschäftslogik ohne Datenbank, ohne Mocks und ohne Netzwerk testbar, während die dünne äußere Schicht meist so einfach bleibt, dass sie kaum eigene Fehler produziert.


<?php

declare(strict_types=1);

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

// PURE core: all business logic, no I/O, no database, fully testable
function calculateOrderTotal(array $lines, float $loyaltyDiscount): float
{
    $subtotal = array_reduce(
        $lines,
        fn (float $carry, OrderLine $line): float => $carry + $line->quantity * $line->unitPrice,
        0.0,
    );

    return round($subtotal * (1 - $loyaltyDiscount), 2);
}

// IMPURE shell: the only place touching the database and the clock
final class OrderService
{
    public function __construct(private readonly OrderRepository $repository)
    {
    }

    public function checkout(int $orderId): float
    {
        $lines = $this->repository->findLines($orderId);          // side effect: DB read
        $discount = $this->repository->findLoyaltyDiscount($orderId); // side effect: DB read

        $total = calculateOrderTotal($lines, $discount);            // pure calculation

        $this->repository->saveTotal($orderId, $total);              // side effect: DB write

        return $total;
    }
}

5. Pure Functions in Klassen: Methoden ohne this-Mutation

Objektorientierter PHP-Code und Pure Functions schließen sich nicht aus. Eine Methode ist pur, wenn sie $this und ihre übergebenen Objekte niemals verändert, sondern stattdessen ein neues Objekt mit dem berechneten Ergebnis zurückgibt. readonly-Eigenschaften seit PHP 8.1 unterstützen dieses Muster direkt auf Sprachebene, weil eine einmal gesetzte Eigenschaft danach gar nicht mehr verändert werden kann, was viele klassische Seiteneffekte von vornherein ausschließt.

Der Unterschied zwischen einer mutierenden Setter-Methode und einer Pure Function-artigen Methode zeigt sich am deutlichsten bei Value Objects: statt $money->addCents(500), das den Zustand von $money verändert, gibt $money->add($other) ein neues Objekt zurück und lässt das Original unangetastet. Dieses Muster verhindert eine ganze Klasse von Bugs, bei denen ein Objekt an mehreren Stellen im Code gehalten wird und eine Änderung an einer Stelle unerwartet an einer anderen sichtbar wird, weil beide Stellen dieselbe Referenz teilen.


<?php

declare(strict_types=1);

final class ShoppingCart
{
    /**
     * @param list<OrderLine> $lines
     */
    public function __construct(private readonly array $lines = [])
    {
    }

    // PURE: returns a new cart, never mutates $this->lines
    public function withLine(OrderLine $line): self
    {
        return new self([...$this->lines, $line]);
    }

    // PURE: computed purely from immutable state, no hidden dependency
    public function total(): float
    {
        return array_reduce(
            $this->lines,
            fn (float $carry, OrderLine $line): float => $carry + $line->quantity * $line->unitPrice,
            0.0,
        );
    }
}

$cart = new ShoppingCart();
$updatedCart = $cart->withLine(new OrderLine('SKU-1', 2, 19.90));

// The original cart is provably unchanged — no mutation happened anywhere
var_dump($cart->total() === 0.0);        // true
var_dump($updatedCart->total() === 39.8); // true

6. Testbarkeit: warum Pure Functions ohne Mocks auskommen

Der größte praktische Vorteil von Pure Functions zeigt sich beim Testen. Eine Funktion ohne Seiteneffekte braucht kein Mock-Objekt, keinen In-Memory-Datenbank-Ersatz und keine Zeitmanipulation, um vorhersagbar getestet zu werden. Der Test ruft die Funktion mit konkreten Eingabewerten auf und prüft den Rückgabewert, fertig. Kein Setup einer Datenbankverbindung, kein Aufräumen nach dem Test, keine Reihenfolgeabhängigkeit zwischen Testfällen, weil keine Funktion einen versteckten Zustand hinterlässt, den ein anderer Test sehen könnte.

Dieser Effekt verstärkt sich mit wachsender Testsuite: unpure Funktionen mit Datenbankzugriff oder Dateisystemzugriff führen oft zu langsamen, manchmal instabilen Tests, die von externer Infrastruktur abhängen. Pure Functions lassen sich dagegen in Millisekunden ausführen, tausendfach parallel, ohne gegenseitige Beeinflussung. Wer den Functional-Core-Ansatz konsequent verfolgt, verschiebt den Großteil der Testfälle auf diese schnellen, robusten Unit-Tests und braucht nur wenige Integrationstests für die dünne, unpure Außenschicht.


<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class CalculateOrderTotalTest extends TestCase
{
    // No mocks, no database, no setUp() needed — pure input, pure output
    public function testAppliesLoyaltyDiscountCorrectly(): void
    {
        $lines = [
            new OrderLine('SKU-1', 2, 10.0),
            new OrderLine('SKU-2', 1, 5.0),
        ];

        $total = calculateOrderTotal($lines, 0.1);

        self::assertSame(22.5, $total); // (20 + 5) * 0.9
    }

    public function testEmptyCartReturnsZero(): void
    {
        self::assertSame(0.0, calculateOrderTotal([], 0.2));
    }

    // Property-style test: the function is deterministic across many calls
    public function testIsDeterministicAcrossRepeatedCalls(): void
    {
        $lines = [new OrderLine('SKU-1', 3, 7.5)];

        $first = calculateOrderTotal($lines, 0.05);
        $second = calculateOrderTotal($lines, 0.05);

        self::assertSame($first, $second);
    }
}

7. Pure Functions und Performance: Vorhersagbarkeit und Caching

Weil eine Pure Function bei identischen Argumenten garantiert dasselbe Ergebnis liefert, lässt sich ihr Ergebnis gefahrlos zwischenspeichern. Dieses Prinzip ist die Grundlage von Memoization, aber auch die theoretische Rechtfertigung für jede Form von Ergebnis-Caching in PHP: nur wenn eine Berechnung referenziell transparent ist, kann ein Cache-Treffer garantiert korrekt sein, ohne veraltete oder falsche Ergebnisse zu riskieren. Bei einer unpure Funktion, die von externem Zustand abhängt, wäre ein Cache dagegen ein Bug in Wartestellung.

Ein zweiter, weniger offensichtlicher Vorteil betrifft Nebenläufigkeit: Pure Functions können ohne Synchronisation parallel ausgeführt werden, weil sie keinen gemeinsamen veränderlichen Zustand teilen. In PHP mit Fibers oder externen Prozessen wie Swoole-Workern ist das relevant, weil parallele Aufrufe derselben Pure Function sich niemals gegenseitig durch Race Conditions beeinflussen können. Der JIT-Compiler in PHP 8.4 profitiert außerdem von vorhersagbaren, seiteneffektfreien Funktionen, weil deren Kontrollfluss leichter zu optimieren ist als der einer Funktion mit unklaren externen Abhängigkeiten.

8. Grenzen: wann PHP-Code niemals vollständig pur sein kann

Ein Web-Request-Handler in PHP kann per Definition niemals vollständig pur sein, weil sein einziger Zweck darin besteht, einen Seiteneffekt zu erzeugen, eine HTTP-Antwort zu senden. Ebenso müssen Konstruktoren, die eine Datenbankverbindung öffnen, oder Repository-Methoden, die Datensätze lesen und schreiben, per Natur unpur bleiben. Der Anspruch ist deshalb nicht, jede einzelne Funktion einer Anwendung zu einer Pure Function zu machen, sondern den Anteil an purem Code so groß wie möglich zu halten und die verbleibenden Seiteneffekte bewusst an klar erkennbaren Stellen zu bündeln.

Ein zweiter praktischer Grenzfall betrifft Logging und Metriken innerhalb ansonsten reiner Funktionen. Streng genommen ist jeder error_log()-Aufruf ein Seiteneffekt. In der Praxis toleriert man solche minimalen, für den Rückgabewert irrelevanten Effekte oft pragmatisch, sollte sie aber konsequent dokumentieren, damit klar bleibt, welche Funktionen tatsächlich vollständig referenziell transparent sind und welche nur nahezu pur, mit einem bewusst akzeptierten Nebeneffekt für Observability.

9. Pure Functions im direkten Vergleich

Nicht jeder Code-Bereich profitiert im selben Maß davon, konsequent als Pure Function geschrieben zu werden. Die folgende Tabelle zeigt typische Aufgaben in PHP-Anwendungen und ordnet sie danach ein, wie gut sie sich für referenziell transparenten Code eignen.

Aufgabe Typischer Ansatz Pure-Function-Eignung Empfehlung
Preis- und Rabattberechnung Methode mit Objektmutation Sehr hoch Als Pure Function schreiben
Validierungsregeln Werfende Methode mit Exception Hoch Ergebnis als Wert statt Exception zurückgeben
Datenbankzugriff Repository-Methode Niemals rein möglich In dünner Imperative Shell isolieren
Zeitstempel-abhängige Logik date() intern aufgerufen Niedrig ohne Anpassung Zeitpunkt als Parameter übergeben
HTTP-Response senden Controller-Aktion Nie rein Ganz am Rand der Architektur belassen

Die Grundregel: je näher eine Funktion an der eigentlichen Fachlogik liegt, desto größer der Nutzen daraus, sie als Pure Function zu schreiben. Je näher eine Funktion an Infrastruktur wie Datenbank, Dateisystem oder Netzwerk liegt, desto mehr lohnt es sich, ihre Seiteneffekte bewusst zu akzeptieren und klar zu kennzeichnen, statt sie künstlich zu verschleiern.

Mironsoft

PHP-Architektur, Code-Reviews und funktionale Muster im Team-Alltag

Geschäftslogik, die sich ohne Mocks testen lässt?

Wir prüfen bestehenden PHP-Code auf versteckte Seiteneffekte und zeigen, wie sich Fachlogik als Pure Functions isolieren lässt, sauber getrennt von Datenbank, Zeit und Netzwerk.

Code-Review

Analyse auf versteckte Seiteneffekte und fehlende referenzielle Transparenz

Refactoring

Functional Core, Imperative Shell in bestehenden Modulen einführen

Schulung

Pure Functions und Testbarkeit praxisnah im Team vermitteln

10. Zusammenfassung

Pure Functions liefern bei gleichen Argumenten immer dasselbe Ergebnis und verändern nichts außerhalb ihres Rückgabewerts. Diese beiden Eigenschaften zusammen ergeben referenzielle Transparenz: ein Funktionsaufruf kann jederzeit durch sein Ergebnis ersetzt werden, ohne das Programmverhalten zu ändern. In PHP lässt sich dieses Prinzip durch das Muster Functional Core, Imperative Shell konsequent umsetzen, indem Geschäftslogik als Pure Functions geschrieben wird, während eine dünne äußere Schicht Datenbank, Dateisystem und Netzwerk anspricht.

Der Gewinn zeigt sich in drei Bereichen: Tests brauchen keine Mocks mehr und laufen in Millisekunden, Refactoring wird sicherer, weil Ausdrücke substituierbar bleiben, und Caching von Ergebnissen ist garantiert korrekt, weil identische Eingaben zwangsläufig identische Ausgaben liefern. Vollständige Reinheit ist in einer realen Anwendung unmöglich, aber jede zusätzliche Pure Function im Kern der Fachlogik reduziert die Fläche für unerwartete Bugs spürbar.

Pure Functions in PHP — Das Wichtigste auf einen Blick

Definition

Gleiche Argumente liefern immer dasselbe Ergebnis, keine Veränderung außerhalb des Rückgabewerts.

Referenzielle Transparenz

Ein Aufruf kann jederzeit durch sein Ergebnis ersetzt werden, Grundlage für sicheres Refactoring und Caching.

Architektur-Muster

Functional Core, Imperative Shell trennt reine Fachlogik von unvermeidbaren Seiteneffekten.

Testbarkeit

Keine Mocks, keine Datenbank, keine Reihenfolgeabhängigkeit zwischen Testfällen nötig.

11. FAQ: Pure Functions in PHP

1Was ist eine Pure Function?
Liefert bei gleichen Argumenten immer dasselbe Ergebnis und verändert außerhalb des Rückgabewerts nichts, keine Datenbank, kein globaler Zustand.
2Was bedeutet referenzielle Transparenz?
Ein Aufruf kann durch sein Ergebnis ersetzt werden, ohne das Programmverhalten zu ändern. Gilt nur bei purem Code.
3Kann eine Anwendung komplett pur sein?
Nein, Seiteneffekte wie DB-Zugriffe oder HTTP-Responses sind unvermeidbar. Ziel: puren Anteil maximieren, Effekte bündeln.
4Ist eine werfende Methode noch pur?
Streng genommen nicht, sie verändert den Kontrollfluss. Ein Result-Typ als Rückgabewert ist oft die reinere Alternative.
5Was ist Functional Core, Imperative Shell?
Geschäftslogik als Pure Functions im Kern, dünne äußere Schicht übernimmt Datenbank, Dateisystem und Netzwerk.
6Warum leichter testbar?
Kein Mock, keine DB-Verbindung, keine Zeitmanipulation nötig. Konkrete Werte rein, Rückgabewert prüfen, fertig.
7Warum sind readonly-Eigenschaften relevant?
Verhindern Mutation auf Sprachebene, dadurch geben Methoden eher neue Objekte zurück statt Zustand zu ändern.
8Darf eine Pure Function loggen?
Streng genommen ist Logging ein Seiteneffekt. Minimale, irrelevante Log-Aufrufe werden oft pragmatisch toleriert und dokumentiert.
9Vorteil bei Caching?
Identische Eingaben liefern garantiert identische Ausgaben, ein Cache-Treffer ist deshalb immer korrekt.
10Relevant für Nebenläufigkeit?
Ja, kein geteilter veränderlicher Zustand bedeutet parallele Ausführung ohne Race Conditions, etwa mit Fibers oder Swoole.