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

ReflectionClass

a class, interface, trait or enum: name, parent, interfaces, methods, properties, constants, attributes, modifiers; newInstance(), newInstanceArgs(), newInstanceWithoutConstructor(), lazy objects (8.4)

ReflectionObject

an object’s class, including its dynamic properties

ReflectionMethod / ReflectionFunction

a method / function or closure: parameters, return type, modifiers, doc comment, attributes; invoke(), invokeArgs()

ReflectionParameter

a parameter: name, position, type, default value, variadic, by reference, promoted

ReflectionProperty

a property: type, modifiers, default, hooks (8.4), raw values; getValue()/setValue()

ReflectionClassConstant

a class constant or enum case

ReflectionConstant

a global constant (8.4; getAttributes() and getFileName() in 8.5)

ReflectionEnum, ReflectionEnumBackedCase, ReflectionEnumUnitCase

enums and their cases

ReflectionNamedType, ReflectionUnionType, ReflectionIntersectionType

declared types

ReflectionAttribute

an attribute on any of the above

ReflectionGenerator, ReflectionFiber, ReflectionReference, ReflectionExtension

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.

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

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().

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() and isArray()/isCallable() were deprecated in 8.0; use getType().

  • Union, intersection and DNF types, enums, attributes, property hooks, lazy objects and ReflectionConstant all post-date PHP 7, so older material only knows ReflectionNamedType.

  • Annotation parsing from getDocComment() has been replaced by native attributes.