Reflection API
What Is Reflection? In simple terms: Reflection lets a PHP program examine its own code while it runs: which properties a class has, which parameters a method expects, what types they use, which attri
What Is Reflection?
In simple terms: Reflection lets a PHP program examine its own code while it runs: which properties a class has, which parameters a method expects, what types they use, which attributes are attached, and more. It can also create objects and call methods based on that information. Reflection is how frameworks "magically" know what your classes need.
Technically: The Reflection extension (always available) provides classes such as ReflectionClass, ReflectionObject, ReflectionMethod, ReflectionProperty, ReflectionFunction, ReflectionParameter, ReflectionNamedType, ReflectionEnum, and ReflectionAttribute. Each one describes a part of the code and offers methods to inspect — and sometimes use — it.
Where Reflection Is Used
| Tool | What It Does With Reflection |
|---|---|
| Dependency injection containers (Laravel, Symfony) | Read constructor parameters to create and inject dependencies automatically |
| ORMs (Doctrine) | Read attributes and properties to map objects to database tables |
| Validators and serializers | Read attributes and property types |
| Testing tools (PHPUnit) | Find test methods, create mocks |
| Documentation and static analysis | Describe classes, methods, and types |
Application code rarely needs reflection directly, but understanding it explains how these tools work.
Inspecting a Class
<?php
final class Product
{
public const CURRENCY = 'INR';
public function __construct(
public readonly string $sku,
private int $pricePaise,
protected ?string $category = null,
) {}
public function price(): float
{
return $this->pricePaise / 100;
}
private function internalCode(): string
{
return 'P-' . $this->sku;
}
}
$class = new ReflectionClass(Product::class);
echo $class->getName() . ($class->isFinal() ? ' (final)' : '') . "\n";
foreach ($class->getProperties() as $property) {
$visibility = $property->isPublic() ? 'public' : ($property->isProtected() ? 'protected' : 'private');
$readonly = $property->isReadOnly() ? ' readonly' : '';
echo " property: $visibility {$property->getType()} \${$property->getName()}$readonly\n";
}
foreach ($class->getMethods() as $method) {
$returnType = $method->hasReturnType() ? ': ' . $method->getReturnType() : '';
echo " method: {$method->getName()}() with " . $method->getNumberOfParameters() . " parameter(s)$returnType\n";
}
print_r($class->getConstants());
Output
Product (final)
property: public string $sku readonly
property: private int $pricePaise
property: protected ?string $category
method: __construct() with 3 parameter(s)
method: price() with 0 parameter(s): float
method: internalCode() with 0 parameter(s): string
Array
(
[CURRENCY] => INR
)
How It Works
new ReflectionClass(Product::class)creates a reflection object describing the class (the class name as a string or an object can be passed).getProperties()returnsReflectionPropertyobjects, including private and protected ones, in declaration order. Promoted constructor properties are included.getType()returns aReflectionType; converting it to a string gives the declared type (?string).getMethods()returnsReflectionMethodobjects;getReturnType()describes return types.getConstants()returns all class constants.
Common ReflectionClass methods include getShortName(), getParentClass(), getInterfaceNames(), implementsInterface(), isAbstract(), isInterface(), hasMethod(), getConstructor(), getAttributes(), and newInstanceArgs().
Inspecting Parameters and Types
<?php
function createInvoice(int $customerId, array $items, ?string $coupon = null, string $currency = 'INR'): string
{
return "invoice";
}
$function = new ReflectionFunction('createInvoice');
foreach ($function->getParameters() as $parameter) {
$type = $parameter->getType();
$line = sprintf('$%s: %s', $parameter->getName(), $type);
if ($type instanceof ReflectionNamedType && $type->allowsNull()) {
$line .= ' (nullable)';
}
if ($parameter->isDefaultValueAvailable()) {
$line .= ' = ' . var_export($parameter->getDefaultValue(), true);
}
echo $line . "\n";
}
Output
$customerId: int
$items: array
$coupon: ?string (nullable) = NULL
$currency: string = 'INR'
ReflectionNamedType describes a single type: getName() returns string, isBuiltin() tells whether it is a built-in type (as opposed to a class), and allowsNull() whether null is accepted. Union and intersection types are described by ReflectionUnionType and ReflectionIntersectionType, whose getTypes() returns the individual types.
Accessing Private Members
Reflection can read private properties and call private methods. Since PHP 8.1, no setAccessible(true) call is needed:
<?php
final class Product
{
public function __construct(private int $pricePaise, private string $sku) {}
private function internalCode(): string
{
return 'P-' . $this->sku;
}
}
$product = new Product(129900, 'LAMP-01');
$class = new ReflectionClass($product);
echo $class->getProperty('pricePaise')->getValue($product) . "\n";
echo $class->getMethod('internalCode')->invoke($product) . "\n";
Output
129900
P-LAMP-01
This is useful for frameworks (hydrating objects from database rows) and occasionally for tests, but bypassing visibility in application code breaks encapsulation. Treat it as a tool for infrastructure code only.
Practical Example: A Tiny Autowiring Container
Dependency injection containers use reflection to read constructor parameters and create the required objects automatically ("autowiring"):
<?php
final class Logger
{
public function log(string $message): void
{
echo "[log] $message\n";
}
}
final class Mailer
{
public function __construct(private Logger $logger) {}
public function send(string $to, string $message): void
{
$this->logger->log("Mail to $to: $message");
}
}
final class OrderService
{
public function __construct(
private Mailer $mailer,
private Logger $logger,
private int $retries = 3,
) {}
public function place(int $orderId, string $email): void
{
$this->logger->log("Placing order $orderId (retries: {$this->retries})");
$this->mailer->send($email, "Order $orderId confirmed");
}
}
final class Container
{
private array $instances = [];
public function get(string $class): object
{
return $this->instances[$class] ??= $this->build($class);
}
private function build(string $class): object
{
$reflection = new ReflectionClass($class);
$constructor = $reflection->getConstructor();
if ($constructor === null) {
return $reflection->newInstance();
}
$arguments = [];
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$arguments[] = $this->get($type->getName());
} elseif ($parameter->isDefaultValueAvailable()) {
$arguments[] = $parameter->getDefaultValue();
} else {
throw new LogicException("Cannot resolve \${$parameter->getName()} for $class");
}
}
return $reflection->newInstanceArgs($arguments);
}
}
$container = new Container();
$container->get(OrderService::class)->place(501, 'asha@example.com');
var_dump($container->get(Logger::class) === $container->get(Logger::class));
Output
[log] Placing order 501 (retries: 3)
[log] Mail to asha@example.com: Order 501 confirmed
bool(true)
Step-by-Step Explanation
- Requesting
OrderServicemakes the container inspect its constructor: it needs aMailer, aLogger, and anintwith a default. - For class-typed parameters, the container recursively builds the dependency (
Maileritself needs aLogger). - Built-in typed parameters with defaults use their default value; anything else that cannot be resolved causes a clear exception.
??=caches each created object, so every class gets one shared instance (the sameLoggeris used everywhere).newInstanceArgs()creates the object with the resolved arguments.
Real containers add interface-to-class bindings, configuration values, and caching of the reflection results, but the principle is the same.
Other Reflection Features
| Feature | Example |
|---|---|
| Attributes | $class->getAttributes(Route::class) (see PHP Attributes) |
| Enums | new ReflectionEnum(OrderStatus::class) → getCases(), getBackingType() |
| Closures | new ReflectionFunction($closure) → parameters, scope, static variables |
| Generators and fibers | ReflectionGenerator, ReflectionFiber for debugging tools |
| Lazy objects (PHP 8.4) | $class->newLazyGhost(...) / newLazyProxy(...) create objects that initialize themselves on first use — used by ORMs and DI containers |
Performance
Creating reflection objects and reading metadata is slower than ordinary code. Frameworks therefore analyze classes once — often during a build or cache-warming step — and store the results (for example, as compiled container code). Avoid reflection inside hot loops of request handling.
Common Mistakes
Using Reflection Where Plain Code Works
Effect: Harder-to-read, slower code.
Fix: Use interfaces, callbacks, and normal method calls; reserve reflection for generic infrastructure.
Breaking Encapsulation in Application Code
Effect: Code depends on private details that may change.
Fix: Expose what is needed through public methods.
Ignoring Union and Intersection Types
Effect: Code assumes getType() always returns a ReflectionNamedType with getName().
Fix: Check the type class with instanceof before calling type-specific methods.
Repeating Reflection on Every Request
Effect: Unnecessary overhead.
Fix: Cache the extracted metadata.
Best Practices
- Use reflection for frameworks, libraries, and tooling — not for everyday business logic.
- Check type classes (
ReflectionNamedType,ReflectionUnionType) before using them. - Combine reflection with attributes for declarative configuration.
- Cache reflection results in production.
- Prefer public APIs over reading private members.
Official reference: https\://www.php.net/manual/en/book.reflection.php