0

Reflection API — ReflectionClass, ReflectionMethod, ReflectionParameter

Advanced5 min read·php-08-013
interviewlaravel-srcframework

Concept

The Reflection API provides runtime inspection of classes, methods, properties, functions, and parameters without executing them. It's the engine behind dependency injection containers, test mocking frameworks, documentation generators, and attribute processors.

Key classes: ReflectionClass — inspect a class's structure. ReflectionMethod — inspect a method's signature, visibility, attributes. ReflectionParameter — inspect a parameter's name, type, default value. ReflectionProperty — inspect properties. ReflectionFunction — inspect functions and closures.

Performance: Reflection is relatively slow (~5-20× slower than direct code) because it reads from and parses PHP's internal structures. Don't use it in hot paths. Containers cache reflection results after the first analysis — Laravel's container only reflects a class once per binding.

Laravel container uses Reflection: When you call app(UserService::class) and UserService is not explicitly bound, the container uses ReflectionClass to inspect the constructor parameters. For each parameter, it reads the type hint, finds the corresponding binding in the container, and injects it. This is how auto-wiring works.

Use cases: Auto-wiring DI containers, unit test mocking (PHPUnit reflects methods to generate mocks), data mappers (hydrating objects from arrays by reading property types), attribute-driven routing (reading #[Route] attributes from controller methods), CLI tools that list command options from parameter types.

Code Example

php
<?php
declare(strict_types=1);

interface LoggerInterface {}
class FileLogger implements LoggerInterface {}

class UserService
{
    public function __construct(
        private readonly LoggerInterface $logger,
        private readonly string $env = 'production',
    ) {}

    #[\Attribute]
    class Route {}

    #[Route]
    public function createUser(string $name, string $email): array
    {
        return ['name' => $name, 'email' => $email];
    }
}

// Inspect UserService
$rc = new ReflectionClass(UserService::class);

echo $rc->getName();            // "UserService"
echo $rc->isAbstract();         // false
echo count($rc->getMethods());  // number of methods

// Inspect constructor parameters (how DI containers do auto-wiring)
$constructor = $rc->getConstructor();
foreach ($constructor->getParameters() as $param) {
    echo "Param: " . $param->getName() . "\n";

    $type = $param->getType();
    if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
        echo "  Type: " . $type->getName() . " (class/interface)\n";
        // Container resolves this by finding a binding for LoggerInterface
    }

    if ($param->isOptional()) {
        echo "  Default: " . var_export($param->getDefaultValue(), true) . "\n";
    }
}

// Read attributes (PHP 8.0+)
$method = $rc->getMethod('createUser');
$attrs  = $method->getAttributes();
foreach ($attrs as $attr) {
    echo "Attribute: " . $attr->getName() . "\n";
    $instance = $attr->newInstance(); // instantiates the attribute class
}

// Simple auto-wiring container (simplified)
function autoWire(string $className, array $bindings): object
{
    $rc = new ReflectionClass($className);
    $constructor = $rc->getConstructor();
    if ($constructor === null) return new $className();

    $args = [];
    foreach ($constructor->getParameters() as $param) {
        $type = $param->getType()?->getName();
        if ($type && isset($bindings[$type])) {
            $args[] = $bindings[$type]; // inject bound implementation
        } elseif ($param->isOptional()) {
            $args[] = $param->getDefaultValue();
        }
    }
    return new $className(...$args);
}

$service = autoWire(UserService::class, [
    LoggerInterface::class => new FileLogger(),
]);