Разбор класса в PHP: от рефлексии до низкоуровневого парсинга
Разбор класса в PHP: методы метапрограммирования
Разбор класса (parse class) - это извлечение информации о его структуре: методах, свойствах, константах, аннотациях, трейтах и т.д. В PHP это реализуется двумя основными подходами: через Reflection API (рекомендуемый) и через низкоуровневый лексический анализ исходного кода. Каждый вариант применим в разных сценариях и имеет свои ограничения.
Как получить полную информацию о классе с помощью ReflectionClass?
ReflectionClass - это встроенный класс, предоставляющий всесторонний доступ к метаданным класса без чтения исходного кода. Он умеет работать с загруженными в память классами, учитывает наследование, интерфейсы, трейты.
<?php
class User
{
const STATUS_ACTIVE = 1;
public string $name;
private int $age;
protected array $roles;
public function __construct(string $name) { $this->name = $name; }
public function getName(): string { return $this->name; }
private function validateAge(): bool { return $this->age > 0; }
}
$reflection = new ReflectionClass('User');
echo 'Имя класса: ' . $reflection->getName() . PHP_EOL;
echo 'Методы: ';
foreach ($reflection->getMethods() as $method) {
echo $method->getName() . ' (' . implode(', ', $method->getModifiers()) . ')';
}
echo PHP_EOL . 'Свойства: ';
foreach ($reflection->getProperties() as $prop) {
echo $prop->getName() . ' (' . implode(', ', $prop->getModifiers()) . ')';
}
echo PHP_EOL . 'Константы: ';
print_r($reflection->getConstants());Parse class php (разбор класса в php)
Имя класса: User Методы: __construct (public), getName (public), validateAge (private) Свойства: name (public), age (private), roles (protected) Константы: Array ( [STATUS_ACTIVE] => 1 )
В примере использованы ключевые методы ReflectionClass: getMethods(), getProperties(), getConstants(). Каждый возвращает массив объектов соответствующих ReflectionMethod, ReflectionProperty, либо скалярные значения для констант.
Типичные ошибки:
- Попытка разобрать несуществующий класс - выбрасывается ReflectionException. Решение: обернуть в try/catch или использовать class_exists().
- ReflectionClass не показывает динамические свойства (те, что добавлены через __set). Для их анализа потребуется перехватывать вызовы на уровне экземпляра.
- Методы, объявленные в трейтах, не помечаются как принадлежащие трейту, но их можно идентифицировать через getDeclaringClass().
Как проанализировать класс, если он ещё не загружен (например, в файле)?
Если класс не требует выполнения, можно применить лексический анализатор PHP token_get_all(). Этот способ полезен для статического анализаторов, IDE, инструментов автодополнения.
<?php
function parseClassFromFile(string $filePath): array
{
$source = file_get_contents($filePath);
$tokens = token_get_all($source);
$classes = [];
$count = count($tokens);
for ($i = 0; $i < $count; $i++) {
if ($tokens[$i][0] === T_CLASS) {
$className = $tokens[$i + 2][1] ?? 'unknown';
$classes[] = $className;
}
}
return $classes;
}
// Пример: parseClassFromFile('User.php');
Array ( [0] => User )
Этот базовый парсер находит только имена классов. Для получения методов и свойств потребуется более глубокий анализ AST. Существуют готовые библиотеки (nikic/php-parser), которые упрощают эту задачу.
Проблемы:
- token_get_all не понимает синтаксис PHP 8 и выше (атрибуты, union types) - могут появиться ложные токены.
- Для корректного разбора наследования, трейтов нужно обрабатывать сложную последовательность токенов.
- Не учитывает классы, объявленные в условных блоках или внутри строк (eval).
Как получить только методы или свойства без Reflection, используя встроенные функции?
Функции get_class_methods() и get_class_vars() возвращают списки, но с ограничениями: они показывают только публичные методы/свойства. Для полного разбора они непригодны.
<?php
class Product {
public $title;
protected $price;
private $id;
public function getTitle() {}
private function setPrice() {}
}
print_r(get_class_methods('Product'));
print_r(get_class_vars('Product'));
Array ( [0] => getTitle ) Array ( [title] => )
Обратите внимание: приватные и защищённые элементы отсутствуют. Поэтому для серьёзных задач нужно использовать Reflection.
Ошибки: get_class_vars() не возвращает значение по умолчанию, если свойство не инициализировано (покажет null). Кроме того, при наследовании эти функции не дают информацию о родительских protected/private членах.
Как извлечь DocBlock комментарии и атрибуты классов?
ReflectionClass имеет методы getDocComment() и getAttributes() (начиная с PHP 8.0). Это позволяет получать аннотации и метаданные для генерации документации или валидации.
<?php
/**
* @author Ivan Petrov
*/
#[Route('/user')]
class UserController {}
$ref = new ReflectionClass('UserController');
echo 'DocBlock: ' . $ref->getDocComment() . PHP_EOL;
$attrs = $ref->getAttributes();
foreach ($attrs as $attr) {
echo 'Attribute: ' . $attr->getName() . PHP_EOL;
}
DocBlock: /** * @author Ivan Petrov */ Attribute: Route
Возможные затруднения: getDocComment() возвращает false, если нет комментария; getAttributes() возвращает пустой массив при отсутствии атрибутов. Нужно проверять на false/empty.
Расширенные примеры разбора класса
1. Рекурсивный обзор иерархии классов с Reflection
Следующий код показывает, как получить цепочку наследования, интерфейсы и трейты для любого класса.
<?php
interface Logger {}
trait Timestampable {
public function getCreatedAt() { return new DateTime(); }
}
class BaseEntity {}
class Product extends BaseEntity implements Logger {
use Timestampable;
public string $name;
protected float $price;
}
$ref = new ReflectionClass('Product');
echo 'Родительский класс: ' . ($ref->getParentClass() ? $ref->getParentClass()->getName() : 'нет') . PHP_EOL;
echo 'Интерфейсы: ' . implode(', ', $ref->getInterfaceNames()) . PHP_EOL;
echo 'Трейты: ' . implode(', ', array_keys($ref->getTraits())) . PHP_EOL;
echo 'Свойства (включая унаследованные): ';
foreach ($ref->getProperties() as $prop) {
echo $prop->getName() . ' из класса ' . $prop->getDeclaringClass()->getName() . ', ';
}
Родительский класс: BaseEntity Интерфейсы: Logger Трейты: Timestampable Свойства (включая унаследованные): name из класса Product, price из класса Product
2. Получение типов параметров методов и возвращаемых значений
ReflectionMethod позволяет анализировать сигнатуру метода, включая union types в PHP 8.
<?php
class Calculator {
public function add(int|float $a, int|float $b): int|float {
return $a + $b;
}
}
$method = new ReflectionMethod('Calculator', 'add');
echo 'Параметры:' . PHP_EOL;
foreach ($method->getParameters() as $param) {
$type = $param->getType();
echo ' ' . $param->getName() . ': ' . $type . ' (разрешённые типы: ' . implode(', ', $type instanceof ReflectionUnionType ? $type->getTypes() : [$type]) . ')' . PHP_EOL;
}
echo 'Возвращаемый тип: ' . $method->getReturnType();
Параметры: a: int|float (разрешённые типы: int, float) b: int|float (разрешённые типы: int, float) Возвращаемый тип: int|float
3. Создание генератора документации на основе аннотаций
Этот пример извлекает все публичные методы класса и их DocBlock комментарии, формируя HTML-список.
<?php
class UserService {
/**
* Получить пользователя по ID
* @param int $id
* @return User|null
*/
public function find(int $id): ?User {}
/**
* Сохранить пользователя
* @param User $user
* @return bool
*/
public function save(User $user): bool {}
}
$ref = new ReflectionClass('UserService');
$methods = $ref->getMethods(ReflectionMethod::IS_PUBLIC);
$docs = [];
foreach ($methods as $method) {
$doc = $method->getDocComment();
if ($doc) {
$docs[$method->getName()] = $doc;
}
}
echo '<ul>';
foreach ($docs as $name => $doc) {
echo '<li><strong>' . htmlspecialchars($name) . '</strong>: ' . nl2br(htmlspecialchars($doc)) . '</li>';
}
echo '</ul>';
<ul><li><strong>find</strong>: /**
* Получить пользователя по ID
* @param int $id
* @return User|null
*/</li><li><strong>save</strong>: /**
* Сохранить пользователя
* @param User $user
* @return bool
*/</li></ul>
4. Динамическая проверка атрибутов (PHP 8) для маршрутизации
Атрибуты - современный способ метапрограммирования. Reflection позволяет их обрабатывать.
<?php
#[Attribute]
class Route {
public function __construct(public string $path) {}
}
#[Route('/api/users')]
class UserApiController {}
$ref = new ReflectionClass('UserApiController');
$routeAttr = $ref->getAttributes(Route::class)[0] ?? null;
if ($routeAttr) {
$route = $routeAttr->newInstance();
echo 'Path: ' . $route->path;
}
Path: /api/users
5. Копирование свойств между объектами с помощью ReflectionProperty
ReflectionProperty позволяет устанавливать и получать значения даже приватных свойств внутри или вне класса (с setAccessible).
<?php
class Order {
private int $id;
public string $status;
public function __construct(int $id, string $status) {
$this->id = $id;
$this->status = $status;
}
}
$order1 = new Order(1, 'new');
$order2 = new Order(2, 'paid');
$ref = new ReflectionClass('Order');
foreach ($ref->getProperties() as $prop) {
$prop->setAccessible(true);
$value = $prop->getValue($order1);
$prop->setValue($order2, $value);
}
echo 'id: ' . $ref->getProperty('id')->getValue($order2) . ', status: ' . $order2->status;
id: 1, status: new