PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 54 · От основ к архитектуре
Атрибуты и Reflection: метаданные, маршруты и границы автоматизации
Видим, как фреймворк читает описание маршрута, чем метаданные отличаются от исполняемого действия и почему обход конструктора опасен.
Узнать договор метода, не выполняя его
У клиента CRM есть listMethod, но вы не помните входные параметры. Reflection предоставляет объект-описание класса или метода. Он читает устройство программы, чтобы построить документацию или проверить конфигурацию.
Что получится. Увидим listMethod: parameters — обязательно; method — обязательно. Сам запрос CRM не выполняется.
- ReflectionMethod получает класс и имя метода
- getParameters возвращает описания параметров
- Каждое описание сообщает имя и обязательность
- Программа печатает подсказку вызова
<?php
declare(strict_types=1);
class B24Client
{
public function listMethod(array $parameters, string $method): array { return []; }
}
$method = new ReflectionMethod(B24Client::class, 'listMethod');
foreach ($method->getParameters() as $parameter) {
echo $method->getName(), ': ', $parameter->getName(),
$parameter->isOptional() ? ' — необязательно' : ' — обязательно', "\n";
}
Ожидаемый результат
listMethod: parameters — обязательно
listMethod: method — обязательно
B24Client::class даёт строковое имя класса, а не экземпляр клиента. ReflectionMethod описывает выбранный метод. getParameters возвращает объекты описания его входов. Пустое тело listMethod здесь служит только декларацией исследуемого договора.
Исследовать публичное API без вывода всех внутренних значений
Reflection читает описание клиента, а не вызывает API
Незнакомый B24Client можно исследовать до запуска его операций: узнать методы, параметры и класс объявления.
- 1Уже созданный B24Client↓
В учебной модели конструктор только сохранил portal = north; запросов ещё нет.
- 2ReflectionClass → описания методов
getMethods выбирает public; диагностический код отдельно пропускает конструктор.
- listMethod: 2 обязательных из 3
parameters, method и необязательный maxPages. Метод объявлен в B24Client.
- oneMethod: 2 обязательных из 2
parameters и method. Найдено описание метода, а не его результат.
- 3На экране — структура, без вызова API
Тела oneMethod и listMethod не выполнялись; private-секреты не читались.
Reflection не показывает историю запросов и локальные переменные завершённого вызова. invoke запускает метод, а создание экземпляра атрибута выполняет его конструктор; в этом исследовании таких шагов нет.
Для неизвестного клиента начните с публичных методов и параметров, а не с дампа всех полей: там могут находиться токены. Reflection описывает типы и видимость, но не заменяет документацию о единицах, побочных эффектах и отказах.
<?php
declare(strict_types=1);
class B24Client
{
public function listMethod(array $parameters, string $method): array { return []; }
public function portalName(): string { return 'Учебный портал'; }
private function credentials(): string { return 'не выводить'; }
}
$class = new ReflectionClass(B24Client::class);
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
echo $method->getName(), '(', $method->getNumberOfRequiredParameters(), " обязательных)\n";
}
Ожидаемый результат
listMethod(2 обязательных)
portalName(0 обязательных)
IS_PUBLIC ограничивает список. Здесь количество нужно для практической подсказки обязательных входов; значения свойств не читаются и методы не запускаются. В большой библиотеке список может включать наследование, поэтому дополнительно исследуйте getDeclaringClass и документацию.
Атрибут хранит метку; обработчик придаёт ей действие
Хотим подписывать экспортируемые классы понятным именем. Атрибут — структурированная метка у объявления. Запись #[...] сама не строит меню и не запускает HTTP-маршрут; нужно прочитать её и решить, что сделать.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_CLASS)]
class ReportTitle
{
public function __construct(public string $text) {}
}
#[ReportTitle('Каталог товаров')]
class ProductReport {}
$class = new ReflectionClass(ProductReport::class);
$description = $class->getAttributes(ReportTitle::class)[0];
$title = $description->newInstance();
echo $title->text, "\n";
Ожидаемый результат
Каталог товаров
#[Attribute(...)] помечает ReportTitle как класс атрибута, TARGET_CLASS разрешает его у класса. #[ReportTitle(...)] сохраняет аргумент метки. getAttributes получает описание; newInstance создаёт объект ReportTitle и проверяет договор его применения. Существование метки и выполнение её конструктора — разные этапы.
| Запись в примере | Что делает каждый элемент |
|---|---|
#[ReportTitle('Каталог товаров')] | Метка и аргумент перед объявлением; это не вызов метода отчёта. |
getAttributes(ReportTitle::class) | Выбрать описания нужного вида. |
newInstance() | Создать объект метаданных; на этом шаге могут проявиться ошибки аргументов или цели. |
Небольшая карта маршрутов из разрешённых меток
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(public string $path) {}
}
class CatalogController
{
#[Route('/products')]
public function index(): string { return 'Каталог товаров'; }
}
$controller = new ReflectionClass(CatalogController::class);
foreach ($controller->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
foreach ($method->getAttributes(Route::class) as $attribute) {
$route = $attribute->newInstance();
echo $route->path, ' → ', $method->getName(), "\n";
}
}
Ожидаемый результат
/products → index
Это реально выполненное построение карты, не готовый HTTP-роутер. Метка описывает путь; диспетчер запроса ещё должен сопоставить HTTP-метод, проверить доступ и вызвать обработчик. При двух одинаковых путях карта должна сообщить конфликт, а не молча выбрать последний.
- Reflection перечисляет публичные методы CatalogController
- У index находит описание Route
- newInstance получает path=/products
- Сборщик связывает /products с index
- Во время HTTP отдельный диспетчер использует эту карту
Проверить допустимую цель и данные при сборке
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(public string $path)
{
if (!str_starts_with($path, '/')) { throw new InvalidArgumentException('Путь должен начинаться с /'); }
}
}
class BadController
{
#[Route('products')]
public function index(): void {}
}
$method = new ReflectionMethod(BadController::class, 'index');
try { $method->getAttributes(Route::class)[0]->newInstance(); }
catch (InvalidArgumentException $error) { echo 'Карта не собрана: ', $error->getMessage(), "\n"; }
Ожидаемый результат
Карта не собрана: Путь должен начинаться с /
Проверка в конструкторе атрибута запускается при newInstance. Описание можно получить до этого; просто обнаружить метку недостаточно для валидации. Повторяемые атрибуты требуют IS_REPEATABLE, а обработчик должен определить смысл нескольких правил.
Создать без конструктора — принять на себя его ответственность
<?php
declare(strict_types=1);
class Order
{
public function __construct(public readonly string $id)
{
if ($id === '') { throw new InvalidArgumentException('Нужен номер'); }
}
}
$description = new ReflectionClass(Order::class);
$order = $description->newInstanceWithoutConstructor();
try { echo 'Накладная для ', $order->id; }
catch (Error $error) { echo "не готова: номер не инициализирован\n"; }
Ожидаемый результат
Накладная для не готова: номер не инициализирован
newInstanceWithoutConstructor пропускает заполнение и проверки конструктора. Такой низкоуровневый приём нужен некоторым ORM и средствам восстановления, но они обязаны проверить структуру и заполнить состояние сами. Для обычной формы безопаснее явная фабрика из проверенных данных.
Сканировать только доверенные классы и понятные метки
Не принимайте имя класса атрибута или контроллера прямо из URL для произвольного создания. Карта приложения строится из известных классов. Конструктор атрибута может выполнять код; держите его лёгким и без HTTP/БД. Reflection не обнаруживает бизнес-обещания, скрытые за __call, и не делает закрытые данные безопасными для вывода.
Попробуйте. Добавьте метод export с #[Route("/products/export")], проверьте две строки карты.
Разобрать решение
Создайте второй публичный метод и метку. Для проверки конфликтов складывайте пути в ассоциативный массив и перед записью проверяйте уже существующий ключ. В рабочем роутере ключ обычно включает и HTTP-метод.
Кэшировать договор класса, а не состояние пользователя
Карта путей и правил обычно меняется при выпуске кода. Её можно построить на этапе сборки и сохранить с версией приложения. Не кэшируйте под видом метаданных текущего пользователя, токен клиента и его доступы. Reflection не обязательно является узким местом; измеряйте реальную стоимость, прежде чем усложнять кэш.
Дальше: полный договор языка и специальные случаи
Далее раскрыты жизненный цикл атрибутов, фильтры семейств, все TARGET_*, promoted-параметры и встроенные атрибуты совместимости/диагностики. Они сохраняют полный договор, который первые метки только вводят.
Разделы ниже сохраняют специальные правила официального PHP Manual: Прочитать метаданные и создать объект атрибута — разные шаги, Выбрать атрибуты по общему договору, SensitiveParameter: скрыть аргумент в трассировке — PHP 8.2+, Предупредить об устаревшем вызове и забытом результате, Атрибуты совместимости: не превращать временный мост в норму, Override у свойства и отложенная проверка цели — PHP 8.5+, Аргумент атрибута может быть объектом: когда выполняется new, Найти класс атрибута: короткое имя, alias и полный путь, Где можно поставить атрибут: полный набор TARGET_*, Одна promoted-запись — атрибут у параметра и у свойства, PHP 8.5: предупредить об устаревшем трейте и константе.
Прочитать метаданные и создать объект атрибута — разные шаги
Документатор хочет узнать название экспортируемого поля, не вызывая действие экспорта. У атрибута есть два уровня: описание, которое вернул Reflection, и объект класса метаданных. После первого атрибута проверим разницу счётчиком вызовов конструктора.
- У export объявлен ExportHeading(title: DEFAULT)
- Reflection читает имя и аргумент Заказы
- Счётчик конструктора остаётся 0
- newInstance создаёт ExportHeading и увеличивает счётчик до 1
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
class ExportHeading
{
public const DEFAULT = 'Заказы';
public static int $constructed = 0;
public function __construct(public string $title)
{
self::$constructed++;
}
}
class OrderDocument
{
#[ExportHeading(title: ExportHeading::DEFAULT)]
public function export(): void
{
throw new LogicException('Экспорт в этом опыте не вызывается');
}
}
$method = new ReflectionMethod(OrderDocument::class, 'export');
$attribute = $method->getAttributes(ExportHeading::class)[0];
echo $attribute->getName(), "\n";
echo $attribute->getArguments()['title'], "\n";
echo 'Создано до newInstance: ', ExportHeading::$constructed, "\n";
$metadata = $attribute->newInstance();
echo $metadata->title, "\n";
echo 'Создано после newInstance: ', ExportHeading::$constructed, "\n";
Ожидаемый результат
ExportHeading
Заказы
Создано до newInstance: 0
Заказы
Создано после newInstance: 1
Title: — именованный аргумент конструктора атрибута; DEFAULT — константное выражение. Метод export не вызывается ни одним шагом. NewInstance запускает конструктор ExportHeading и проверяет возможность такого применения; это уже выполнение нашего кода метаданных.
Атрибут начинается #[ и заканчивается ]; несколько разных записей можно поместить рядом или разделить запятыми внутри одной пары. Имя разрешается по обычным правилам namespace/use. Короткое имя из другого namespace без правильного use не становится нужным классом автоматически.
В этом опыте аргументы — простая константа, поэтому getArguments не создаёт объект ExportHeading. Не расширяйте наблюдение до «Reflection никогда ничего не выполняет»: загрузка классов, вычисление поддерживаемых выражений аргументов и newInstance имеют свои эффекты. Не загружайте произвольные имена классов из запроса как якобы безопасные метаданные.
Повторяемость и допустимое место применения задаются в #[Attribute(...)]. Для метки свойства с повторами используется TARGET_PROPERTY | IS_REPEATABLE; следующий опыт отдельно показывает ошибки цели и повторения. Флаг | объединяет битовые возможности: это не оператор выбора одного случайного варианта.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
class ExportOnly
{
public function __construct(public string $name) {}
}
#[ExportOnly('Ошибка места')]
class WrongExportTarget {}
class RepeatedExportTarget
{
#[ExportOnly('Первая'), ExportOnly('Вторая')]
public function run(): void {}
}
$targets = [
new ReflectionClass(WrongExportTarget::class),
new ReflectionMethod(RepeatedExportTarget::class, 'run'),
];
foreach ($targets as $target) {
try {
$target->getAttributes(ExportOnly::class)[0]->newInstance();
} catch (Error $error) {
echo "Некорректная схема атрибута отклонена\n";
}
}
Ожидаемый результат
Некорректная схема атрибута отклонена
Некорректная схема атрибута отклонена
Пользовательский ExportOnly разрешён только у методов и не помечен IS_REPEATABLE. Описания синтаксически прочитались, но создание объектов метаданных остановлено. У ряда встроенных атрибутов PHP дополнительно проверяет правила уже при компиляции; не переносите время проверки этого опыта на все атрибуты.
Фреймворку лучше проверить собственную известную конфигурацию при запуске или сборке кэша и показать понятную ошибку разработчику. Ошибка схемы маршрутов — проблема приложения, а не повод молча считать маршрут отсутствующим. Для пользовательского ввода используйте отдельную валидацию данных.
Выбрать атрибуты по общему договору
У поля экспорта может быть подпись и единица измерения. Обработчику нужны обе разновидности метаданных, но не любой атрибут проекта. Флаг ReflectionAttribute::IS_INSTANCEOF позволяет искать по общему интерфейсу или родителю; без этого флага фильтр имени ищет конкретный класс.
- weight помечен ColumnTitle и ColumnUnit
- Оба класса выполняют ExportHint
- IS_INSTANCEOF находит оба описания
- Обработчик получает подпись Вес и единицу кг
<?php
declare(strict_types=1);
interface ExportHint
{
public function text(): string;
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class ColumnTitle implements ExportHint
{
public function __construct(private string $value) {}
public function text(): string { return 'Подпись: ' . $this->value; }
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class ColumnUnit implements ExportHint
{
public function __construct(private string $value) {}
public function text(): string { return 'Единица: ' . $this->value; }
}
class ExportRow
{
#[ColumnTitle('Вес'), ColumnUnit('кг')]
public int $weight = 2;
}
$property = new ReflectionProperty(ExportRow::class, 'weight');
$attributes = $property->getAttributes(ExportHint::class, ReflectionAttribute::IS_INSTANCEOF);
foreach ($attributes as $attribute) {
echo $attribute->newInstance()->text(), "\n";
}
Ожидаемый результат
Подпись: Вес
Единица: кг
Оба класса явно объявлены атрибутами и выполняют ExportHint. Повторяемости не требуется: на свойстве стоят разные классы по одному разу. Обработчик работает с известными разработчику метаданными, а не вызывает методы случайного класса из HTTP-ввода.
Точный фильтр и IS_INSTANCEOF решают разные задачи. Если движок понимает только ColumnTitle, широкий поиск по общему предку может принять неожиданный тип. Если новые типы должны расширять семейство, интерфейс делает точку расширения явной. Не вызывайте повторный Reflection/newInstance в цикле каждой строки большого экспорта: подготовьте описание колонок один раз, с продуманным обновлением кэша.
SensitiveParameter: скрыть аргумент в трассировке — PHP 8.2+
Ошибка авторизации может попасть в трассировку с аргументами функции. Атрибут SensitiveParameter заменяет представление помеченного аргумента оболочкой SensitiveParameterValue. Это конкретная защита диагностического пути, а не шифрование токена и не очистка каждого лога.
- В rejectDemoToken передана вымышленная строка
- Функция выбрасывает исключение
- Помеченный аргумент в trace заменён SensitiveParameterValue
- Проверка видит оболочку и не выводит значение
<?php
declare(strict_types=1);
function rejectDemoToken(#[SensitiveParameter] string $token): void
{
throw new RuntimeException('Учебный отказ');
}
$previous = ini_set('zend.exception_ignore_args', '0');
try {
try {
rejectDemoToken('DEMO-NOT-A-REAL-TOKEN');
} catch (RuntimeException $error) {
$argument = $error->getTrace()[0]['args'][0];
echo $argument instanceof SensitiveParameterValue
? "Аргумент трассировки скрыт оболочкой\n"
: "Аргумент не скрыт\n";
}
} finally {
if ($previous !== false) {
ini_set('zend.exception_ignore_args', $previous);
}
}
Ожидаемый результат
Аргумент трассировки скрыт оболочкой
Только для опыта включены аргументы трассировки, затем настройка восстановлена. Используется вымышленная строка. Мы проверяем наличие SensitiveParameterValue, не пытаясь извлечь её содержимое. В рабочей среде отключённые аргументы трассировки могут быть предпочтительнее для всего приложения.
Если внутри функции написать токен в текст исключения или передать его в незащищённый логгер, этот атрибут не перепишет созданную строку. Он также не удаляет секрет из памяти и не маскирует автоматически поля JSON. Помечайте реальные параметры выполняемых функций и методов, через которые проходит секрет, и отдельно ограничивайте состав логов.
Обычному публичному ID заказа такая маркировка часто не нужна: диагностика потеряет полезную информацию. Сначала определите, какие данные секретны и какие вообще допустимы в журнале. Не отправляйте клиенту подробную трассировку только потому, что один аргумент скрыт.
Предупредить об устаревшем вызове и забытом результате
У библиотеки меняется API: oldTotal ещё работает, но новые места должны вызывать total. В другом месте функция возвращает результат проверки, который нельзя случайно выбросить. Для этих двух задач есть разные встроенные атрибуты. Они помогают разработчику заметить проблему, но не исправляют вызывающий код самостоятельно.
- Код вызывает oldTotal
- Deprecated выдаёт диагностический сигнал
- oldTotal передаёт работу total
- Вызывающий код получает 240 и задачу заменить старое имя
<?php
declare(strict_types=1);
function total(): int
{
return 240;
}
#[Deprecated(message: 'Используйте total()', since: '2.0')]
function oldTotal(): int
{
return total();
}
set_error_handler(function (int $level, string $message): bool {
if ($level !== E_USER_DEPRECATED) {
return false;
}
echo "Замечен устаревший вызов\n";
return true;
});
try {
echo oldTotal(), "\n";
} finally {
restore_error_handler();
}
Ожидаемый результат
Замечен устаревший вызов
240
Since — версия нашей библиотеки, PHP не проверяет её смысл. Обработчик предупреждения намеренно локальный и выводит устойчивое сообщение для теста. В приложении deprecation обычно направляют в журнал и исправляют вызов, а не прячут глобальным отключением error_reporting.
<?php
declare(strict_types=1);
#[NoDiscard('Нужно проверить возможность отправки')]
function canShip(int $stock): bool
{
return $stock > 0;
}
$warnings = 0;
set_error_handler(function (int $level, string $message) use (&$warnings): bool {
if ($level !== E_USER_WARNING || !str_contains($message, 'canShip')) {
return false;
}
$warnings++;
return true;
});
try {
canShip(0);
$allowed = canShip(0);
echo $allowed ? "Можно отправить\n" : "Отправка запрещена\n";
(void) canShip(1);
} finally {
restore_error_handler();
}
echo 'Потерянных результатов: ', $warnings, "\n";
Ожидаемый результат
Отправка запрещена
Потерянных результатов: 1
Первый результат не использован и выдаёт предупреждение. Второй участвует в решении. Последняя строка с (void) явно отбрасывает результат и подавляет предупреждение: это демонстрация синтаксиса PHP 8.5, а не совет игнорировать реальную проверку склада.
NoDiscard на интерфейсном или абстрактном методе не навешивается автоматически на реализующий метод; значимо объявление реально вызванной реализации. Переопределение тоже требует собственной маркировки. Метод, импортированный из трейта, сохраняет атрибут. Присвоить результат переменной и затем ничего не сделать с ней всё ещё можно — смысловой анализ вашего решения PHP этим атрибутом не выполняет.
Deprecated появился в PHP 8.4; PHP 8.5 расширил его применение на трейты и compile-time константы вне класса. NoDiscard и приведение (void) — PHP 8.5. На старом PHP само неизвестное имя атрибута может разбираться без нужного эффекта, а новый синтаксис (void) несовместим: не считайте отсутствие parse error доказательством работающей защиты.
Атрибуты совместимости: не превращать временный мост в норму
При обновлении старой библиотеки иногда нужно сохранить прежний договор и поэтапно исправить объявления. AllowDynamicProperties и ReturnTypeWillChange существуют для конкретных случаев совместимости. Они не делают любую структуру хорошей и не заменяют переход на явную модель.
- Старая модель принимает свободные поля
- AllowDynamicProperties допускает запись без deprecation
- Опечатки всё ещё принимаются
- Следующий шаг — определить схему и перейти на явные поля
<?php
declare(strict_types=1);
#[AllowDynamicProperties]
class LegacyImportRow {}
$row = new LegacyImportRow();
$row->title = 'Чай';
$row->warehouse = 'A';
echo $row->title, ' / ', $row->warehouse, "\n";
echo property_exists($row, 'warehouse') ? "Поле создано\n" : "Поля нет\n";
Ожидаемый результат
Чай / A
Поле создано
Атрибут убирает deprecation именно создания динамических полей этого класса; это не проверка разрешённых имён. Свободная опечатка тоже будет принята. Для нового импорта обычно яснее массив со схемой или DTO с объявленными свойствами; stdClass уже допускает динамические свойства без такой пометки.
<?php
declare(strict_types=1);
class LegacyLineCount implements Countable
{
#[ReturnTypeWillChange]
public function count()
{
return count(['Чай', 'Чашка']);
}
}
class LineCount implements Countable
{
public function count(): int
{
return count(['Чай', 'Чашка']);
}
}
echo 'Старый договор: ', count(new LegacyLineCount()), " строки\n";
echo 'Современный договор: ', count(new LineCount()), " строки\n";
Ожидаемый результат
Старый договор: 2 строки
Современный договор: 2 строки
Обе реализации считают один учебный список. ReturnTypeWillChange временно подавляет предупреждение о tentative return type встроенного Countable; он не меняет результат. Для PHP 8.2+ следует объявить count(): int. В пользовательском наследовании атрибут не разрешает несовместимую сигнатуру.
ReturnTypeWillChange не разрешает нарушать тип результата пользовательского родителя: несовместимость там остаётся ошибкой. И сам временный режим встроенных методов не обещан навечно. Если библиотека уже поддерживает нужный тип, исправьте сигнатуру вместо бессрочного подавления предупреждения.
AllowDynamicProperties влияет и на потомков помеченного класса. Поэтому атрибут на общей базе может скрыть опечатки во всём дереве. Перед массовой маркировкой найдите места записи, определите схему и проверьте данные. Пример обнаружения опечатки находится в разборе динамических свойств.
Override у свойства и отложенная проверка цели — PHP 8.5+
В Override у метода атрибут ловит опечатку при переопределении. С PHP 8.5 тот же контроль доступен свойствам: разработчик явно заявляет, что продолжает существующий договор, а не добавляет случайное новое имя.
- Родитель объявил format
- Потомок пометил свой format атрибутом Override
- PHP находит соответствующее имя
- При опечатке объявление не пройдёт проверку
<?php
declare(strict_types=1);
class ExportSettings
{
public string $format = 'csv';
}
class JsonExportSettings extends ExportSettings
{
#[Override]
public string $format = 'json';
}
echo (new JsonExportSettings())->format, "\n";
Ожидаемый результат
json
Если написать другое имя свойства, PHP обнаружит отсутствие соответствующего объявления родителя при компиляции. В этом опыте тип и видимость совместимы. Override не делает несовместимое изменение типа допустимым и не означает, что значение readonly разрешено менять.
Отдельный DelayedTargetValidation предназначен прежде всего для совместимости библиотек с изменяющимися целями встроенных атрибутов. Он переносит ошибку неверного места применения на newInstance. Это продвинутый инструмент для автора библиотеки; при обычной опечатке нужно исправить место атрибута, а не отодвинуть ошибку.
<?php
declare(strict_types=1);
class CompatibilityProbe
{
#[DelayedTargetValidation]
#[Override]
public const FORMAT = 'csv';
}
echo CompatibilityProbe::FORMAT, "\n";
$constant = new ReflectionClassConstant(CompatibilityProbe::class, 'FORMAT');
try {
$constant->getAttributes(Override::class)[0]->newInstance();
} catch (Error $error) {
echo "Override нельзя применять к этой константе\n";
}
Ожидаемый результат
csv
Override нельзя применять к этой константе
Это намеренно неправильное место Override, изолированное для исследования механизма. Объявление допускается благодаря DelayedTargetValidation, но Reflection всё равно отвергает неподдерживаемую цель. Такой код не является полезной настройкой формата экспорта — он показывает границу совместимости.
DelayedTargetValidation затрагивает только проверку цели встроенных атрибутов. Он не подавит функциональное требование Override, если обычный метод помечен как переопределение, но родительского метода нет. Override нельзя ставить на конструктор. Делать все проверки отложенными «на всякий случай» означает откладывать обнаружение ошибки до редкого пути исполнения.
Аргумент атрибута может быть объектом: когда выполняется new
Настройке поля импорта нужен диапазон количества от 1 до 10. Вместо двух непонятных чисел положим в атрибут маленький объект правила QuantityRange. Сначала нужны разница getArguments и newInstance и создание объекта.
- Reflection получает описание: создано 0 QuantityRange
- Первый getArguments вычисляет new QuantityRange(1, 10)
- Повторное чтение создаёт другой объект правила
- NewInstance снова вычисляет аргумент и создаёт сам атрибут
С PHP 8.1 запись new ИмяКласса(...) допускается в аргументах атрибута. Имя класса здесь фиксировано; динамическое имя, анонимный класс и распаковка ... в этом initializer не разрешены. Объявление не запускает конструктор сразу. Когда именно появится объект, проверим счётчиком; такой счётчик нужен для наблюдения, а не как полезная часть правила.
<?php
declare(strict_types=1);
class QuantityRange
{
public static int $created = 0;
public function __construct(public int $min, public int $max)
{
self::$created++;
}
public function allows(int $quantity): bool
{
return $quantity >= $this->min && $quantity <= $this->max;
}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class QuantityCheck
{
public function __construct(public QuantityRange $range) {}
}
class ImportLine
{
#[QuantityCheck(new QuantityRange(1, 10))]
public int $quantity = 3;
}
$description = (new ReflectionProperty(ImportLine::class, 'quantity'))
->getAttributes(QuantityCheck::class)[0];
echo 'До чтения: ', QuantityRange::$created, "\n";
$first = $description->getArguments()[0];
$second = $description->getArguments()[0];
echo 'После двух чтений: ', QuantityRange::$created, "\n";
echo $first === $second ? "Общий объект\n" : "Два разных правила\n";
$check = $description->newInstance();
echo 'После newInstance: ', QuantityRange::$created, "\n";
echo $check->range->allows(3) ? "Количество 3 допустимо\n" : "Ошибка\n";
echo $check->range->allows(11) ? "Принято\n" : "Количество 11 отклонено\n";
Ожидаемый результат
До чтения: 0
После двух чтений: 2
Два разных правила
После newInstance: 3
Количество 3 допустимо
Количество 11 отклонено
GetAttributes возвращает описание. GetArguments уже вычисляет выражения аргументов, поэтому new QuantityRange выполняется при каждом чтении. NewInstance создаёт QuantityCheck с новым аргументом. Нельзя считать любое чтение Reflection лишённым побочных эффектов.
У правила нет сети и файлов. Конструктор метаданных лучше делать дешёвым и предсказуемым: Reflection может вызываться при построении документации, тестах и прогреве кэша. Для рабочего импорта один раз прочитайте известную схему, затем применяйте правило к строкам. Сам атрибут не проверяет quantity автоматически — в опыте это делает явный вызов allows.
Попробуйте сами. Почему установка quantity = 11 сама по себе не бросает ошибку?
Разбор решения
Это обычное свойство, а атрибут — описание правила. Потребитель должен прочитать схему и вызвать allows либо валидатор. Если проверка должна происходить при каждой записи, нужен другой механизм — операция объекта или подходящий setter.
Найти класс атрибута: короткое имя, alias и полный путь
Модуль экспорта и модуль импорта могут оба назвать атрибут Column. Пространство имён убирает неоднозначность. Use выбирает, какое полное имя означает короткая запись; файл оно само не подключает. Базовая запись namespace разобрана в отдельной главе.
- Класс объявлен как LibraryMeta\Column
- Use даёт ему короткое имя Heading и namespace-alias Meta
- Heading, Meta\Column и \LibraryMeta\Column разрешаются в один класс
- Незнакомое Column без use остаётся именем ExportArea\Column
| Форма | Как читать |
|---|---|
| #[Heading(...)] | Короткое импортированное имя класса. |
| #[Meta\Column(...)] | Квалифицированное имя через alias пространства имён. |
| #[\LibraryMeta\Column(...)] | Полностью квалифицированное имя: начальный обратный слеш начинает путь от корня. |
| #[Column(...)] без use | В текущем namespace это иной класс; PHP не ищет все похожие имена в проекте. |
<?php
declare(strict_types=1);
namespace LibraryMeta {
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Column
{
public function __construct(public string $title) {}
}
}
namespace ExportArea {
use LibraryMeta\Column as Heading;
use LibraryMeta as Meta;
class ItemRow
{
#[Heading('Артикул')]
public string $sku = 'TEA-1';
#[Meta\Column('Название')]
public string $title = 'Чай';
#[\LibraryMeta\Column('Остаток')]
public int $stock = 3;
#[Column('Нет такого класса в ExportArea')]
public string $mistake = '';
}
foreach (['sku', 'title', 'stock'] as $field) {
$description = (new \ReflectionProperty(ItemRow::class, $field))->getAttributes()[0];
echo $description->getName(), ': ', $description->newInstance()->title, "\n";
}
try {
(new \ReflectionProperty(ItemRow::class, 'mistake'))->getAttributes()[0]->newInstance();
} catch (\Error $error) {
echo "Для Column не указан нужный use\n";
}
}
Ожидаемый результат
LibraryMeta\Column: Артикул
LibraryMeta\Column: Название
LibraryMeta\Column: Остаток
Для Column не указан нужный use
Обе namespace-группы находятся в одном самостоятельном example.php. Фигурные скобки ограничивают область каждого пространства. Начальный слеш у Attribute/ReflectionProperty/Error выбирает встроенные глобальные классы. Неверное имя можно прочитать как описание; ошибка возникла при попытке создать его объект.
В многофайловом проекте классы должны быть доступны через require или настроенную автозагрузку. Исправленный use не загрузит отсутствующий файл сам. Для фильтра getAttributes тоже передавайте правильный ::class; строка «Column» не является поиском по последней части имени.
Попробуйте сами. Замените Heading на FieldCaption. Нужно ли переименовывать класс LibraryMeta\Column?
Разбор решения
Нет. Изменяются alias в use и применение атрибута. Сам класс и его полное имя остаются прежними; getName продолжит выводить LibraryMeta\Column.
Где можно поставить атрибут: полный набор TARGET_*
Генератор документации помечает происхождение разных частей схемы: класса, свойства, параметра и константы. Чтобы опечатка в месте применения обнаруживалась, класс атрибута задаёт разрешённые цели. Отдельно выбирают, можно ли повторять одну и ту же метку. Эти вопросы не следует смешивать.
- Класс SchemaLabel объявляет допустимые TARGET_*
- Автор ставит метку у конкретного объявления
- Reflection описывает реальную цель через getTarget
- NewInstance проверяет цель и создаёт метаданные
| Флаг | Цель |
|---|---|
| TARGET_CLASS | Объявление класса, интерфейса, трейта или enum. |
| TARGET_FUNCTION | Функция, включая замыкание. |
| TARGET_METHOD | Метод класса. |
| TARGET_PROPERTY | Свойство. |
| TARGET_CLASS_CONSTANT | Константа класса; сюда относятся case перечисления. |
| TARGET_PARAMETER | Параметр функции или метода. |
| TARGET_CONSTANT — PHP 8.5+ | Константа const вне класса, включая namespace. |
| TARGET_ALL | Все поддерживаемые этой версией места; повторяемость не включена. |
| IS_REPEATABLE | Дополнительное разрешение повторить тот же атрибут на одной цели; это не отдельная цель. |
Оператор | объединяет флаги. Без аргумента #[Attribute] использует TARGET_ALL. Сам встроенный класс Attribute имеет конструктор __construct(int $flags = Attribute::TARGET_ALL) и свойство flags; это описание политики другого класса атрибута. Не используйте числовые значения флагов вручную. Следующий файл целиком требует PHP 8.5 из-за атрибута глобальной константы.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
class SchemaLabel
{
public function __construct(public string $text) {}
}
#[SchemaLabel('глобальная константа')]
const EXPORT_SCHEMA_VERSION = 2;
#[SchemaLabel('функция')]
function prepareExport(#[SchemaLabel('параметр')] int $limit): int
{
return $limit;
}
#[SchemaLabel('класс')]
#[SchemaLabel('схема документа')]
class ExportSchema
{
#[SchemaLabel('свойство')]
public string $title = 'Заказы';
#[SchemaLabel('константа класса')]
public const FORMAT = 'csv';
#[SchemaLabel('метод')]
public function build(): void {}
}
$targets = [
new ReflectionClass(ExportSchema::class),
new ReflectionFunction('prepareExport'),
new ReflectionMethod(ExportSchema::class, 'build'),
new ReflectionProperty(ExportSchema::class, 'title'),
new ReflectionClassConstant(ExportSchema::class, 'FORMAT'),
new ReflectionParameter('prepareExport', 'limit'),
new ReflectionConstant('EXPORT_SCHEMA_VERSION'),
];
foreach ($targets as $target) {
echo $target->getAttributes(SchemaLabel::class)[0]->newInstance()->text, "\n";
}
$definition = (new ReflectionClass(SchemaLabel::class))->getAttributes(Attribute::class)[0]->newInstance();
echo ($definition->flags & Attribute::TARGET_CONSTANT) !== 0 ? "Глобальный const разрешён\n" : "Запрещён\n";
echo count((new ReflectionClass(ExportSchema::class))->getAttributes(SchemaLabel::class)), " метки класса\n";
Ожидаемый результат
класс
функция
метод
свойство
константа класса
параметр
глобальная константа
Глобальный const разрешён
2 метки класса
Массив targets здесь только собирает семь описаний для общего цикла. Getter getAttributes на каждом описании выполняет одну понятную задачу. Маску разрешённых мест читаем у #[Attribute] класса SchemaLabel; это не то же самое, что getTarget у конкретного применения. Две метки на ExportSchema допустимы благодаря IS_REPEATABLE.
Для настоящего валидатора свойства лучше TARGET_PROPERTY, а для маршрута — TARGET_METHOD: TARGET_ALL выбран здесь ради обзорного опыта. Значение flags полученного объекта метаданных можно прочитать, но изменение этого объекта не переписывает объявление класса в исходнике. #[Attribute] объявляет именно конкретный класс атрибута: в PHP 8.5 попытка поставить его на абстрактный класс, enum, интерфейс или трейт завершается ошибкой уже при компиляции. Это не запрещает ставить на такие объявления обычные пользовательские атрибуты с TARGET_CLASS.
Попробуйте сами. Какая маска нужна метке, разрешённой только у функции и метода, с возможностью повтора?
Разбор решения
Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE. Если поставить такую метку на свойство, описание может прочитаться, но newInstance отвергнет цель. Для встроенных атрибутов отдельные проверки могут происходить раньше.
Одна promoted-запись — атрибут у параметра и у свойства
Входной документ создаётся через конструктор: public string $sku одновременно принимает артикул и объявляет поле. Если перед этой записью стоит атрибут, Reflection увидит его в двух местах. Валидатор входа может смотреть параметры, а построитель формы — свойства. Им нужно согласовать, кто и что обрабатывает. Синтаксис promotion объяснён в главе конструктора.
- #[FieldLabel] стоит перед public string $sku
- У конструктора появляется параметр sku с меткой
- У объекта появляется свойство sku с меткой
- Каждый потребитель выбирает свою цель; две копии не означают повторную валидацию автоматически
Для атрибута, который нужно создавать через обе точки Reflection, разрешите TARGET_PARAMETER | TARGET_PROPERTY. Копирование описания не делает запрещённую цель разрешённой. Вторая половина опыта намеренно задаёт только TARGET_PROPERTY и показывает разницу.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
class FieldLabel
{
public function __construct(public string $title) {}
}
class ProductInput
{
public function __construct(#[FieldLabel('Артикул')] public string $sku) {}
}
$parameter = new ReflectionParameter([ProductInput::class, '__construct'], 'sku');
$property = new ReflectionProperty(ProductInput::class, 'sku');
echo 'Параметр: ', $parameter->getAttributes(FieldLabel::class)[0]->newInstance()->title, "\n";
echo 'Свойство: ', $property->getAttributes(FieldLabel::class)[0]->newInstance()->title, "\n";
#[Attribute(Attribute::TARGET_PROPERTY)]
class StoredField {}
class StorageInput
{
public function __construct(#[StoredField] public string $sku) {}
}
(new ReflectionProperty(StorageInput::class, 'sku'))->getAttributes(StoredField::class)[0]->newInstance();
echo "Метка свойства создана\n";
try {
(new ReflectionParameter([StorageInput::class, '__construct'], 'sku'))
->getAttributes(StoredField::class)[0]->newInstance();
} catch (Error $error) {
echo "Та же метка не разрешена для параметра\n";
}
Ожидаемый результат
Параметр: Артикул
Свойство: Артикул
Метка свойства создана
Та же метка не разрешена для параметра
В обоих случаях описания скопированы. У FieldLabel разрешены обе цели; у StoredField только свойство, поэтому ошибка появляется при newInstance описания параметра. Это не повторяемость: IS_REPEATABLE относится к нескольким применениям на одной цели, а здесь цели две.
Не обходите одновременно параметры и свойства без своей политики: иначе одна бизнес-проверка может выполниться дважды. Если метка относится только к хранению, обрабатывайте свойства или объявите поле отдельно от параметра. Значение по умолчанию promoted-параметра, в отличие от атрибута, относится к параметру; не делайте из дублирования метаданных вывод о двух независимо заданных default-значениях.
Попробуйте сами. Как исключить метку из Reflection-параметра, если она нужна только полю?
Разбор решения
Объявить свойство с атрибутом отдельно, принять обычный немодифицированный параметр конструктора и присвоить его полю в теле. Это чуть длиннее, зато две роли явно разделены.
PHP 8.5: предупредить об устаревшем трейте и константе
Библиотека постепенно меняет формат экспорта. Старый трейт ещё нужен совместимым клиентам, а старый размер пакета заменён новой настройкой. Deprecated позволяет заметить использование, не удаляя возможность сразу. Перед опытом нужны подключение трейта через use и Deprecated у функции.
- LegacyExportName объявлен устаревшим
- Обработчик установлен до подключения файла потребителя
- При объявлении класса с use PHP выдаёт deprecation
- Старый метод продолжает работать; новый код использует явный метод
У трейта предупреждение связано с его включением в объявление класса. Поэтому файл old-export.php подключается через require уже после установки обработчика. Это два настоящих файла в ZIP, а не две программы, которые нужно склеить. Since обозначает версию нашей библиотеки; значение 2.0 не выбирает версию PHP.
<?php
declare(strict_types=1);
#[Deprecated(message: 'Объявите exportName() в своём классе', since: '2.0')]
trait LegacyExportName
{
public function exportName(): string
{
return 'orders.csv';
}
}
$notices = 0;
set_error_handler(function (int $level, string $message) use (&$notices): bool {
if ($level !== E_USER_DEPRECATED || !str_contains($message, 'LegacyExportName')) {
return false;
}
$notices++;
return true;
});
try {
require __DIR__ . '/old-export.php';
} finally {
restore_error_handler();
}
echo 'Предупреждений при подключении: ', $notices, "\n";
echo (new OldExport())->exportName(), "\n";
class CurrentExport
{
public function exportName(): string
{
return 'orders.csv';
}
}
echo (new CurrentExport())->exportName(), "\n";
Ожидаемый результат
Предупреждений при подключении: 1
orders.csv
orders.csv
Демо не записывает CSV: метод возвращает имя будущего файла. Предупреждение возникает при применении трейта, не при каждом вызове exportName. Обработчик перехватывает только известное deprecation и обязательно восстанавливается.
Для глобальной константы атрибут ставят перед const; помеченную константу объявляют отдельной инструкцией, без списка нескольких имён через запятую. Define создаёт константу вызовом функции: поставить над этим вызовом атрибут объявления нельзя. Константа может находиться и в namespace. В следующем самостоятельном файле читаем старое значение через constant в момент выполнения, чтобы установленный обработчик увидел событие.
<?php
declare(strict_types=1);
const CURRENT_IMPORT_BATCH = 50;
#[Deprecated(message: 'Используйте CURRENT_IMPORT_BATCH', since: '2.0')]
const LEGACY_IMPORT_BATCH = 20;
$notices = 0;
set_error_handler(function (int $level, string $message) use (&$notices): bool {
if ($level !== E_USER_DEPRECATED || !str_contains($message, 'LEGACY_IMPORT_BATCH')) {
return false;
}
$notices++;
return true;
});
try {
echo defined('LEGACY_IMPORT_BATCH') ? "Старое имя пока существует\n" : "Имя удалено\n";
echo 'Старый пакет: ', constant('LEGACY_IMPORT_BATCH'), "\n";
} finally {
restore_error_handler();
}
echo 'Событий устаревшего чтения: ', $notices, "\n";
echo 'Новый пакет: ', CURRENT_IMPORT_BATCH, "\n";
Ожидаемый результат
Старое имя пока существует
Старый пакет: 20
Событий устаревшего чтения: 1
Новый пакет: 50
Defined проверяет наличие; чтение constant использует значение и выдаёт deprecation. Новый размер выбран в условии задачи: Deprecated не меняет 20 на 50. Изменение размера реального импорта надо отдельно проверить на лимитах сервиса и памяти.
Эти формы требуют PHP 8.5. Атрибут функции/метода появился раньше, в PHP 8.4. Поставить тот же атрибут на обычный класс лишь потому, что у Deprecated есть TARGET_CLASS в маске, нельзя: для этой возможности допустим именно трейт. Маска — необходимая часть правил, но встроенный атрибут может проверять и смысл цели. Сообщение должно назвать замену; предупреждение без пути миграции оставляет читателю работу по угадыванию.
Попробуйте сами. Что произойдёт, если удалить LEGACY_IMPORT_BATCH, сохранив вызов constant?
Разбор решения
Это уже отсутствие константы и Error, а не мягкий сигнал об устаревании. Сначала найдите потребителей, обновите их и согласуйте несовместимый выпуск. Deprecated само ничего не удаляет по дате или версии since.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
class_existsПроверить, объявлен ли класс, при необходимости дав автозагрузчику найти его файл.
PHP 4+ · Core
class_exists(string $class, bool $autoload = true): boolПроверить, объявлен ли класс, при необходимости дав автозагрузчику найти его файл.
- Настройка выбрала CsvExporter::class
- class_exists проверяет таблицу уже объявленных типов
- Если имени нет и autoload=true, загрузчик получает имя и может объявить класс
| Вход / результат | Точный смысл |
|---|---|
| $class | Имя класса, включая namespace. Проверка объявленного имени регистронезависима; строки не получают импорт use автоматически. |
| $autoload | true по умолчанию: неизвестное имя передаётся зарегистрированным загрузчикам. false проверяет только уже объявленное. Конструктор функция не вызывает. |
| Результат | true для обычного, abstract или enum-класса; false для отсутствующего класса, интерфейса или трейта. Для enum есть более точная enum_exists. |
| Ошибки | Ошибки и исключения из autoloader проходят наружу. false при autoload=false не доказывает отсутствие PHP-файла/Composer-пакета. |
::class создаёт строку имени без new. Функция годится для диагностики необязательного пакета, но не доказывает совместимость API или возможность создания объекта.
<?php
declare(strict_types=1);
class CsvExporter
{
public function title(): string { return 'Выгрузка каталога CSV'; }
}
if (class_exists(CsvExporter::class, false)) {
echo (new CsvExporter())->title(), "\n";
}
echo class_exists('PdfExporter', false) ? "PDF доступен\n" : "PDF-экспортёр ещё не подключён\n";
Ожидаемый результат
Выгрузка каталога CSV
PDF-экспортёр ещё не подключён
false во втором аргументе проверяет уже загруженные определения без попытки автозагрузки. Поэтому отрицательный ответ не означает, что на диске нет файла класса. Не создавайте произвольный класс по имени из пользовательского запроса; допустимые варианты выбирают из своей конфигурации.
<?php
declare(strict_types=1);
$loader = static function (string $name): void {
if ($name === 'CsvExporter') {
require __DIR__ . '/CsvExporter.php';
}
};
spl_autoload_register($loader);
try {
echo class_exists('CsvExporter', false) ? "Определение уже загружено\n" : "Определение ещё не загружено\n";
echo class_exists('CsvExporter') ? "Автозагрузчик подключил экспортёр\n" : "Экспортёр не найден\n";
echo class_exists('CsvExporter', false) ? "Определение уже загружено\n" : "Определение ещё не загружено\n";
} finally {
spl_autoload_unregister($loader);
}
Ожидаемый результат
Определение ещё не загружено
Автозагрузчик подключил экспортёр
Определение уже загружено
Первый false запрещает загрузку, второй вызов по умолчанию разрешает её, третья проверка видит уже подключённое определение. Загружается конкретный известный файл из фикстуры. Конструктор экспортёра здесь не вызывался; автозагрузка классов и создание объектов — разные действия.
Абстрактный класс тоже существует, но new для него запрещён. Проверяйте контракт интерфейса отдельно. Если класс должен присутствовать обязательно, не маскируйте ошибку установки тихой веткой «ничего не делать». Сначала разберите vendor/autoload.php и PSR-4.
Рядом: interface_exists, trait_exists, enum_exists, is_a. Как выбрать проверку.
Дополнительные примеры из практической главы
Возвращает признак существования класса. По умолчанию может вызвать зарегистрированный autoloader; false запрещает этот шаг. Для интерфейсов и трейтов предназначены interface_exists и trait_exists.
<?php
declare(strict_types=1);
final class LocalExportAdapter {}
echo class_exists(LocalExportAdapter::class, false) ? "Загружен\n" : "Не загружен\n";
Ожидаемый результат
Загружен
autoload=false полезен для диагностики без побочного запуска загрузчика.
<?php
declare(strict_types=1);
$optionalClass = 'Demo\\OptionalPdfAdapter';
echo class_exists($optionalClass, false) ? "PDF доступен\n" : "PDF-адаптер не подключён\n";
Ожидаемый результат
PDF-адаптер не подключён
Боевой код должен отличать отсутствующую необязательную возможность от неправильного развёртывания обязательной зависимости.
Рядом: is_subclass_of, method_exists.
ReflectionClass::getPropertiesИсследовать объявления свойств класса.
PHP 8.2+ · Core
public function getProperties(?int $filter = null): arrayВозвращает ReflectionProperty для свойств, удовлетворяющих фильтру видимости/модификаторов. Без фильтра исследуется доступный Reflection набор объявлений; это не массив пользовательских значений.
<?php
declare(strict_types=1);
final class Input { public string $email; public int $quantity; private string $token; }
$reflection = new ReflectionClass(Input::class);
foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { echo $property->getName(), "\n"; }
Ожидаемый результат
email
quantity
Это описание полей, не доказательство их заполненности.
<?php
declare(strict_types=1);
final class SessionState
{
public string $name = 'Редактор';
private string $token = 'demo-only';
}
$class = new ReflectionClass(SessionState::class);
foreach ($class->getProperties(ReflectionProperty::IS_PRIVATE) as $property) {
echo 'Не экспортировать автоматически: ', $property->getName(), "\n";
}
Ожидаемый результат
Не экспортировать автоматически: token
IS_PRIVATE фильтрует объявления, getName возвращает только имя. Код не читает значение токена. Публичное представление следует задавать явным списком разрешённых полей: наличие public тоже не доказывает пригодность для ответа клиенту.
ReflectionProperty::isInitializedПроверить готовность типизированного свойства до чтения.
PHP 8.2+ · Core
public function isInitialized(?object $object = null): boolДля свойства экземпляра передайте соответствующий объект. Возвращает false для неинициализированного typed property; null является инициализированным значением nullable-поля.
<?php
declare(strict_types=1);
final class Row { public ?string $note = null; public int $id; }
$row = new Row();
echo (new ReflectionProperty(Row::class,'note'))->isInitialized($row) ? "note готов\n" : "Нет\n";
echo (new ReflectionProperty(Row::class,'id'))->isInitialized($row) ? "id готов\n" : "id не готов\n";
Ожидаемый результат
note готов
id не готов
Nullable и uninitialized — разные состояния.
<?php
declare(strict_types=1);
final class Imported { public string $sku; }
$item = new Imported();
$field = new ReflectionProperty(Imported::class, 'sku');
if (!$field->isInitialized($item)) { echo "Строка ещё не готова к экспорту\n"; }
$item->sku = 'TEA';
if ($field->isInitialized($item)) { echo 'Экспортировать: ', $item->sku, "\n"; }
Ожидаемый результат
Строка ещё не готова к экспорту
Экспортировать: TEA
isInitialized проверяет состояние именно этого объекта. Объявленное string-свойство без начального значения ещё нельзя читать. Для обычной модели обязательный sku проще потребовать конструктором; Reflection здесь показывает диагностику поэтапного загрузчика.
ReflectionMethod::getAttributesПрочитать метаданные конкретного метода.
PHP 8.2+ · Core
public function getAttributes(?string $name = null, int $flags = 0): arrayВозвращает ReflectionAttribute; можно фильтровать по имени или флагу IS_INSTANCEOF. Метод унаследован от ReflectionFunctionAbstract. Сами бизнес-действия атрибутами не выполняются.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)] final class Verb { public function __construct(public string $name) {} }
final class Api { #[Verb('POST')] public function create(): void {} }
$attributes=(new ReflectionMethod(Api::class,'create'))->getAttributes(Verb::class);
echo $attributes[0]->getArguments()[0], "\n";
Ожидаемый результат
POST
Это чтение описания; проверку реального HTTP-метода должен сделать роутер.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
final class InternalOnly {}
final class Pages
{
public function home(): string { return 'Каталог товаров'; }
}
$page = new Pages();
$method = new ReflectionMethod(Pages::class, 'home');
if ($method->getAttributes(InternalOnly::class) === []) {
echo 'Обычная страница: ', $page->home(), "\n";
}
Ожидаемый результат
Обычная страница: Каталог товаров
Пустой массив означает отсутствие этой метки. Решение принимает наш код после чтения метаданных. Это учебная классификация известных методов, не готовая авторизация: доступ пользователя должен проверяться отдельным правилом.
ReflectionAttribute::newInstanceСоздать объект атрибута и выполнить его конструктор.
PHP 8.2+ · Core
public function newInstance(): objectПередаёт объявленные аргументы конструктору класса атрибута и возвращает объект. Ошибки цели, повторяемости и аргументов могут проявиться на этом шаге.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)] final readonly class Limit { public function __construct(public int $value) {} }
final class Search { #[Limit(2)] public function run(): void {} }
$attribute=(new ReflectionMethod(Search::class,'run'))->getAttributes(Limit::class)[0];
$limit = $attribute->newInstance();
$products = ['Чай', 'Чашка', 'Кофе'];
echo 'Выдача: ', implode(', ', array_slice($products, 0, $limit->value)), "\n";
Ожидаемый результат
Выдача: Чай, Чашка
newInstance создаёт Limit(2), а array_slice использует его value. Атрибут сам не ограничивает массив: два шага связал потребитель метаданных. Следующий пример проверяет ошибку настройки в конструкторе атрибута.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)] final class PositiveLimit {
public function __construct(public int $value) { if ($value<1) { throw new InvalidArgumentException('Лимит'); } }
}
final class Feed { #[PositiveLimit(0)] public function list(): void {} }
$a=(new ReflectionMethod(Feed::class,'list'))->getAttributes(PositiveLimit::class)[0];
try { $a->newInstance(); } catch (InvalidArgumentException $e) { echo "Ошибка конфигурации\n"; }
Ожидаемый результат
Ошибка конфигурации
Такой тест запускают при сборке карты, чтобы не ждать первого запроса к редкому методу.
Источники и обсуждения
- Атрибуты
- Чтение атрибутов
- ReflectionClass
- Обход конструктора
- Публичные методы класса
- Описание параметров метода
- Число обязательных параметров
- Класс объявления метода
- Готовность типизированного свойства
- Файл объявления метода
- Строка объявления метода
- Синтаксис атрибутов
- Цели и повторяемость атрибутов
- Фильтрация атрибутов свойства
- ReflectionAttribute
- SensitiveParameter
- Оболочка скрытого значения
- Deprecated
- NoDiscard
- AllowDynamicProperties
- ReturnTypeWillChange
- Override для методов и свойств
- DelayedTargetValidation
- New в инициализаторах PHP 8.1
- Принятый RFC: вычисление new в атрибутах
- Имена внутри namespace
- Импорт и alias
- Все флаги Attribute и PHP 8.5
- Принятый RFC: атрибуты глобальных констант
- Атрибуты enum и case
- Принятый RFC: атрибуты при promotion
- Новые возможности PHP 8.5
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


