Reflection
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05; the isReadable()/isWritable() example was run on the
PHP 8.6 release candidate. Reflection lets a program inspect its own code at runtime: the classes, their methods,
properties, constants and attributes, the parameters and types of functions, and the cases of enums. It is how
dependency-injection containers autowire services, how routers find attribute routes, how ORMs map properties to
columns and how test frameworks find tests. Application code rarely needs it directly.
The main classes
| Class | Reflects |
|---|---|
|
a class, interface, trait or enum: name, parent, interfaces, methods, properties, constants, attributes, modifiers; |
|
an object’s class, including its dynamic properties |
|
a method / function or closure: parameters, return type, modifiers, doc comment, attributes; |
|
a parameter: name, position, type, default value, variadic, by reference, promoted |
|
a property: type, modifiers, default, hooks (8.4), raw values; |
|
a class constant or enum case |
|
a global constant (8.4; |
|
enums and their cases |
|
declared types |
|
an attribute on any of the above |
|
specialised cases |
Source: Reflection
Inspecting classes, methods and parameters
<?php
declare(strict_types=1);
interface Clock { public function now(): DateTimeImmutable; }
/** Computes due dates for loans. */
final class LoanService
{
public const int DEFAULT_DAYS = 21;
public function __construct(private Clock $clock, private int $days = self::DEFAULT_DAYS) {}
public function dueDate(string $isbn, ?int $extraDays = null): string
{
return $this->clock->now()->modify('+' . ($this->days + ($extraDays ?? 0)) . ' days')->format('Y-m-d');
}
}
$class = new ReflectionClass(LoanService::class);
printf("%s, final: %s, doc: %s\n", $class->getShortName(), var_export($class->isFinal(), true), trim((string) $class->getDocComment(), "/* \n"));
print_r($class->getConstants()); // ["DEFAULT_DAYS" => 21]
foreach ($class->getMethods() as $method) {
$params = array_map(
fn (ReflectionParameter $p): string => sprintf('%s $%s%s', $p->getType(), $p->getName(),
$p->isDefaultValueAvailable() ? ' = ' . var_export($p->getDefaultValue(), true) : ''),
$method->getParameters(),
);
printf("%s(%s): %s\n", $method->getName(), implode(', ', $params), $method->getReturnType() ?? 'mixed');
}
LoanService, final: true, doc: Computes due dates for loans.
Array
(
[DEFAULT_DAYS] => 21
)
__construct(Clock $clock, int $days = 21): mixed
dueDate(string $isbn, ?int $extraDays = NULL): string
Since PHP 8.4, new ReflectionMethod('Class::method') with a single string argument is deprecated; use
ReflectionMethod::createFromMethodName('Class::method') or the two-argument form.
Source: ReflectionClass, ReflectionMethod and ReflectionParameter
Types
getType() (on parameters and properties) and getReturnType() return null for no declaration, a
ReflectionNamedType for a single type (including ?T), or a ReflectionUnionType / ReflectionIntersectionType
whose getTypes() lists the members. ReflectionNamedType::isBuiltin() distinguishes int from a class name.
<?php
declare(strict_types=1);
final class Book
{
public int|string $id = 1;
public (Countable&Traversable)|null $tags = null;
public ?DateTimeImmutable $publishedAt = null;
}
foreach ((new ReflectionClass(Book::class))->getProperties() as $property) {
$type = $property->getType();
printf("%-12s %-22s %s nullable:%s\n", $property->getName(), get_class($type), $type, var_export($type->allowsNull(), true));
}
id ReflectionUnionType string|int nullable:false
tags ReflectionUnionType (Countable&Traversable)|null nullable:true
publishedAt ReflectionNamedType ?DateTimeImmutable nullable:true
Source: ReflectionType, ReflectionUnionType and ReflectionIntersectionType
Attributes, property hooks, enums and constants
<?php
declare(strict_types=1);
enum Format: string { case Ebook = 'eb'; case Paperback = 'pb'; }
final class Item
{
public string $title {
get => ucfirst($this->title);
set => trim($value);
}
}
// Property hooks (8.4)
$prop = new ReflectionProperty(Item::class, 'title');
var_dump($prop->hasHooks(), array_keys($prop->getHooks()), $prop->getHook(PropertyHookType::Set)?->getName());
// Enums
$enum = new ReflectionEnum(Format::class);
echo $enum->getBackingType(), ' ', implode(',', array_map(fn ($c) => $c->getBackingValue(), $enum->getCases())), "\n"; // string eb,pb
// Global constants (ReflectionConstant, 8.4)
var_dump((new ReflectionConstant('PHP_VERSION'))->getExtensionName()); // string(4) "Core" (8.5)
Reading attributes is shown in Attributes: every reflector has
getAttributes().
Source: ReflectionProperty::getHooks, ReflectionEnum and ReflectionConstant
isReadable() and isWritable() (upcoming in PHP 8.6)
With asymmetric visibility, readonly and hooks, "can this scope read or write this property?" became complicated.
PHP 8.6 adds ReflectionProperty::isReadable(?string $scope) and isWritable(?string $scope), which answer it for
a given calling scope (null meaning global scope):
<?php
declare(strict_types=1);
// requires PHP 8.6
final class Copy
{
public private(set) string $status = 'available';
public string $label { get => strtoupper($this->status); }
}
$status = new ReflectionProperty(Copy::class, 'status');
var_dump($status->isReadable(null), $status->isWritable(null), $status->isWritable(Copy::class)); // true false true
var_dump((new ReflectionProperty(Copy::class, 'label'))->isWritable(Copy::class)); // false: get-only virtual
Worked example: a tiny autowiring container
A dependency-injection container builds objects together with their dependencies. Autowiring means it reads constructor parameter types with Reflection and resolves them recursively, so most services need no configuration. This is, in miniature, what Laravel’s service container and Symfony’s DependencyInjection component do (see Service Container, Providers and Facades).
<?php
declare(strict_types=1);
interface Clock { public function now(): DateTimeImmutable; }
final class FixedClock implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-10-05'); } }
final class LoanRepository { public array $loans = []; }
final class LoanService
{
public function __construct(private Clock $clock, private LoanRepository $repository, private int $days = 21) {}
public function lend(string $isbn): string
{
$due = $this->clock->now()->modify("+{$this->days} days")->format('Y-m-d');
$this->repository->loans[$isbn] = $due;
return "{$isbn} due {$due}";
}
}
final class Container
{
/** @var array<class-string, class-string> */
private array $bindings = [];
/** @var array<class-string, object> */
private array $instances = [];
public function bind(string $abstract, string $concrete): void { $this->bindings[$abstract] = $concrete; }
/** @template T of object @param class-string<T> $id @return T */
public function get(string $id): object
{
if (isset($this->instances[$id])) {
return $this->instances[$id]; // shared (singleton) instances
}
$class = new ReflectionClass($this->bindings[$id] ?? $id);
if (!$class->isInstantiable()) {
throw new LogicException("Cannot instantiate {$id}: bind it to a concrete class");
}
$args = [];
foreach ($class->getConstructor()?->getParameters() ?? [] as $param) {
$type = $param->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$args[] = $this->get($type->getName()); // recurse into class dependencies
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue(); // scalars: use the default
} else {
throw new LogicException("Cannot resolve \${$param->getName()} of {$class->getName()}");
}
}
return $this->instances[$id] = $class->newInstanceArgs($args);
}
}
$container = new Container();
$container->bind(Clock::class, FixedClock::class); // interface -> implementation
echo $container->get(LoanService::class)->lend('978-0-00-000001-1'), "\n"; // 978-0-00-000001-1 due 2026-10-26
var_dump($container->get(LoanRepository::class)->loans); // the same shared repository was injected
Worked example: an attribute-based validator
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_PROPERTY)]
final class NotBlank {}
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Range { public function __construct(public int $min, public int $max) {} }
final class ReviewForm
{
public function __construct(
#[NotBlank] public string $title = '',
#[Range(1, 5)] public int $stars = 0,
public string $comment = '',
) {}
}
/** @return list<string> */
function validate(object $form): array
{
$errors = [];
foreach ((new ReflectionObject($form))->getProperties() as $property) {
$value = $property->getValue($form);
foreach ($property->getAttributes() as $attribute) {
$rule = $attribute->newInstance();
$error = match (true) {
$rule instanceof NotBlank => trim((string) $value) === '' ? 'must not be blank' : null,
$rule instanceof Range => $value < $rule->min || $value > $rule->max ? "must be between {$rule->min} and {$rule->max}" : null,
default => null,
};
if ($error !== null) {
$errors[] = "{$property->getName()} {$error}";
}
}
}
return $errors;
}
print_r(validate(new ReviewForm(title: ' ', stars: 7)));
print_r(validate(new ReviewForm(title: 'Loved it', stars: 5))); // []
Array
(
[0] => title must not be blank
[1] => stars must be between 1 and 5
)
Array
(
)
Attributes on promoted constructor parameters apply to both the parameter and the property (unless the attribute’s
target restricts it), which is why #[NotBlank] is visible through getProperties() here.
Costs and cautions
-
Reflection is slower than direct code; cache whatever you derive from it (frameworks compile containers and routes).
-
setAccessible()is no longer needed (and has no effect) since PHP 8.1: Reflection can read private members directly. That power bypasses encapsulation; keep it in infrastructure code and tests. -
newInstanceWithoutConstructor()creates objects without running their constructor, leaving typed properties uninitialised; serialisers and ORMs use it, application code should not.
Where older books and tutorials are outdated
-
setAccessible(true)before reading private properties (Beak ch. 5) is a no-op since 8.1. -
ReflectionParameter::getClass()andisArray()/isCallable()were deprecated in 8.0; usegetType(). -
Union, intersection and DNF types, enums, attributes, property hooks, lazy objects and
ReflectionConstantall post-date PHP 7, so older material only knowsReflectionNamedType. -
Annotation parsing from
getDocComment()has been replaced by native attributes.