Variable Funktionen und Variable Variablen: Low-Level-Metaprogrammierung in PHP
AI generated
<?php
8.4
PHP · Dynamischer Dispatch · Metaprogrammierung
Variable Funktionen und Variable Variablen
Low-Level-Metaprogrammierung in PHP

Lange bevor PHP Reflection oder Attribute kannte, gab es bereits die Möglichkeit, Funktionsnamen und Variablennamen selbst zur Laufzeit als Werte zu behandeln. Variable Funktionen und variable Variablen sind die älteste Form der Metaprogrammierung in PHP, mächtig und riskant zugleich, und bis heute die Grundlage vieler dynamischer Dispatcher.

16 Min. Lesezeit $fn() · $$name · call_user_func PHP 5.x bis 8.4

1. Was variable Funktionen und variable Variablen sind

Variable Funktionen und variable Variablen sind zwei der ältesten Metaprogrammierungsmechanismen in PHP, verfügbar seit den ersten Versionen der Sprache, lange bevor Reflection, Attribute oder Closures existierten. Eine variable Funktion liegt vor, wenn der Name der aufzurufenden Funktion selbst in einer Variablen steht und die Syntax $name() statt eines literalen Funktionsnamens verwendet wird. Eine variable Variable liegt vor, wenn der Name einer Variablen selbst aus einer anderen Variablen gebildet wird, mit der Syntax $$name.

Beide Mechanismen erlauben es, Programmstruktur, die normalerweise zur Entwicklungszeit feststeht, erst zur Laufzeit aus Daten abzuleiten. Ein Funktionsname, der aus einer Konfigurationsdatei oder einer Benutzereingabe stammt, kann direkt aufgerufen werden, ohne eine lange match-Kette zu schreiben. Diese Flexibilität war vor der Einführung moderner Alternativen wie Closures und Reflection oft die einzig praktikable Lösung für generische, datengetriebene Dispatcher.

Der Preis dieser Flexibilität ist Nachvollziehbarkeit: Weder ein menschlicher Leser noch ein Static-Analysis-Werkzeug kann allein aus dem Quelltext ableiten, welche konkrete Funktion oder Variable tatsächlich zur Laufzeit angesprochen wird, wenn der Name erst aus einer Variablen zusammengesetzt wird. Dieser Artikel zeigt beide Mechanismen im Detail, ihre legitimen Einsatzorte und die modernen Alternativen, die in den meisten Fällen vorzuziehen sind.

2. Variable Funktionen im Detail: $fn() und Callables aus Strings

Die Syntax für eine variable Funktion ist denkbar einfach: Steht in einer Variablen ein String, der dem Namen einer existierenden Funktion entspricht, ruft $variable() genau diese Funktion auf. Dasselbe Prinzip funktioniert auch mit Methodennamen in Kombination mit einem Array in der Form [$objekt, 'methodenname'], was PHP intern als gültigen Callable-Typ behandelt, unabhängig davon, ob er über call_user_func() oder direkt über $callable() aufgerufen wird.

Wichtig für den sicheren Einsatz variabler Funktionen ist die Prüfung mit is_callable() vor dem eigentlichen Aufruf, weil PHP bei einem nicht existierenden Funktionsnamen einen Fatal Error wirft, der sich nur schwer kontrolliert abfangen lässt. Bei Aufrufen, die aus externen, nicht vertrauenswürdigen Quellen wie Benutzereingaben stammen, ist zusätzlich eine Allowlist bekannter, erlaubter Funktionsnamen unverzichtbar, um beliebige Codeausführung zu verhindern.


<?php

declare(strict_types=1);

function formatAsEuro(float $amount): string
{
    return number_format($amount, 2, ',', '.') . ' EUR';
}

function formatAsPercentage(float $value): string
{
    return number_format($value * 100, 1) . ' %';
}

// The function name comes from configuration, not a literal in the code
$formatterName = 'formatAsEuro';

// Always validate against an allowlist before invoking a dynamic name
$allowedFormatters = ['formatAsEuro', 'formatAsPercentage'];

