Klassen, Methoden und Parameter zur Laufzeit untersuchen
Die Reflection API macht PHP-Klassen, Methoden, Properties und Parameter zu untersuchbaren Objekten zur Laufzeit. Wer verstehen will, wie Dependency Injection Container, ORMs und Test Frameworks intern arbeiten, kommt an ReflectionClass, ReflectionMethod und ReflectionProperty nicht vorbei, und wer sie richtig einsetzt, kann Frameworks bauen, die sich an beliebige, zur Entwicklungszeit unbekannte Klassen anpassen.
Inhaltsverzeichnis
- 1. Was die Reflection API leistet und wann man sie braucht
- 2. ReflectionClass: Klassen zur Laufzeit untersuchen
- 3. Methoden und Parameter mit ReflectionMethod
- 4. Properties auslesen und schreiben mit ReflectionProperty
- 5. Typen und Nullability über ReflectionType
- 6. Private und protected Member sicher zugänglich machen
- 7. Objekte ohne Konstruktor instanziieren
- 8. Performance: Reflection cachen und wann man sie vermeidet
- 9. Reflection API im Vergleich zu Alternativen
- 10. Zusammenfassung
- 11. FAQ
1. Was die Reflection API leistet und wann man sie braucht
Die Reflection API ist der Teil von PHP, der es einem Programm erlaubt, sich selbst zur Laufzeit zu untersuchen. Klassen, Methoden, Properties und Parameter werden dabei nicht als Text geparst, sondern als echte Objekte zugänglich gemacht, die man abfragen und teilweise sogar verändern kann. Wer schon einmal einen Dependency Injection Container, einen ORM Mapper oder ein Test Framework wie PHPUnit von innen betrachtet hat, ist zwangsläufig auf die Reflection API gestoßen, denn ohne sie müsste jede dieser Bibliotheken feste Annahmen über konkrete Klassen treffen, die sie zur Entwicklungszeit gar nicht kennen kann.
Der typische Auslöser für den Einsatz der Reflection API ist eine Anforderung, bei der Code mit beliebigen, erst zur Laufzeit bekannten Klassen arbeiten muss. Ein Autowiring-Container muss wissen, welche Konstruktor-Parameter eine Klasse erwartet, ohne dass ein Entwickler das manuell konfiguriert. Ein Serializer muss private Properties eines Objekts auslesen können, ohne dass die Klasse dafür öffentliche Getter bereitstellt. Genau für diese Fälle liefert PHP mit den Klassen ReflectionClass, ReflectionMethod, ReflectionProperty und weiteren eine vollständige, in sich konsistente API.
Wichtig ist die Abgrenzung: Die Reflection API ist kein Werkzeug für den alltäglichen Anwendungscode. Sie ist primär für Framework- und Bibliothekscode gedacht, der generisch mit unbekannten Typen arbeiten muss. Wer sie in fachlichem Code einsetzt, um beispielsweise eine private Property von außen zu setzen, umgeht damit bewusst die Kapselung der Klasse, und das sollte eine seltene, gut begründete Ausnahme bleiben, nicht die Regel.
2. ReflectionClass: Klassen zur Laufzeit untersuchen
ReflectionClass ist der Einstiegspunkt in fast jede Nutzung der Reflection API. Ein Objekt dieser Klasse wird entweder mit einem Klassennamen als String oder mit einer bestehenden Instanz erzeugt und liefert danach Zugriff auf sämtliche Metadaten der Klasse: Name, Namespace, Datei, Vererbungshierarchie, implementierte Interfaces, verwendete Traits sowie alle deklarierten Methoden und Properties. Das Besondere daran ist, dass keine dieser Abfragen die Klasse tatsächlich instanziiert. Man kann eine Klasse vollständig untersuchen, ohne jemals ein Objekt davon zu erzeugen.
Für die Praxis besonders relevant ist die Fähigkeit von ReflectionClass, Vererbung und Interfaces programmatisch zu prüfen. Mit implementsInterface() oder isSubclassOf() lässt sich zur Laufzeit feststellen, ob eine gegebene Klasse zu einer bestimmten Kategorie gehört, ohne dass der aufrufende Code die konkrete Klasse selbst kennen muss. Genau darauf bauen Plugin-Systeme und Event-Dispatcher auf, die zur Laufzeit entscheiden, ob ein registrierter Handler für ein bestimmtes Ereignis zuständig ist.
<?php
declare(strict_types=1);
final class ProductPriceCalculator
{
public function __construct(
private readonly float $basePrice,
private readonly float $taxRate = 0.19,
) {
}
public function calculate(): float
{
return $this->basePrice * (1 + $this->taxRate);
}
}
$reflection = new ReflectionClass(ProductPriceCalculator::class);
// Inspect basic class metadata via the Reflection API
echo $reflection->getName() . PHP_EOL; // ProductPriceCalculator
echo $reflection->isFinal() ? 'final' : 'not final'; // final
echo PHP_EOL . $reflection->getFileName() . PHP_EOL; // absolute path to the file
// List all public methods without instantiating the class
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
echo $method->getName() . PHP_EOL;
}
// Check inheritance and interfaces purely by class name
if ($reflection->implementsInterface(Countable::class)) {
echo 'implements Countable' . PHP_EOL;
}
3. Methoden und Parameter mit ReflectionMethod
Sobald man eine Klasse über die Reflection API geöffnet hat, lässt sich jede einzelne Methode wiederum als eigenes Objekt vom Typ ReflectionMethod abfragen. Dieses Objekt kennt nicht nur den Namen der Methode, sondern auch ihre Sichtbarkeit, ob sie statisch, abstrakt oder final ist, und vor allem: welche Parameter sie erwartet. Jeder Parameter wiederum wird als ReflectionParameter repräsentiert, mit eigenem Namen, Typ, Standardwert und der Information, ob er optional ist.
Diese Kombination aus ReflectionMethod und ReflectionParameter ist das technische Fundament von Autowiring in modernen Dependency Injection Containern. Der Container liest die Parameter des Konstruktors aus, ermittelt für jeden Parameter den erwarteten Typ und versucht, eine passende Implementierung aus seiner Konfiguration aufzulösen. Ohne die Reflection API müsste jede Abhängigkeit manuell registriert werden, was bei größeren Anwendungen schnell unpraktikabel wird.
Neben dem reinen Auslesen erlaubt ReflectionMethod auch das dynamische Aufrufen einer Methode über invoke() oder invokeArgs(), selbst wenn die Methode zur Entwicklungszeit gar nicht namentlich bekannt war. Das ist die Grundlage von Command-Bus-Implementierungen, bei denen ein Handler anhand des Namens eines eingehenden Kommandos dynamisch aufgerufen wird, statt über eine lange match-Kette entschieden zu werden.
<?php
declare(strict_types=1);
$reflection = new ReflectionClass(ProductPriceCalculator::class);
$method = $reflection->getMethod('calculate');
foreach ($reflection->getConstructor()->getParameters() as $parameter) {
$type = $parameter->getType();
$typeName = $type instanceof ReflectionNamedType ? $type->getName() : 'mixed';
printf(
'%s: %s%s%s',
$parameter->getName(),
$typeName,
$parameter->allowsNull() ? '|null' : '',
$parameter->isOptional() ? ' (optional)' : ''
);
echo PHP_EOL;
}
// Instantiate via the Reflection API and invoke a method dynamically
$instance = $reflection->newInstance(19.99, 0.19);
$result = $method->invoke($instance);
echo $result . PHP_EOL;
4. Properties auslesen und schreiben mit ReflectionProperty
Neben Methoden erschließt die Reflection API auch Properties über die Klasse ReflectionProperty. Sie liefert Namen, Sichtbarkeit, Typdeklaration und, seit PHP 8.1, direkten Lese- und Schreibzugriff auf den Wert einer Property in einer konkreten Objektinstanz, auch wenn diese Property privat oder protected deklariert ist. Das ist die technische Basis für Serializer und Hydratoren, die Objekte aus Datenbank-Zeilen oder JSON-Payloads aufbauen, ohne dass die Zielklasse dafür öffentliche Setter bereitstellen muss.
Ein Serializer, der auf der Reflection API basiert, iteriert typischerweise über alle Properties einer Klasse mit getProperties(), liest für jede Property Name und deklarierten Typ aus und ordnet daraufhin die passenden Werte aus dem Quelldatensatz zu. Dieser Mechanismus funktioniert unabhängig davon, wie die Zielklasse intern aufgebaut ist, solange die Namen der Properties mit den Schlüsseln der Quelldaten übereinstimmen oder über eine Konvention gemappt werden können.
<?php
declare(strict_types=1);
final class LegacyOrder
{
private float $totalNet = 0.0;
}
$order = new LegacyOrder();
$reflection = new ReflectionClass($order);
$property = $reflection->getProperty('totalNet');
// PHP 8.1+: setAccessible(true) is no longer required
$property->setValue($order, 149.90);
echo $property->getValue($order) . PHP_EOL; // 149.9
// Iterate over all declared properties, including private ones
foreach ($reflection->getProperties() as $prop) {
printf('%s (%s)', $prop->getName(), $prop->isPrivate() ? 'private' : 'public');
echo PHP_EOL;
}
5. Typen und Nullability über ReflectionType
Seit PHP Union Types und Intersection Types unterstützt, reicht ein einfacher String für die Typinformation nicht mehr aus. Die Reflection API löst das mit einer eigenen Klassenhierarchie: ReflectionNamedType für einen einzelnen Typ wie string oder Order, ReflectionUnionType für eine Kombination wie int|string, und ReflectionIntersectionType für Kombinationen wie Countable&Iterator. Jede dieser Klassen implementiert das gemeinsame Interface ReflectionType, sodass Code, der nur grob prüfen will, ob überhaupt ein Typ deklariert ist, unabhängig vom konkreten Fall funktioniert.
Ein häufiger Fehler bei der Arbeit mit der Reflection API ist die Annahme, jeder Parameter habe automatisch einen ReflectionNamedType. Bei Union Types führt ein direkter Aufruf von getName() auf dem falschen Typobjekt zu einem Fehler, weil ReflectionUnionType diese Methode nicht besitzt. Robuster Code prüft daher immer zuerst mit instanceof, um welche konkrete Typklasse es sich handelt, bevor er typ-spezifische Methoden aufruft.
<?php
declare(strict_types=1);
function describeParameterTypes(string $className, string $methodName): void
{
$method = new ReflectionMethod($className, $methodName);
foreach ($method->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof ReflectionUnionType) {
$names = array_map(
static fn (ReflectionNamedType $t): string => $t->getName(),
$type->getTypes()
);
echo $parameter->getName() . ': ' . implode('|', $names) . PHP_EOL;
continue;
}
if ($type instanceof ReflectionNamedType) {
$nullable = $type->allowsNull() ? '?' : '';
echo $parameter->getName() . ': ' . $nullable . $type->getName() . PHP_EOL;
continue;
}
echo $parameter->getName() . ': mixed' . PHP_EOL;
}
}
6. Private und protected Member sicher zugänglich machen
Vor PHP 8.1 musste jeder Zugriff auf ein privates oder protected Member über die Reflection API mit einem expliziten Aufruf von setAccessible(true) freigeschaltet werden. Seit PHP 8.1 ist dieser Aufruf für ReflectionMethod und ReflectionProperty nicht mehr notwendig, die Sichtbarkeit spielt für den reinen Zugriff über Reflection keine Rolle mehr. Das vereinfacht Code erheblich, ändert aber nichts an der Verantwortung: Nur weil man technisch auf jede Property zugreifen kann, sollte man das nicht ohne triftigen Grund tun.
In der Praxis ist der Zugriff auf private Member über die Reflection API vor allem in zwei Szenarien gerechtfertigt: beim Schreiben von Unit Tests, die den internen Zustand eines Objekts prüfen müssen, ohne die Produktionsklasse mit zusätzlichen Testing-Gettern zu verunreinigen, und bei generischen Serializern beziehungsweise Hydratoren, die Objekte aus externen Daten aufbauen. Außerhalb dieser Fälle ist der direkte Zugriff auf private Zustände ein Zeichen dafür, dass die Klassengrenzen möglicherweise falsch gezogen wurden.
7. Objekte ohne Konstruktor instanziieren
Ein besonders mächtiges Werkzeug der Reflection API ist ReflectionClass::newInstanceWithoutConstructor(). Diese Methode erzeugt ein vollständiges Objekt der Zielklasse, ohne den Konstruktor auszuführen. Das klingt zunächst ungewöhnlich, ist aber für bestimmte Anwendungsfälle unverzichtbar: Ein ORM, das eine Entität aus einer Datenbankzeile rekonstruiert, will nicht die Geschäftslogik des Konstruktors erneut durchlaufen, etwa das Setzen eines neuen Zeitstempels oder das Auslösen eines Domain Events, das nur bei echter Neuanlage sinnvoll ist.
Nach der Instanziierung ohne Konstruktor werden die Properties typischerweise direkt über ReflectionProperty::setValue() befüllt, wie im vorherigen Abschnitt gezeigt. Diese Kombination aus newInstanceWithoutConstructor() und direktem Property-Zugriff ist genau der Mechanismus, den viele PHP-ORMs wie Doctrine intern nutzen, um sogenannte Proxy-Objekte und Lazy-Loading-Entitäten zu erzeugen, ohne die fachliche Konstruktorlogik zu verletzen.
<?php
declare(strict_types=1);
final class ReflectionCache
{
/** @var array<class-string, ReflectionClass> */
private static array $cache = [];
public static function forClass(string $className): ReflectionClass
{
// Reuse ReflectionClass instances instead of building them per call
return self::$cache[$className] ??= new ReflectionClass($className);
}
}
// Build an object graph without triggering constructor side effects
$reflection = ReflectionCache::forClass(LegacyOrder::class);
$order = $reflection->newInstanceWithoutConstructor();
$property = $reflection->getProperty('totalNet');
$property->setValue($order, 0.0);
// Repeated lookups reuse the same cached ReflectionClass instance
$again = ReflectionCache::forClass(LegacyOrder::class);
var_dump($reflection === $again); // bool(true)
8. Performance: Reflection cachen und wann man sie vermeidet
Die Reflection API ist deutlich langsamer als direkter Code, weil PHP für jede Abfrage interne Metadaten-Strukturen durchsuchen muss, die für normalen Codepfad gar nicht vorgehalten werden. In einer heißen Schleife, die tausendfach pro Request ausgeführt wird, kann wiederholtes Erzeugen von ReflectionClass-Instanzen und wiederholtes Auslesen derselben Methoden- und Property-Listen zu einem messbaren Overhead führen. Die gängige Gegenmaßnahme ist Caching: Statt für jeden Aufruf ein neues Reflection-Objekt zu erzeugen, wird es einmal berechnet und danach für die Lebensdauer des Requests oder sogar prozessübergreifend über OPcache-kompatible Strukturen wiederverwendet.
Frameworks wie Symfony und Laravel gehen noch einen Schritt weiter und vermeiden die Reflection API im heißen Pfad komplett, indem sie zur Buildzeit oder beim ersten Aufruf generierten PHP-Code erzeugen, der die zuvor per Reflection ermittelten Informationen fest verdrahtet. Dieses Muster wird in einem eigenen Artikel dieser Serie zur Code-Generierung zur Buildzeit vertieft. Für die meisten Anwendungen reicht jedoch ein einfacher, statischer Cache pro ReflectionClass, wie im vorherigen Codebeispiel gezeigt, um den Großteil des Overheads zu eliminieren.
9. Reflection API im Vergleich zu Alternativen
Nicht jede Aufgabe, die man mit der Reflection API lösen kann, sollte auch tatsächlich damit gelöst werden. Die folgende Tabelle zeigt typische Aufgaben und stellt jeweils die Alternative ohne Reflection gegenüber, um die Entscheidung im konkreten Projekt zu erleichtern.
| Aufgabe | Ohne Reflection API | Mit Reflection API | Vorteil |
|---|---|---|---|
| Private Property in Tests setzen | Nur-für-Tests-Setter im Produktionscode | ReflectionProperty::setValue() |
Kein zusätzlicher Produktionscode nötig |
| Klassen dynamisch instanziieren | Lange match-Kette über Klassennamen |
ReflectionClass::newInstance() |
Erweiterbar ohne Codeänderung |
| Konstruktor-Parameter für DI auflösen | Manuelle Registrierung jeder Abhängigkeit | ReflectionParameter::getType() |
Automatisches Autowiring |
| Interfaces prüfen | instanceof mit bekannter Klasse |
implementsInterface() |
Funktioniert auch mit reinen Klassennamen-Strings |
| Objekt ohne Konstruktor erzeugen | Nicht möglich mit new |
newInstanceWithoutConstructor() |
Für ORM-Hydration und Deserialisierung |
Die Tabelle zeigt ein Muster: Die Reflection API lohnt sich immer dann, wenn Code mit Klassen arbeiten muss, die zur Entwicklungszeit nicht bekannt sind, oder wenn er auf Zustände zugreifen muss, für die es keine öffentliche Schnittstelle gibt. Ist die Zielklasse hingegen fest bekannt, ist direkter Code fast immer die schnellere und robustere Wahl.
Mironsoft
PHP-Architektur, Framework-Entwicklung und Legacy-Modernisierung
Framework-nahen PHP-Code sauber und wartbar bauen?
Wir entwickeln PHP-Bibliotheken und Frameworks, die die Reflection API dort einsetzen, wo sie echten Mehrwert bringt, und dort vermeiden, wo generierter Code oder einfache Konventionen ausreichen.
Architektur-Review
Prüfung, wo Reflection sinnvoll ist und wo sie Performance kostet
DI-Container
Autowiring-fähige Container mit Caching-Strategie entwickeln
Performance-Tuning
Reflection-lastige Bibliotheken auf Produktionslast optimieren
10. Zusammenfassung
Die PHP Reflection API macht Klassen, Methoden, Properties und Parameter zur Laufzeit zu untersuchbaren Objekten. ReflectionClass liefert Metadaten über eine Klasse, ohne sie zu instanziieren. ReflectionMethod und ReflectionParameter erschließen Methodensignaturen und sind die Grundlage von Autowiring in Dependency Injection Containern. ReflectionProperty erlaubt seit PHP 8.1 direkten Zugriff auf private und protected Zustände, ohne den umständlichen Aufruf von setAccessible(true).
newInstanceWithoutConstructor() ist das Werkzeug für ORM-Hydration und Deserialisierung, bei der Objekte rekonstruiert werden müssen, ohne die Konstruktorlogik erneut auszuführen. Weil die Reflection API spürbar langsamer ist als direkter Code, gehört Caching der Reflection-Objekte in jede produktive Nutzung, und in performance-kritischen Pfaden lohnt sich oft der Wechsel zu Code-Generierung zur Buildzeit statt wiederholter Reflection zur Laufzeit.
Die PHP Reflection API im Detail — Das Wichtigste auf einen Blick
ReflectionClass
Liefert Name, Interfaces, Vererbung und Mitglieder einer Klasse, ohne sie zu instanziieren.
ReflectionMethod/Parameter
Grundlage für Autowiring in DI-Containern und für dynamisches Aufrufen von Methoden.
ReflectionProperty
Seit PHP 8.1 direkter Zugriff auf private/protected Properties ohne setAccessible(true).
Performance
Reflection-Objekte cachen, in heißen Pfaden Code-Generierung zur Buildzeit erwägen.