Этот раздел посвящён продвинутым возможностям Reflection API: работе с атрибутами, перечислениями, Fiber и построению реальных инструментов -- роутера, валидатора, сериализатора и ORM-маппера.
ReflectionFunction -- анализ функций и замыканий
<?php
declare(strict_types=1);
// Named function
function calculateTax(float $amount, float $rate = 0.20): float
{
return $amount * $rate;
}
$ref = new ReflectionFunction('calculateTax');
echo $ref->getName(); // calculateTax
echo $ref->getNumberOfParameters(); // 2
echo $ref->getNumberOfRequiredParameters(); // 1
echo $ref->getReturnType()->getName(); // float
// Analyzing closures
$multiply = fn(int $a, int $b): int => $a * $b;
$closureRef = new ReflectionFunction($multiply);
echo $closureRef->getName(); // {closure}
foreach ($closureRef->getParameters() as $param) {
echo "\${$param->getName()}: {$param->getType()->getName()}" . PHP_EOL;
}
// $a: int
// $b: int
Анализ замыканий с привязкой
<?php
declare(strict_types=1);
$factor = 10;
$closure = function (int $x) use ($factor): int {
return $x * $factor;
};
$ref = new ReflectionFunction($closure);
// Get closure's bound variables (use-variables)
$staticVars = $ref->getStaticVariables();
print_r($staticVars); // ['factor' => 10]
// Get closure's $this binding
$closureThis = $ref->getClosureThis();
var_dump($closureThis); // null (no $this binding)
// Get closure's scope class
$scopeClass = $ref->getClosureScopeClass();
var_dump($scopeClass); // null (not bound to a class)
ReflectionEnum -- анализ перечислений (PHP 8.1+)
<?php
declare(strict_types=1);
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
public function label(): string
{
return match ($this) {
self::Active => 'Active',
self::Inactive => 'Inactive',
self::Pending => 'Pending Review',
};
}
}
$ref = new ReflectionEnum(Status::class);
// Basic checks
var_dump($ref->isEnum()); // true
var_dump($ref->isBacked()); // true (has string backing type)
// Backing type
$backingType = $ref->getBackingType();
echo $backingType->getName(); // string
// Get all cases
$cases = $ref->getCases();
foreach ($cases as $case) {
echo sprintf(
'%s = %s',
$case->getName(),
$case->getValue(), // Only for backed enums (ReflectionEnumBackedCase)
) . PHP_EOL;
}
// Active = active
// Inactive = inactive
// Pending = pending
// Get single case
$activeCase = $ref->getCase('Active');
echo $activeCase->getName(); // Active
if ($activeCase instanceof ReflectionEnumBackedCase) {
echo $activeCase->getBackingValue(); // active
}
// Check if case exists
var_dump($ref->hasCase('Active')); // true
var_dump($ref->hasCase('Deleted')); // false
Unit enum (без backing type)
<?php
declare(strict_types=1);
enum Color
{
case Red;
case Green;
case Blue;
}
$ref = new ReflectionEnum(Color::class);
var_dump($ref->isBacked()); // false
var_dump($ref->getBackingType()); // null
$case = $ref->getCase('Red');
// $case is ReflectionEnumUnitCase (not ReflectionEnumBackedCase)
echo $case->getName(); // Red
// $case->getBackingValue() would throw Error (no backing value)
Программное создание экземпляров enum
<?php
declare(strict_types=1);
enum Priority: int
{
case Low = 1;
case Medium = 5;
case High = 10;
}
$ref = new ReflectionEnum(Priority::class);
// Get enum value through ReflectionEnumBackedCase
foreach ($ref->getCases() as $case) {
if ($case instanceof ReflectionEnumBackedCase) {
$enumInstance = $case->getValue(); // Returns actual enum instance
echo sprintf(
'%s (value: %d, label: %s)',
$enumInstance->name,
$enumInstance->value,
get_class($enumInstance),
) . PHP_EOL;
}
}
ReflectionAttribute -- атрибуты (PHP 8.0+)
Атрибуты -- нативные метаданные PHP. Рефлексия позволяет их читать.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Route
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET',
public readonly ?string $name = null,
) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
final class Middleware
{
/** @param list<string> $middlewares */
public function __construct(
public readonly array $middlewares,
) {}
}
#[Route('/api/users', name: 'users')]
final class UserController
{
#[Route('/api/users', method: 'GET', name: 'users.list')]
#[Middleware(['auth', 'throttle'])]
public function list(): array
{
return [];
}
#[Route('/api/users/{id}', method: 'GET', name: 'users.show')]
public function show(int $id): array
{
return [];
}
}
Чтение атрибутов через рефлексию
<?php
declare(strict_types=1);
$classRef = new ReflectionClass(UserController::class);
// Get attributes on class
$classAttributes = $classRef->getAttributes();
foreach ($classAttributes as $attr) {
echo $attr->getName(); // Route
echo $attr->getTarget(); // Attribute::TARGET_CLASS
var_dump($attr->isRepeated()); // false (only one Route on class)
// Get constructor arguments
$args = $attr->getArguments();
// [0 => '/api/users', 'name' => 'users']
// Create instance of the attribute
$instance = $attr->newInstance();
echo $instance->path; // /api/users
echo $instance->name; // users
echo $instance->method; // GET (default)
}
// Get attributes on method
$methodRef = $classRef->getMethod('list');
$methodAttrs = $methodRef->getAttributes();
foreach ($methodAttrs as $attr) {
$instance = $attr->newInstance();
echo get_class($instance) . ': ';
if ($instance instanceof Route) {
echo "{$instance->method} {$instance->path}";
} elseif ($instance instanceof Middleware) {
echo implode(', ', $instance->middlewares);
}
echo PHP_EOL;
}
// Route: GET /api/users
// Middleware: auth, throttle
Фильтрация атрибутов по типу
<?php
declare(strict_types=1);
$methodRef = new ReflectionMethod(UserController::class, 'list');
// Get only Route attributes (filter by class name)
$routeAttrs = $methodRef->getAttributes(Route::class);
foreach ($routeAttrs as $attr) {
$route = $attr->newInstance();
echo "{$route->method} {$route->path}" . PHP_EOL;
}
// Filter with IS_INSTANCEOF (includes subclasses)
$routeAttrs = $methodRef->getAttributes(
Route::class,
ReflectionAttribute::IS_INSTANCEOF,
);
Построение роутера через атрибуты
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
final class ApiRoute
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET',
public readonly ?string $name = null,
) {}
}
final readonly class RouteDefinition
{
public function __construct(
public string $path,
public string $httpMethod,
public string $controllerClass,
public string $controllerMethod,
public ?string $name = null,
) {}
}
final class RouteCollector
{
/** @var list<RouteDefinition> */
private array $routes = [];
/**
* Scan controller classes and collect routes from attributes.
*
* @param list<class-string> $controllers
*/
public function collect(array $controllers): void
{
foreach ($controllers as $controllerClass) {
$classRef = new ReflectionClass($controllerClass);
foreach ($classRef->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$attributes = $method->getAttributes(ApiRoute::class);
foreach ($attributes as $attr) {
$route = $attr->newInstance();
$this->routes[] = new RouteDefinition(
path: $route->path,
httpMethod: $route->method,
controllerClass: $controllerClass,
controllerMethod: $method->getName(),
name: $route->name,
);
}
}
}
}
/**
* Find matching route for a request.
*/
public function match(string $httpMethod, string $path): ?RouteDefinition
{
foreach ($this->routes as $route) {
if ($route->httpMethod !== $httpMethod) {
continue;
}
// Simple pattern matching (convert {param} to regex)
$pattern = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $route->path);
$pattern = '#^' . $pattern . '$#';
if (preg_match($pattern, $path)) {
return $route;
}
}
return null;
}
/** @return list<RouteDefinition> */
public function getRoutes(): array
{
return $this->routes;
}
}
// Example controller
final class OrderController
{
#[ApiRoute('/api/orders', method: 'GET', name: 'orders.list')]
public function list(): array
{
return ['orders' => []];
}
#[ApiRoute('/api/orders/{id}', method: 'GET', name: 'orders.show')]
public function show(int $id): array
{
return ['order' => $id];
}
#[ApiRoute('/api/orders', method: 'POST', name: 'orders.create')]
public function create(): array
{
return ['created' => true];
}
}
// Usage
$collector = new RouteCollector();
$collector->collect([OrderController::class]);
$route = $collector->match('GET', '/api/orders/42');
if ($route !== null) {
echo "{$route->controllerClass}::{$route->controllerMethod}" . PHP_EOL;
// OrderController::show
}
Построение валидатора через атрибуты
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_PROPERTY)]
final class NotBlank
{
public function __construct(
public readonly string $message = 'This field cannot be blank',
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Length
{
public function __construct(
public readonly ?int $min = null,
public readonly ?int $max = null,
public readonly string $message = 'Invalid length',
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Email
{
public function __construct(
public readonly string $message = 'Invalid email address',
) {}
}
final readonly class ValidationError
{
public function __construct(
public string $property,
public string $message,
) {}
}
final class Validator
{
/**
* Validate an object based on its property attributes.
*
* @return list<ValidationError>
*/
public function validate(object $object): array
{
$errors = [];
$ref = new ReflectionClass($object);
foreach ($ref->getProperties() as $property) {
$value = $property->getValue($object);
foreach ($property->getAttributes() as $attr) {
$constraint = $attr->newInstance();
$error = match (true) {
$constraint instanceof NotBlank => $this->validateNotBlank(
$value,
$property->getName(),
$constraint,
),
$constraint instanceof Length => $this->validateLength(
$value,
$property->getName(),
$constraint,
),
$constraint instanceof Email => $this->validateEmail(
$value,
$property->getName(),
$constraint,
),
default => null,
};
if ($error !== null) {
$errors[] = $error;
}
}
}
return $errors;
}
private function validateNotBlank(mixed $value, string $prop, NotBlank $constraint): ?ValidationError
{
if ($value === null || $value === '' || $value === []) {
return new ValidationError($prop, $constraint->message);
}
return null;
}
private function validateLength(mixed $value, string $prop, Length $constraint): ?ValidationError
{
if (!is_string($value)) {
return null;
}
$length = mb_strlen($value);
if ($constraint->min !== null && $length < $constraint->min) {
return new ValidationError($prop, "{$constraint->message}: minimum {$constraint->min}");
}
if ($constraint->max !== null && $length > $constraint->max) {
return new ValidationError($prop, "{$constraint->message}: maximum {$constraint->max}");
}
return null;
}
private function validateEmail(mixed $value, string $prop, Email $constraint): ?ValidationError
{
if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_EMAIL)) {
return new ValidationError($prop, $constraint->message);
}
return null;
}
}
// DTO with validation attributes
final class CreateUserRequest
{
public function __construct(
#[NotBlank]
#[Length(min: 2, max: 50)]
public readonly string $name,
#[NotBlank]
#[Email]
public readonly string $email,
#[Length(min: 8, max: 128)]
public readonly string $password,
) {}
}
$request = new CreateUserRequest(
name: 'A',
email: 'not-an-email',
password: '123',
);
$validator = new Validator();
$errors = $validator->validate($request);
foreach ($errors as $error) {
echo "{$error->property}: {$error->message}" . PHP_EOL;
}
// name: Invalid length: minimum 2
// email: Invalid email address
// password: Invalid length: minimum 8
Построение сериализатора
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_PROPERTY)]
final class SerializedName
{
public function __construct(
public readonly string $name,
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Ignore
{
}
final class Serializer
{
/**
* Serialize object to associative array.
*
* @return array<string, mixed>
*/
public function serialize(object $object): array
{
$ref = new ReflectionClass($object);
$data = [];
foreach ($ref->getProperties() as $property) {
// Skip properties with #[Ignore]
if ($property->getAttributes(Ignore::class) !== []) {
continue;
}
// Check for custom name
$nameAttrs = $property->getAttributes(SerializedName::class);
$key = $nameAttrs !== []
? $nameAttrs[0]->newInstance()->name
: $this->camelToSnake($property->getName());
$value = $property->getValue($object);
// Recursively serialize nested objects
$data[$key] = match (true) {
is_object($value) && !$value instanceof \DateTimeInterface && !$value instanceof \BackedEnum
=> $this->serialize($value),
$value instanceof \DateTimeInterface
=> $value->format('c'),
$value instanceof \BackedEnum
=> $value->value,
is_array($value)
=> array_map(
fn($item) => is_object($item) ? $this->serialize($item) : $item,
$value,
),
default => $value,
};
}
return $data;
}
private function camelToSnake(string $input): string
{
return strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst($input)));
}
}
// Usage
final class UserResponse
{
public function __construct(
public readonly int $id,
#[SerializedName('full_name')]
public readonly string $firstName,
public readonly string $email,
#[Ignore]
public readonly string $passwordHash,
public readonly \DateTimeImmutable $createdAt,
) {}
}
$user = new UserResponse(
id: 1,
firstName: 'Alice',
email: '[email protected]',
passwordHash: '$2y$10$...',
createdAt: new \DateTimeImmutable('2025-01-15 12:00:00'),
);
$serializer = new Serializer();
$data = $serializer->serialize($user);
print_r($data);
// [
// 'id' => 1,
// 'full_name' => 'Alice',
// 'email' => '[email protected]',
// 'created_at' => '2025-01-15T12:00:00+00:00',
// ]
// Note: passwordHash is excluded (has #[Ignore])
ReflectionClassConstant -- типизированные константы (PHP 8.3+)
<?php
declare(strict_types=1);
class Config
{
public const string APP_NAME = 'MyApp';
public const int MAX_RETRIES = 3;
public const float TIMEOUT = 30.5;
protected const array ALLOWED_HOSTS = ['localhost', '127.0.0.1'];
}
$ref = new ReflectionClass(Config::class);
foreach ($ref->getReflectionConstants() as $const) {
echo sprintf(
'%s %s: %s = %s',
$const->isPublic() ? 'public' : ($const->isProtected() ? 'protected' : 'private'),
$const->hasType() ? $const->getType()->getName() : 'untyped',
$const->getName(),
var_export($const->getValue(), true),
) . PHP_EOL;
}
// public string: APP_NAME = 'MyApp'
// public int: MAX_RETRIES = 3
// public float: TIMEOUT = 30.5
// protected array: ALLOWED_HOSTS = array('localhost', '127.0.0.1')
ReflectionFiber -- анализ Fiber (PHP 8.1+)
<?php
declare(strict_types=1);
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('first suspend');
echo "Resumed with: {$value}" . PHP_EOL;
Fiber::suspend('second suspend');
});
$refFiber = new ReflectionFiber($fiber);
// Before start
// echo $refFiber->getExecutingFile(); // Error: fiber not started
$result = $fiber->start();
echo $result . PHP_EOL; // first suspend
// After start, during suspend
$refFiber = new ReflectionFiber($fiber);
echo $refFiber->getExecutingFile() . PHP_EOL; // /path/to/file.php
echo $refFiber->getExecutingLine() . PHP_EOL; // line number of Fiber::suspend()
// Get backtrace
$trace = $refFiber->getTrace();
print_r($trace); // Stack trace at point of suspension
// Get the actual Fiber
$actualFiber = $refFiber->getFiber();
var_dump($actualFiber === $fiber); // true
Тестирование с рефлексией
Рефлексия часто используется для тестирования приватных методов и свойств.
<?php
declare(strict_types=1);
final class PaymentProcessor
{
private int $retryCount = 0;
private const int MAX_RETRIES = 3;
public function process(float $amount): bool
{
return $this->attemptPayment($amount);
}
private function attemptPayment(float $amount): bool
{
// Complex payment logic
$this->retryCount++;
if ($amount <= 0) {
return false;
}
return $this->retryCount <= self::MAX_RETRIES;
}
private function calculateFee(float $amount): float
{
return round($amount * 0.029 + 0.30, 2);
}
}
// In PHPUnit test:
final class PaymentProcessorTest // extends TestCase
{
/**
* Helper to invoke private methods.
*/
private function invokePrivateMethod(
object $object,
string $methodName,
mixed ...$args,
): mixed {
$method = new ReflectionMethod($object, $methodName);
return $method->invoke($object, ...$args);
}
/**
* Helper to get private property value.
*/
private function getPrivateProperty(object $object, string $propertyName): mixed
{
$property = new ReflectionProperty($object, $propertyName);
return $property->getValue($object);
}
/**
* Helper to set private property value.
*/
private function setPrivateProperty(
object $object,
string $propertyName,
mixed $value,
): void {
$property = new ReflectionProperty($object, $propertyName);
$property->setValue($object, $value);
}
public function testCalculateFee(): void
{
$processor = new PaymentProcessor();
$fee = $this->invokePrivateMethod($processor, 'calculateFee', 100.00);
assert($fee === 3.20); // 100 * 0.029 + 0.30 = 3.20
}
public function testRetryCountIncrement(): void
{
$processor = new PaymentProcessor();
$processor->process(50.00);
$retryCount = $this->getPrivateProperty($processor, 'retryCount');
assert($retryCount === 1);
}
public function testMaxRetriesExceeded(): void
{
$processor = new PaymentProcessor();
// Set retry count close to max
$this->setPrivateProperty($processor, 'retryCount', 3);
$result = $processor->process(50.00);
assert($result === false); // retryCount (4) > MAX_RETRIES (3)
}
}
Замечание: Тестирование приватных методов -- спорная практика. Многие считают, что тестировать нужно только публичный API. Если вам часто приходится тестировать приватные методы, возможно, стоит вынести логику в отдельный класс.
Производительность: кэширование рефлексии
<?php
declare(strict_types=1);
final class ReflectionCache
{
/** @var array<class-string, ReflectionClass> */
private static array $classes = [];
/** @var array<string, list<ReflectionAttribute>> */
private static array $attributes = [];
/** @var array<class-string, list<ReflectionProperty>> */
private static array $properties = [];
/**
* @template T of object
* @param class-string<T> $className
* @return ReflectionClass<T>
*/
public static function getClass(string $className): ReflectionClass
{
return self::$classes[$className] ??= new ReflectionClass($className);
}
/**
* @return list<ReflectionProperty>
*/
public static function getProperties(string $className): array
{
return self::$properties[$className] ??= self::getClass($className)->getProperties();
}
/**
* Get attributes of a specific type from a property.
*
* @template T of object
* @param class-string<T> $attributeClass
* @return list<ReflectionAttribute<T>>
*/
public static function getPropertyAttributes(
string $className,
string $propertyName,
string $attributeClass,
): array {
$key = "{$className}::{$propertyName}@{$attributeClass}";
if (!isset(self::$attributes[$key])) {
$property = self::getClass($className)->getProperty($propertyName);
self::$attributes[$key] = $property->getAttributes($attributeClass);
}
return self::$attributes[$key];
}
public static function clear(): void
{
self::$classes = [];
self::$attributes = [];
self::$properties = [];
}
}
// Usage — repeated reflection calls use cache
$ref = ReflectionCache::getClass(User::class); // Creates ReflectionClass once
$ref2 = ReflectionCache::getClass(User::class); // Returns cached instance
var_dump($ref === $ref2); // true
Запомни:
ReflectionAttribute::newInstance()создаёт новый экземпляр атрибута при каждом вызове -- обязательно кэшируйте результаты.ReflectionEnumдоступен с PHP 8.1 и позволяет анализировать перечисления: получать case'ы, backing type и значения. Для сложных систем на атрибутах (роутинг, валидация, ORM) всегда используйте кэш рефлексии -- это может ускорить работу в 10-100 раз.