Разбор класса в PHP: от рефлексии до низкоуровневого парсинга

Раздел: 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

Разбор класса в PHP - comments

En
Parse class php (php)