Inspecting classes, methods and parameters at runtime
The Reflection API turns PHP classes, methods, properties and parameters into inspectable objects at runtime. Anyone who wants to understand how dependency injection containers, ORMs and test frameworks work internally cannot avoid ReflectionClass, ReflectionMethod and ReflectionProperty, and using them correctly lets you build frameworks that adapt to arbitrary classes unknown at development time.
Table of Contents
- 1. What the Reflection API actually solves
- 2. ReflectionClass: inspecting classes at runtime
- 3. Methods and parameters with ReflectionMethod
- 4. Reading and writing properties with ReflectionProperty
- 5. Types and nullability via ReflectionType
- 6. Safely accessing private and protected members
- 7. Instantiating objects without a constructor
- 8. Performance: caching reflection and when to avoid it
- 9. Reflection API compared to alternatives
- 10. Summary
- 11. FAQ
1. What the Reflection API actually solves
The Reflection API is the part of PHP that lets a program inspect itself at runtime. Classes, methods, properties and parameters are not read as text, they are exposed as real objects that can be queried and, in some cases, even modified. Anyone who has ever looked inside a dependency injection container, an ORM mapper or a test framework like PHPUnit has inevitably run into the Reflection API, because without it, each of these libraries would need to make fixed assumptions about concrete classes it cannot possibly know about at development time.
The typical trigger for reaching for the Reflection API is a requirement where code must work with arbitrary classes that are only known at runtime. An autowiring container needs to know which constructor parameters a class expects, without a developer configuring that by hand. A serializer needs to read private properties of an object without the target class providing public getters for that purpose. PHP addresses exactly these cases with a complete, internally consistent API built around classes like ReflectionClass, ReflectionMethod, ReflectionProperty and more.
An important distinction: the Reflection API is not a tool for everyday application code. It is primarily meant for framework and library code that has to work generically with unknown types. Using it in domain code, for example to set a private property from the outside, deliberately bypasses the class's encapsulation, and that should remain a rare, well justified exception rather than the rule.
2. ReflectionClass: inspecting classes at runtime
ReflectionClass is the entry point into almost every use of the Reflection API. An object of this class is created either from a class name as a string or from an existing instance, and afterward it exposes all of the class's metadata: name, namespace, file, inheritance hierarchy, implemented interfaces, used traits, and every declared method and property. What is notable is that none of these queries actually instantiate the class. You can fully inspect a class without ever creating an object of it.
Particularly relevant in practice is ReflectionClass's ability to check inheritance and interfaces programmatically. With implementsInterface() or isSubclassOf() you can determine at runtime whether a given class belongs to a certain category, without the calling code needing to know the concrete class itself. That is exactly what plugin systems and event dispatchers rely on when they decide at runtime whether a registered handler is responsible for a particular event.
<?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. Methods and parameters with ReflectionMethod
Once a class has been opened via the Reflection API, every single method can in turn be queried as its own object of type ReflectionMethod. This object knows not only the method's name, but also its visibility, whether it is static, abstract or final, and above all which parameters it expects. Each parameter in turn is represented as a ReflectionParameter, with its own name, type, default value, and information about whether it is optional.
This combination of ReflectionMethod and ReflectionParameter is the technical foundation of autowiring in modern dependency injection containers. The container reads the constructor's parameters, determines the expected type for each parameter, and tries to resolve a matching implementation from its configuration. Without the Reflection API, every dependency would have to be registered manually, which quickly becomes impractical in larger applications.
Beyond pure introspection, ReflectionMethod also allows dynamically calling a method via invoke() or invokeArgs(), even if the method's name was not known at development time. That is the foundation of command bus implementations, where a handler is invoked dynamically based on the name of an incoming command, instead of being decided through a long match chain.
<?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. Reading and writing properties with ReflectionProperty
Besides methods, the Reflection API also exposes properties through the ReflectionProperty class. It provides name, visibility, declared type and, since PHP 8.1, direct read and write access to the value of a property on a concrete object instance, even if that property is declared private or protected. This is the technical basis for serializers and hydrators that build objects from database rows or JSON payloads without the target class having to provide public setters for that purpose.
A serializer built on the Reflection API typically iterates over all properties of a class with getProperties(), reads the name and declared type for each property, and then assigns the matching values from the source data. This mechanism works regardless of how the target class is structured internally, as long as the property names match the keys of the source data, or can be mapped through a convention.
<?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. Types and nullability via ReflectionType
Since PHP added support for union types and intersection types, a plain string is no longer enough to represent type information. The Reflection API solves this with its own class hierarchy: ReflectionNamedType for a single type like string or Order, ReflectionUnionType for a combination like int|string, and ReflectionIntersectionType for combinations like Countable&Iterator. Each of these classes implements the shared ReflectionType interface, so code that only needs to roughly check whether a type is declared at all works regardless of the concrete case.
A common mistake when working with the Reflection API is assuming that every parameter automatically has a ReflectionNamedType. With union types, calling getName() directly on the wrong type object leads to an error, because ReflectionUnionType does not have that method. Robust code always checks with instanceof first to determine which concrete type class it is dealing with, before calling type-specific methods.
<?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. Safely accessing private and protected members
Before PHP 8.1, every access to a private or protected member through the Reflection API had to be unlocked with an explicit call to setAccessible(true). Since PHP 8.1, this call is no longer needed for ReflectionMethod and ReflectionProperty, visibility no longer matters for pure reflection access. This simplifies code considerably, but it does not change the responsibility involved: just because you technically can access every property does not mean you should, without a solid reason.
In practice, accessing private members through the Reflection API is mainly justified in two scenarios: writing unit tests that need to verify an object's internal state without polluting the production class with testing-only getters, and building generic serializers or hydrators that construct objects from external data. Outside of these cases, direct access to private state is usually a sign that the class boundaries may have been drawn incorrectly.
7. Instantiating objects without a constructor
A particularly powerful tool in the Reflection API is ReflectionClass::newInstanceWithoutConstructor(). This method creates a complete object of the target class without running the constructor. That sounds unusual at first, but it is indispensable for certain use cases: an ORM that reconstructs an entity from a database row does not want to rerun the constructor's business logic again, such as setting a new timestamp or firing a domain event that only makes sense on genuine creation.
After instantiation without a constructor, properties are typically populated directly through ReflectionProperty::setValue(), as shown in the previous section. This combination of newInstanceWithoutConstructor() and direct property access is exactly the mechanism many PHP ORMs like Doctrine use internally to create so called proxy objects and lazy loading entities without violating the constructor's domain logic.
<?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: caching reflection and when to avoid it
The Reflection API is noticeably slower than direct code, because PHP has to search internal metadata structures for every query that are not maintained for the normal code path at all. In a hot loop that runs thousands of times per request, repeatedly creating ReflectionClass instances and repeatedly reading the same method and property lists can lead to measurable overhead. The usual countermeasure is caching: instead of creating a new reflection object on every call, it is computed once and then reused for the lifetime of the request, or even across processes via OPcache-compatible structures.
Frameworks like Symfony and Laravel go a step further and avoid the Reflection API entirely in the hot path, by generating PHP code at build time or on first call that hardcodes the information previously determined through reflection. This pattern is covered in depth in a dedicated article of this series on build-time code generation. For most applications, however, a simple static cache per ReflectionClass, as shown in the previous code example, is enough to eliminate most of the overhead.
9. Reflection API compared to alternatives
Not every task that can be solved with the Reflection API should actually be solved with it. The following table shows typical tasks and contrasts them with the alternative without reflection, to make the decision easier in a concrete project.
| Task | Without Reflection API | With Reflection API | Benefit |
|---|---|---|---|
| Setting a private property in tests | Testing-only setter in production code | ReflectionProperty::setValue() |
No extra production code needed |
| Instantiating classes dynamically | Long match chain over class names |
ReflectionClass::newInstance() |
Extensible without code changes |
| Resolving constructor parameters for DI | Manual registration of every dependency | ReflectionParameter::getType() |
Automatic autowiring |
| Checking interfaces | instanceof with a known class |
implementsInterface() |
Works with plain class name strings too |
| Creating an object without a constructor | Not possible with new |
newInstanceWithoutConstructor() |
For ORM hydration and deserialization |
The table reveals a pattern: the Reflection API pays off whenever code has to work with classes that are unknown at development time, or when it needs to access state for which there is no public interface. If the target class is fixed and known, direct code is almost always the faster and more robust choice.
Mironsoft
PHP architecture, framework development and legacy modernization
Building clean, maintainable framework-level PHP code?
We build PHP libraries and frameworks that use the Reflection API where it adds real value, and avoid it where generated code or simple conventions are enough.
Architecture review
Checking where reflection is worthwhile and where it costs performance
DI containers
Building autowiring-capable containers with a caching strategy
Performance tuning
Optimizing reflection-heavy libraries under production load
10. Summary
The PHP Reflection API turns classes, methods, properties and parameters into inspectable objects at runtime. ReflectionClass provides metadata about a class without instantiating it. ReflectionMethod and ReflectionParameter expose method signatures and form the basis of autowiring in dependency injection containers. ReflectionProperty has allowed direct access to private and protected state since PHP 8.1, without the cumbersome call to setAccessible(true).
newInstanceWithoutConstructor() is the tool for ORM hydration and deserialization, where objects must be reconstructed without rerunning constructor logic. Because the Reflection API is noticeably slower than direct code, caching reflection objects belongs in every production usage, and in performance-critical paths it is often worth switching to build-time code generation instead of repeated runtime reflection.
The PHP Reflection API in Detail — Key Takeaways
ReflectionClass
Provides name, interfaces, inheritance and members of a class without instantiating it.
ReflectionMethod/Parameter
Foundation for autowiring in DI containers and for dynamically invoking methods.
ReflectionProperty
Since PHP 8.1, direct access to private/protected properties without setAccessible(true).
Performance
Cache reflection objects, consider build-time code generation in hot paths.