if (in_array($formatterName, $allowedFormatters, true) && is_callable($formatterName)) {
    echo $formatterName(1299.9) . PHP_EOL; // 1.299,90 EUR
}

3. Variable Variablen: $$name und ihre seltenen legitimen Zwecke

Eine variable Variable entsteht mit der Syntax $$name, wobei $name selbst eine Variable ist, deren Inhalt als Name einer weiteren Variable verwendet wird. Enthält $name beispielsweise den String 'total', greift $$name auf die Variable $total zu, unabhängig davon, ob diese vorher überhaupt existierte. Dieses Feature existiert seit den frühesten PHP-Versionen und wurde ursprünglich häufig genutzt, um aus HTTP-Formulardaten dynamisch Variablen im aktuellen Scope zu erzeugen.

Genau diese ursprüngliche Nutzung gilt heute als Antipattern und ist der Hauptgrund, warum register_globals längst aus PHP entfernt wurde: Unkontrollierte variable Variablen aus Benutzereingaben konnten beliebige, bereits existierende Variablen im Scope überschreiben, was zu gravierenden Sicherheitslücken führte. Legitime, seltene Einsatzorte für variable Variablen heute sind ausschließlich interne, kontrollierte Kontexte, etwa das programmatische Setzen mehrerer thematisch verwandter Instanzvariablen aus einer festen, internen Liste von Namen, niemals aus direkt vom Benutzer kommenden Daten.


<?php

declare(strict_types=1);

final class QuarterlyReport
{
    public float $q1 = 0.0;
    public float $q2 = 0.0;
    public float $q3 = 0.0;
    public float $q4 = 0.0;

    /** @param array<string, float> $values internal, trusted data only */
    public function fillFromInternalData(array $values): void
    {
        // Controlled, internal use only — never with raw user input
        foreach (['q1', 'q2', 'q3', 'q4'] as $quarterKey) {
            if (isset($values[$quarterKey])) {
                $this->$quarterKey = $values[$quarterKey];
            }
        }
    }
}

$report = new QuarterlyReport();
$report->fillFromInternalData(['q1' => 1000.0, 'q2' => 1200.0]);
echo $report->q1 . PHP_EOL; // 1000

4. Dynamische Methodenaufrufe: $obj->$method() und Array-Callables

Analog zu variablen Funktionen erlaubt PHP dynamische Methodenaufrufe mit der Syntax $objekt->$methodenname(), wobei $methodenname eine Variable ist, die den Namen der aufzurufenden Methode als String enthält. Diese Technik ist die Grundlage vieler generischer Dispatcher, die eine Aktion anhand eines Strings, etwa eines Kommandonamens oder eines Ereignistyps, an die passende Methode weiterleiten, ohne für jeden möglichen Fall eine explizite match-Verzweigung schreiben zu müssen.

Statische Methodenaufrufe können ebenfalls dynamisch erfolgen, mit der Syntax $klasse::$methode() ab PHP 8.0, wobei sowohl der Klassenname als auch der Methodenname aus Variablen stammen können. Für alle diese Varianten gilt derselbe Sicherheitshinweis wie bei variablen Funktionen: Ohne Prüfung mit method_exists() und is_callable() kann ein nicht existierender Methodenname zu einem schwer diagnostizierbaren Fatal Error führen.


<?php

declare(strict_types=1);

final class OrderCommandHandler
{
    public function create(array $payload): string
    {
        return 'Order created: ' . $payload['id'];
    }

    public function cancel(array $payload): string
    {
        return 'Order cancelled: ' . $payload['id'];
    }
}

$handler = new OrderCommandHandler();
$command = 'create'; // e.g. resolved from a CLI argument or a message queue payload

if (method_exists($handler, $command) && is_callable([$handler, $command])) {
    echo $handler->$command(['id' => 42]) . PHP_EOL; // Order created: 42
}

5. Praxisbeispiel: ein dynamischer Dispatcher für CLI-Kommandos

Ein klassischer Anwendungsfall für variable Funktionen und dynamische Methodenaufrufe ist ein einfacher CLI-Dispatcher, der ein Kommando-Argument entgegennimmt und die passende Methode auf einem Handler aufruft, ohne für jedes mögliche Kommando einen eigenen match-Zweig zu schreiben. Dieses Muster reduziert Boilerplate deutlich, insbesondere wenn regelmäßig neue Kommandos hinzukommen und die zentrale Dispatch-Logik unverändert bleiben soll.

Die Sicherheit dieses Musters hängt vollständig von der Kombination aus Allowlist und Existenzprüfung ab. Ein Dispatcher, der Kommandonamen ungefiltert an eine dynamische Methode weiterreicht, öffnet potenziell die Tür zum Aufruf interner, nicht für die CLI gedachter Methoden. Die Allowlist bekannter Kommandos ist daher kein optionales Detail, sondern der zentrale Sicherheitsmechanismus dieses Ansatzes.


<?php

declare(strict_types=1);

final class CliDispatcher
{
    /** @var string[] explicit allowlist of exposed command methods */
    private const ALLOWED_COMMANDS = ['create', 'cancel'];

    public function __construct(private readonly OrderCommandHandler $handler)
    {
    }

    public function dispatch(string $command, array $payload): string
    {
        if (!in_array($command, self::ALLOWED_COMMANDS, true)) {
            throw new InvalidArgumentException("Unknown command: {$command}");
        }

        if (!method_exists($this->handler, $command) || !is_callable([$this->handler, $command])) {
            throw new RuntimeException("Command handler missing for: {$command}");
        }

        return $this->handler->$command($payload);
    }
}

$dispatcher = new CliDispatcher(new OrderCommandHandler());
echo $dispatcher->dispatch('create', ['id' => 42]) . PHP_EOL; // Order created: 42

6. Praxisbeispiel: dynamische Property-Zugriffe für generische DTOs

Variable Variablen in Form von dynamischem Property-Zugriff, also $objekt->$propertyName, sind nützlich für generische Datenübertragungsobjekte, die Werte anhand eines zur Laufzeit bekannten Feldnamens lesen oder setzen müssen, etwa beim Mapping einer Zeile aus einer CSV-Datei auf ein Objekt mit fest definierten, typisierten Properties. Anders als bei einer vollen Reflection-basierten Hydration bleibt dieser Ansatz auf den einfachen Fall beschränkt, in dem alle Zielproperties öffentlich sind.

Der Vorteil gegenüber ReflectionProperty::setValue() liegt im geringeren Overhead, weil kein Reflection-Objekt konstruiert werden muss. Der Nachteil ist, dass private oder protected Properties auf diesem Weg nicht erreichbar sind und PHP bei einem falsch geschriebenen Property-Namen stillschweigend eine neue, dynamische Property anlegt, sofern die Klasse nicht explizit dagegen abgesichert ist, was Tippfehler unsichtbar machen kann.


<?php

declare(strict_types=1);

final class ProductRow
{
    public string $sku = '';
    public float $price = 0.0;
    public int $stock = 0;
}

/** @param array<string, string> $csvRow */
function mapCsvRowToProduct(array $csvRow): ProductRow
{
    $product = new ProductRow();

    // Only known, allowed property names are ever assigned dynamically
    $allowedFields = ['sku', 'price', 'stock'];

    foreach ($allowedFields as $field) {
        if (!isset($csvRow[$field])) {
            continue;
        }

        $product->$field = match ($field) {
            'price' => (float) $csvRow[$field],
            'stock' => (int) $csvRow[$field],
            default => $csvRow[$field],
        };
    }

    return $product;
}

$product = mapCsvRowToProduct(['sku' => 'SKU-1', 'price' => '19.99', 'stock' => '5']);
echo $product->price . PHP_EOL; // 19.99

7. Static-Analysis-Grenzen: warum PHPStan hier kapituliert

Werkzeuge wie PHPStan und Psalm arbeiten, indem sie den Kontrollfluss und die Typen eines Programms statisch, also ohne Ausführung, analysieren. Bei einem Aufruf wie $this->$method(), bei dem $method erst zur Laufzeit einen konkreten Wert erhält, kann kein statisches Werkzeug zuverlässig bestimmen, welche Methode tatsächlich aufgerufen wird, und kann folglich auch nicht prüfen, ob diese Methode überhaupt existiert oder ob die übergebenen Argumente zu ihrer Signatur passen.

Dieser blinde Fleck ist der wichtigste praktische Grund, variable Funktionen und dynamische Methodenaufrufe sparsam einzusetzen: Jeder solche Aufruf ist ein Bereich im Code, den Static-Analysis-Werkzeuge nicht mehr vollständig prüfen können, unabhängig davon, wie hoch das konfigurierte PHPStan-Level ist. Ein Refactoring, das eine Methode umbenennt, wird an dieser Stelle stillschweigend nicht erkannt, während dieselbe Umbenennung bei einem direkten, literalen Methodenaufruf sofort einen Fehler auf Level 0 auslösen würde.

8. First-Class-Callable-Syntax als typsichere Alternative

Seit PHP 8.1 bietet die First-Class-Callable-Syntax mit funktion(...) beziehungsweise $objekt->methode(...) eine typsichere Möglichkeit, eine Referenz auf eine Funktion oder Methode zu erzeugen, ohne auf einen String-Namen zurückzugreifen. Der entscheidende Unterschied zu variablen Funktionen: Der Methodenname bleibt ein literaler Bestandteil des Quelltexts, wodurch IDEs, PHPStan und Refactoring-Werkzeuge den Aufruf vollständig verstehen und bei Umbenennungen automatisch mitziehen.

Für Dispatcher-Muster, bei denen mehrere mögliche Aktionen im Voraus bekannt sind, lässt sich diese Syntax mit einem Array kombinieren, das Kommandonamen auf First-Class-Callables abbildet. Dieser Ansatz behält die Flexibilität eines datengetriebenen Dispatchers, verliert aber nicht die Typsicherheit und Nachvollziehbarkeit, die variable Funktionen und dynamische Methodenaufrufe aufgeben.


<?php

declare(strict_types=1);

final class TypeSafeCommandDispatcher
{
    /** @var array<string, Closure> */
    private array $handlers;

    public function __construct(private readonly OrderCommandHandler $handler)
    {
        // First-class callable syntax: method names stay literal in the source
        $this->handlers = [
            'create' => $this->handler->create(...),
            'cancel' => $this->handler->cancel(...),
        ];
    }

    public function dispatch(string $command, array $payload): string
    {
        $callable = $this->handlers[$command]
            ?? throw new InvalidArgumentException("Unknown command: {$command}");

        return $callable($payload);
    }
}

$dispatcher = new TypeSafeCommandDispatcher(new OrderCommandHandler());
echo $dispatcher->dispatch('cancel', ['id' => 42]) . PHP_EOL; // Order cancelled: 42

9. Variable Funktionen/Variablen im Vergleich zu Alternativen

Die folgende Tabelle stellt variable Funktionen und variable Variablen ihren jeweiligen modernen Alternativen gegenüber.

Anwendungsfall Low-Level-Ansatz Moderne Alternative Vorteil der Alternative
Bekannte Menge von Aktionen dispatchen $obj->$method() First-Class-Callable + Array Statische Analyse und Refactoring bleiben möglich
Mehrere Instanzvariablen aus Formularfeldern setzen $$name Explizites Array mit Allowlist Kein unkontrolliertes Überschreiben von Variablen
Datengetriebene Formatter-Auswahl $fn() aus String match-Ausdruck oder Strategy-Objekt IDE-Unterstützung und Typprüfung erhalten
Objekt aus unbekannten Feldern befüllen $obj->$field ReflectionProperty mit Whitelist Auch private Properties, mit klarer Fehlerbehandlung

In fast allen Fällen bieten moderne Alternativen dieselbe Flexibilität wie variable Funktionen und variable Variablen, ohne deren größten Nachteil zu übernehmen: den Verlust an statischer Prüfbarkeit. Variable Funktionen und variable Variablen bleiben dennoch relevant, um zu verstehen, wie viele ältere Codebasen und einige generische Bibliotheken intern funktionieren.

Mironsoft

Legacy-Code-Modernisierung und typsichere PHP-Architektur

Riskante dynamische Aufrufe im Code entschärfen?

Wir identifizieren variable Funktionen und variable Variablen in bestehenden Codebasen und ersetzen sie dort, wo möglich, durch typsichere, von PHPStan vollständig prüfbare Alternativen.

Legacy-Audit

Riskante dynamische Aufrufe systematisch aufspüren

Sicheres Refactoring

Migration zu First-Class-Callables und typsicheren Dispatchern

PHPStan-Einführung

Statische Analyse auch für dynamisch geprägte Legacy-Projekte etablieren

10. Zusammenfassung

Variable Funktionen und variable Variablen sind die älteste Form der Metaprogrammierung in PHP: Ein Funktionsname in $fn() oder ein Variablenname in $$name wird erst zur Laufzeit aus einem String aufgelöst. Beide Mechanismen sind mächtig für datengetriebene Dispatcher und generische Property-Zugriffe, verlangen aber konsequente Absicherung mit is_callable(), method_exists() und einer Allowlist erlaubter Namen, um Fatal Errors und Sicherheitslücken zu vermeiden.

Der größte Nachteil ist der Verlust an statischer Prüfbarkeit: PHPStan und Psalm können bei dynamisch zusammengesetzten Namen weder Existenz noch Signatur prüfen. Seit PHP 8.1 ersetzt die First-Class-Callable-Syntax in Kombination mit einem Array viele klassische Anwendungsfälle, ohne die Typsicherheit und IDE-Unterstützung aufzugeben, die variable Funktionen und variable Variablen fehlt.

Variable Funktionen und Variable Variablen — Das Wichtigste auf einen Blick

Variable Funktionen

$fn() ruft die Funktion auf, deren Name im String von $fn steht, mit is_callable() prüfen.

Variable Variablen

$$name greift auf die Variable zu, deren Name in $name steht, nur intern und kontrolliert nutzen.

Static-Analysis-Grenze

PHPStan kann Existenz und Signatur bei dynamisch zusammengesetzten Namen nicht prüfen.

Moderne Alternative

First-Class-Callable-Syntax seit PHP 8.1 hält Methodennamen literal und typsicher.

11. FAQ: Variable Funktionen und Variable Variablen

1Was ist eine variable Funktion?
Der Funktionsname steht in einer Variablen, aufgerufen mit $name() statt einem literalen Funktionsnamen.
2Was ist eine variable Variable?
$$name greift auf die Variable zu, deren Name im Inhalt von $name steht.
3Warum Antipattern?
Unkontrolliert aus Benutzereingaben konnten sie beliebige Variablen überschreiben, wie einst bei register_globals.
4Aufruf absichern?
Mit is_callable() prüfen, bei externen Quellen zusätzlich eine Allowlist erlaubter Namen führen.
5Warum kein PHPStan-Check?
Der Name entsteht erst zur Laufzeit, statische Analyse ohne Ausführung kann ihn nicht vorhersagen.
6Unterschied zu $obj->$method()?
Dasselbe Prinzip, nur auf Objektmethoden angewendet statt auf freie Funktionen.
7Was ist First-Class-Callable-Syntax?
funktion(...) erzeugt seit PHP 8.1 eine typsichere Referenz mit literal bleibendem Namen.
8Noch legitim einsetzbar?
Nur intern und kontrolliert, niemals mit direkten Benutzereingaben.
9Was bei nicht existierendem Namen?
Ein Fatal Error, deshalb ist is_callable() vor jedem Aufruf unverzichtbar.
10Ganz vermeiden in neuen Projekten?
Meist ja, zugunsten von match oder First-Class-Callables, nur bei offenen Dispatchern mit Allowlist pragmatisch.