Глава 73 · От метки к проверяемому договору
Встроенные атрибуты: намерения, совместимость и защита диагностики
Все предопределённые атрибуты PHP, их цели, версии, конструкторы и SensitiveParameterValue с практическими примерами.
Атрибут: подписать объявление понятными программе метаданными
Атрибут описывает обработчик, а программа читает описание
Пометка Route хранит путь страницы рядом с функцией catalog.
- #[Route('/catalog')]
Метаданные прикреплены к объявлению функции.
- ReflectionFunction('catalog')
Программа исследует объявление.
- getAttributes(Route::class)
Выбирает нужный вид метаданных.
- newInstance()
Создаёт Route с переданным путём; конструктор выполняется здесь.
- $attribute->path
Маршрутизатор получает /catalog и применяет своё правило.
Пользовательская Route сама не обрабатывает HTTP. Встроенные атрибуты имеют отдельные, известные PHP правила.
Сначала нужна конкретная задача: метка маршрута, предупреждение об устаревшем вызове или защита аргумента в трассировке. Атрибуты решают разные задачи и появились в разных версиях. Пользовательский Route работает только потому, что наш читатель его обрабатывает; встроенные Override/Deprecated/NoDiscard обрабатывает сам PHP.
В сервисе есть обработчик страницы. Хочется записать рядом с ним путь /catalog, чтобы маршрутизатор прочитал его автоматически. Атрибут — структурированная пометка над объявлением. В отличие от обычного комментария его имя и аргументы можно прочитать через Reflection. Но пользовательский атрибут сам не запускает маршрутизатор: нужен код, который его использует.
#[Route("/catalog")] прикрепляет метаданные Route к следующему объявлению. Класс Route помечается встроенным #[Attribute(...)], чтобы PHP разрешил использовать его как атрибут. Его конструктор получает аргументы пометки, когда Reflection создаёт экземпляр. До первого примера: public string $path — открытое строковое свойство; $this->path = $path сохраняет параметр в объекте; newInstance создаёт атрибут и вызывает конструктор.
- Над catalog стоит #[Route("/catalog")]
- ReflectionFunction находит пометку
- ReflectionAttribute::newInstance() создаёт Route
- Код маршрутизатора читает path и решает, что делать
<?php
#[Attribute(Attribute::TARGET_FUNCTION)]
class Route {
public string $path;
public function __construct(string $path) { $this->path = $path; }
}
#[Route('/catalog')]
function catalog(): string { return 'Каталог'; }
$reflection = new ReflectionFunction('catalog');
$attribute = $reflection->getAttributes(Route::class)[0]->newInstance();
echo $attribute->path, "\n";
echo catalog(), "\n";
Ожидаемый результат
/catalog
Каталог
Reflection отдельно прочитал метаданные, а обычный catalog() отдельно выполнил функцию. Скобки [0] выбирают первую найденную пометку; в общем загрузчике проверьте наличие и допустимое количество. Не создавайте атрибуты из непроверенного кода: их конструкторы выполняются.
Встроенные атрибуты из этой главы имеют известный PHP смысл: Override проверяет переопределение, SensitiveParameter меняет трассировку и так далее. Это отличается от Route, который ничего не маршрутизирует без вашего обработчика. В namespace используйте полное имя вроде #[\Override] или импорт use Override;. Иначе похожее имя может обозначать другой, даже несуществующий класс.
Базовый синтаксис, повторение и чтение аргументов подробнее: атрибуты и Reflection. Встроенные классы атрибутов final: их не расширяют наследованием. Создание new Override() даёт объект метаданных и само по себе не прикрепляет пометку к методу.
Attribute: где разрешена пометка и можно ли её повторять
Параметр flags у Attribute — битовая маска. Оператор | объединяет разрешения, & проверяет отдельный бит. TARGET_ALL означает все поддержанные места, но не разрешает повторение само по себе. IS_REPEATABLE добавляется отдельно. Свойство flags хранит переданную маску.
| Константа | К чему можно прикреплять пользовательский атрибут |
|---|---|
| TARGET_CLASS | Класс |
| TARGET_FUNCTION | Функция |
| TARGET_METHOD | Метод |
| TARGET_PROPERTY | Свойство |
| TARGET_CLASS_CONSTANT | Константа класса |
| TARGET_PARAMETER | Параметр |
| TARGET_CONSTANT | Константа вне класса, PHP 8.5+ |
| TARGET_ALL | Все допустимые места в данной версии |
| IS_REPEATABLE | Дополнительный флаг: разрешить повторение на одном объявлении |
<?php
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)]
class Tag {
public function __construct(public string $name) {}
}
#[Tag('catalog'), Tag('public')]
function listProducts(): void {}
foreach ((new ReflectionFunction('listProducts'))->getAttributes(Tag::class) as $metadata) {
echo $metadata->newInstance()->name, "\n";
}
Ожидаемый результат
catalog
public
public string $name в параметре конструктора одновременно объявляет и заполняет свойство. В реальном приложении метки могут выбрать обработчики кеширования или документации. Пока вы не написали обработчик, это только данные.
Для пользовательского атрибута нарушение цели или повторения проверяется при создании экземпляра через Reflection. Встроенные атрибуты могут иметь проверки на этапе компиляции. С PHP 8.5 само #[Attribute] на abstract class, enum, interface или trait вызывает ошибку компиляции; раньше ошибка обнаруживалась при попытке newInstance. Пользовательский класс атрибута должен быть пригоден для создания экземпляра.
<?php
#[Attribute(Attribute::TARGET_FUNCTION)]
class FunctionTag {}
#[FunctionTag]
class Product {}
$metadata = (new ReflectionClass(Product::class))->getAttributes(FunctionTag::class)[0];
echo $metadata->getName(), "\n";
try { $metadata->newInstance(); }
catch (Error $error) { echo "Неподходящая цель\n"; }
Ожидаемый результат
FunctionTag
Неподходящая цель
Чтение имени атрибута и создание его объекта — разные операции. Поэтому система, которая проверяет конфигурацию при запуске, должна проверять не только наличие метки, но и корректность её использования.
AllowDynamicProperties: осознанно разрешить свободные поля
С PHP 8.2 запись нового, не объявленного свойства обычного класса вызывает deprecated. Это помогает обнаружить опечатку вроде $order->stauts вместо status. Если контейнер по замыслу принимает произвольные поля, AllowDynamicProperties разрешает их явно. Если поля заранее известны, объявите их в классе — это полезнее подавления предупреждения.
<?php
#[\AllowDynamicProperties]
class FlexibleRecord {}
class ImportedRecord extends FlexibleRecord {}
$record = new ImportedRecord();
$record->externalCode = 'EXT-7';
echo $record->externalCode, "\n";
Ожидаемый результат
EXT-7
Наследуется эффект разрешения динамических свойств, а не общая способность всех атрибутов наследоваться. В namespace обратный слеш у \AllowDynamicProperties выбирает именно встроенный класс.
stdClass уже разрешает динамические поля. Обработчик __set может перенаправить запись в заранее объявленный внутренний массив; тогда новое свойство самого объекта не создаётся. Но запись $this->$name = $value внутри __set снова создаёт динамическое свойство и не становится автоматически корректной. Один лишь __get не решает запись неизвестных полей.
<?php
class Order {
public string $status = 'new';
}
$order = new Order();
$order->status = 'paid';
echo $order->status, "\n";
Ожидаемый результат
paid
Для заранее определённого статуса отдельный атрибут не нужен. Тип string ограничивает тип значения, но не набор допустимых статусов: для этого можно использовать enum из главы о перечислениях.
Override: поймать опечатку, пока код ещё не запущен
Вы переопределяете метод отправки уведомления. Если ошибиться в имени, без проверки получится новый метод, который никто не вызовет. #[Override] сообщает намерение: в родителе или реализуемом интерфейсе должно быть соответствующее объявление. При его отсутствии PHP выдаёт ошибку компиляции. Атрибут не делает несовместимую сигнатуру совместимой.
<?php
interface Sender { public function send(string $text): string; }
class ConsoleSender implements Sender {
#[\Override]
public function send(string $text): string { return 'Отправлено: ' . $text; }
}
echo (new ConsoleSender())->send('Заказ готов'), "\n";
Ожидаемый результат
Отправлено: Заказ готов
Если назвать метод snd, требуемый send не будет реализован; если пометить лишний метод без совпадения, Override тоже обнаружит ошибку. Компиляционная ошибка такого объявления не является обычным возвращаемым результатом вызова.
<?php
class BaseView { public string $title = 'Страница'; }
class CatalogView extends BaseView {
#[\Override]
public string $title = 'Каталог';
}
echo (new CatalogView())->title, "\n";
Ожидаемый результат
Каталог
Override доступен для методов с PHP 8.3, для свойств — с PHP 8.5. Он не применяется к __construct, который имеет особые правила сигнатуры. Пометка на совершенно новом свойстве не подходит: требуется совпадение в родительском/интерфейсном договоре.
Deprecated: дать вызывающему коду понятный путь обновления
Библиотека заменяет старый метод, но не может сразу сломать клиентов. #[Deprecated] с PHP 8.4 позволяет выдать E_USER_DEPRECATED при использовании старой функции/метода или константы класса. Тело функции всё ещё выполняется. Сообщение должно говорить, что вызвать вместо неё, а since — с какой версии вашего API началась миграция.
<?php
set_error_handler(function (int $level, string $message): bool {
if ($level !== E_USER_DEPRECATED) { return false; }
echo "Устаревший вызов\n";
return true;
});
#[\Deprecated(message: 'Используйте currentPrice()', since: '2.0')]
function oldPrice(): int { return 100; }
echo oldPrice(), "\n";
restore_error_handler();
Ожидаемый результат
Устаревший вызов
100
Обработчик перехватывает ожидаемую диагностику только для воспроизводимого примера и затем восстанавливается. В приложении такую диагностику фиксируют в журнале разработки и исправляют вызовы; глобальное подавление скроет план миграции.
message и since — публичные readonly ?string, оба по умолчанию null. PHP не проверяет, что since является номером версии: это может быть дата или принятый в проекте текст. С PHP 8.5 Deprecated разрешён также на trait и константе вне класса, объявленной через const. Разрешение TARGET_CLASS здесь не означает, что можно так пометить любой обычный класс: для этой возможности речь о trait.
<?php
set_error_handler(function (int $level): bool {
if ($level !== E_USER_DEPRECATED) { return false; }
echo "Старое имя константы\n";
return true;
});
#[\Deprecated('Используйте API_TIMEOUT', '2.0')]
const OLD_TIMEOUT = 10;
echo OLD_TIMEOUT, "\n";
restore_error_handler();
Ожидаемый результат
Старое имя константы
10
Атрибут записан при объявлении const, а предупреждение связано с использованием. Для константы, создаваемой обычным вызовом define, такой синтаксис пометки не применяется.
NoDiscard: не потерять важный результат операции
Проверка пачки заказов вернула список отклонённых элементов. Если вызывающий код отбросил результат, он может ошибочно считать все заказы пригодными к оформлению. С PHP 8.5 #[NoDiscard] на функции/методе предупреждает, когда результат вообще не используется. Это помощь против пропуска проверки, а не доказательство, что результат обработан правильно.
<?php
declare(strict_types=1);
set_error_handler(function (int $level): bool {
if ($level !== E_USER_WARNING) { return false; }
echo "Не потеряйте список отклонённых заказов\n";
return true;
});
#[NoDiscard('Проверьте отклонённые элементы')]
function rejectedItems(): array { return ['Заказ A-42: неизвестный артикул']; }
rejectedItems();
$rejected = rejectedItems();
foreach ($rejected as $message) { echo $message, "\n"; }
restore_error_handler();
Ожидаемый результат
Не потеряйте список отклонённых заказов
Заказ A-42: неизвестный артикул
PHP 8.5+. Первый вызов намеренно выбрасывает важный результат, и NoDiscard предупреждает. Второй сохраняет список и даёт полезное сообщение для исправления импорта. Атрибут не заставляет программу правильно обработать список: присвоение само по себе тоже ещё не отчёт об ошибках.
<?php
declare(strict_types=1);
#[NoDiscard]
function replaceCaption(array &$preview, string $caption): string
{
$previous = $preview['caption'];
$preview['caption'] = $caption;
return $previous;
}
$preview = ['caption' => 'Черновик'];
(void) replaceCaption($preview, 'Готовый отчёт');
echo $preview['caption'], "\n";
$_ = replaceCaption($preview, 'Отчёт за октябрь');
echo $preview['caption'], "\n";
Ожидаемый результат
Готовый отчёт
Отчёт за октябрь
Функция заменяет подпись и возвращает прежний текст. В первом вызове (void) явно отбрасывает прошлую подпись; во втором результат присвоен переменной $_ и потому считается использованным. $_ — обычное имя, не встроенная защита. Так нельзя обосновывать игнорирование списка ошибок: сначала определите, действительно ли возвращённые данные не нужны.
Срабатывает атрибут фактически вызванного объявления. Пометка только в interface или abstract-методе не переносит предупреждение на реализацию. Переопределённый метод должен быть помечен отдельно, если ему нужна та же проверка. Метод, скопированный из trait, сохраняет атрибут. На PHP 8.4 и ниже неизвестная пометка NoDiscard не добавляет эту проверку; это не повод писать неподдерживаемый там (void).
ReturnTypeWillChange: временный мост для старой библиотеки
С PHP 8.1 у многих переопределяемых внутренних методов появились предварительные возвращаемые типы. Старый класс без подходящего типа получает deprecated при проверке наследования. Если библиотека ещё обязана поддерживать старый синтаксис PHP и не может сразу указать тип, #[ReturnTypeWillChange] временно подавляет именно эту диагностику.
<?php
declare(strict_types=1);
class LegacyBasket implements Countable
{
private array $items = ['Чай', 'Кофе'];
#[ReturnTypeWillChange]
public function count() { return count($this->items); }
}
echo 'В старой корзине: ', count(new LegacyBasket()), " позиции\n";
Ожидаемый результат
В старой корзине: 2 позиции
ReturnTypeWillChange временно подавляет deprecation из-за отсутствующего ожидаемого типа результата внутреннего договора. Он не исправляет ошибочный алгоритм и не обещает будущую совместимость. Следующий пример показывает конечную цель миграции.
<?php
declare(strict_types=1);
class Basket implements Countable
{
private array $items = ['Чай', 'Кофе'];
public function count(): int { return count($this->items); }
}
echo 'В новой корзине: ', count(new Basket()), " позиции\n";
Ожидаемый результат
В новой корзине: 2 позиции
Добавлен совместимый : int, поэтому временный атрибут не нужен. Число по-прежнему обозначает позиции, а не суммарные единицы. Миграция сохраняет видимый результат и делает договор проверяемым.
SensitiveParameter: скрыть аргумент в трассировке, сохранив работу функции
- authenticate получает обычную строку пароля
- При построении trace PHP заменяет помеченный аргумент оболочкой SensitiveParameterValue
- Обычный дамп этой оболочки не показывает исходную строку
- Сам пароль внутри функции остаётся доступен её коду
- Поэтому явное логирование секрета всё равно запрещает договор приложения
При сбое входа в сервис стек вызовов может включать пароль или токен. #[SensitiveParameter] ставится на параметр функции/метода: в трассировке его значение заменяется объектом SensitiveParameterValue. Внутри функции параметр остаётся обычным исходным значением. Это защита от случайного вывода аргументов стека, не шифрование и не запрет доступа к значению.
- В функцию пришёл учебный пароль
- Тело получает обычную строку
- При создании трассировки аргумент оборачивается
- Диагностический вывод оболочки не показывает содержимое
<?php
ini_set('zend.exception_ignore_args', '0');
function authenticate(#[\SensitiveParameter] string $password): void {
throw new RuntimeException('Вход не выполнен');
}
try { authenticate('demo-secret'); }
catch (RuntimeException $error) {
$argument = $error->getTrace()[0]['args'][0];
echo get_class($argument), "\n";
var_dump($argument->__debugInfo());
}
Ожидаемый результат
SensitiveParameterValue
array(0) {
}
Настройка включает аргументы только внутри самостоятельного опыта, чтобы показать обёртку. При zend.exception_ignore_args=1 аргументы исключения вообще не сохраняются. Во многих рабочих окружениях это подходящий более общий способ не включать их в диагностику.
<?php
declare(strict_types=1);
function tokenStatus(#[SensitiveParameter] string $token): string
{
return $token === 'demo-token' ? 'Учебный токен принят' : 'Учебный токен отклонён';
}
echo tokenStatus('demo-token'), "\n";
echo tokenStatus('other'), "\n";
Ожидаемый результат
Учебный токен принят
Учебный токен отклонён
SensitiveParameter меняет представление аргумента в трассировке, но внутри функции token остаётся обычной строкой. Это демонстрационное сравнение, не готовая авторизация. Не выводите и не логируйте сам токен; атрибут не защитит от вашего echo или записи в файл.
Пометка относится к конкретному параметру конкретного вызова. Если дальше передать секрет в другую функцию без пометки, её кадр может раскрыть значение. У promoted-параметра конструктора атрибут защищает аргумент, а не произвольный вывод свойства объекта. Обёртка SensitiveParameterValue хранит private readonly mixed value, но предоставляет явный getValue: это не граница безопасности от кода с доступом к объекту.
DelayedTargetValidation: отложить проверку места применения
Библиотека может поставлять код с атрибутом, для которого будущая версия PHP разрешит новое место применения. С PHP 8.5 DelayedTargetValidation откладывает проверку допустимой цели встроенных атрибутов на том же объявлении до ReflectionAttribute::newInstance(). Это специализированный инструмент совместимости: он не делает ошибочную цель корректной.
<?php
class Feature {
#[\DelayedTargetValidation]
#[\Override]
public const LABEL = 'Каталог';
}
echo Feature::LABEL, "\n";
$reflection = new ReflectionClassConstant(Feature::class, 'LABEL');
$metadata = $reflection->getAttributes(Override::class)[0];
try { $metadata->newInstance(); }
catch (Error $error) { echo "Override не допускает эту цель\n"; }
Ожидаемый результат
Каталог
Override не допускает эту цель
В PHP 8.5 Override допускает метод/свойство, но не константу класса. Отложенная проверка позволила загрузить объявление, затем newInstance обнаружил несоответствие. Это контролируемая иллюстрация, а не рекомендуемое оформление константы.
Атрибут разрешён на всех целях, не имеет собственных документированных методов и относится только к target validation встроенных атрибутов. Он не подавляет смысловую проверку: метод с Override всё равно обязан переопределять существующее объявление. Без учёта этой границы можно ошибочно принять отсрочку за разрешение любых атрибутов.
Выбрать атрибут по намерению и версии PHP
| Потребность | Атрибут | Версия и граница |
|---|---|---|
| Создать собственные метаданные | Attribute | 8.0; константы вне класса с 8.5 |
| Осознанно разрешить свободные свойства | AllowDynamicProperties | 8.2; не обнаружит опечатки полей |
| Отложить проверку цели встроенной пометки | DelayedTargetValidation | 8.5; не отменяет смысловую проверку |
| Предупредить об устаревшем API | Deprecated | 8.4; trait и внешние const с 8.5 |
| Не потерять важный результат | NoDiscard | 8.5; не доказывает корректную обработку |
| Проверить намерение переопределить | Override | 8.3; свойства с 8.5 |
| Временно поддержать старую сигнатуру | ReturnTypeWillChange | 8.1; только tentative-типы внутренних методов |
| Не показывать параметр в стеке | SensitiveParameter | 8.2; не чистит ручные логи/сообщения |
Перед обновлением библиотеки проверьте используемый PHP и минимальную поддерживаемую версию. Неизвестный атрибут может не повлиять на выполнение старого PHP, но его Reflection::newInstance всё равно не создаст отсутствующий класс. Метаданные, синтаксис и фактические проверки имеют разные границы совместимости.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
Attribute::__constructСоздать описание разрешённых целей пользовательского атрибута.
public function __construct(int $flags = Attribute::TARGET_ALL)Создать описание разрешённых целей пользовательского атрибута.
| Параметр / результат | Договор |
|---|---|
| flags | Битовая маска TARGET_* и, при необходимости, IS_REPEATABLE. По умолчанию TARGET_ALL. |
| Свойство | Публичное int flags сохраняет маску. |
| Результат new | Объект Attribute. Прямое создание не помечает автоматически другой класс. |
- TARGET_FUNCTION | IS_REPEATABLE
- new Attribute(flags)
- Свойство flags позволяет прочитать настройку
<?php
declare(strict_types=1);
$metadata = new Attribute();
echo $metadata->flags === Attribute::TARGET_ALL ? "Цели по умолчанию: все допустимые объявления\n" : "Цели ограничены\n";
echo ($metadata->flags & Attribute::IS_REPEATABLE) !== 0 ? "Повторение разрешено\n" : "Повторение надо разрешить отдельно\n";
Ожидаемый результат
Цели по умолчанию: все допустимые объявления
Повторение надо разрешить отдельно
flags — битовая маска разрешений. TARGET_ALL объединяет цели, но не IS_REPEATABLE. Сам new Attribute только создаёт объект метаданных; чтобы пометить класс пользовательского атрибута, применяется #[Attribute(...)]. Реальное применение показано в начале главы.
<?php
declare(strict_types=1);
$metadata = new Attribute(Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE);
echo ($metadata->flags & Attribute::TARGET_FUNCTION) !== 0 ? "Можно помечать функции\n" : "Функции запрещены\n";
echo ($metadata->flags & Attribute::TARGET_METHOD) !== 0 ? "Можно помечать методы\n" : "Методы не входят в разрешённые цели\n";
echo ($metadata->flags & Attribute::IS_REPEATABLE) !== 0 ? "Несколько меток разрешены\n" : "Только одна метка\n";
Ожидаемый результат
Можно помечать функции
Методы не входят в разрешённые цели
Несколько меток разрешены
| объединяет выбранные биты, & выделяет проверяемый бит. TARGET_FUNCTION и TARGET_METHOD различаются: метод — функция внутри класса, но у атрибута это отдельная цель. Результат разрешений нужен сборщику конфигурации, а не заменяет фактическую проверку newInstance.
Рядом: Override::__construct, Deprecated::__construct. Карта встроенных атрибутов.
AllowDynamicProperties::__constructСоздать экземпляр маркера разрешённых динамических свойств.
public function __construct()Создать экземпляр маркера разрешённых динамических свойств.
| Параметр / результат | Договор |
|---|---|
| Аргументы | Нет. |
| Свойства настройки | Нет. |
| Применение | #[AllowDynamicProperties] на классе, PHP 8.2+. Эффект действует у его потомков. |
- #[AllowDynamicProperties] над FlexibleRecord
- PHP разрешает новые поля этой ветви классов
- Reflection может отдельно создать объект маркера
<?php
#[\AllowDynamicProperties]
class FlexibleRecord {}
$reflection = new ReflectionClass(FlexibleRecord::class);
$marker = $reflection->getAttributes(AllowDynamicProperties::class)[0]->newInstance();
echo get_class($marker), "\n";
Ожидаемый результат
AllowDynamicProperties
Маркер не содержит список разрешённых имён: он разрешает динамические свойства вообще. Для отдельных известных полей лучше объявление свойств.
<?php
namespace Import;
#[\AllowDynamicProperties]
class Row {}
$row = new Row();
$row->remoteId = 'A-7';
echo $row->remoteId, "\n";
Ожидаемый результат
A-7
Без обратного слеша или импорта AllowDynamicProperties в namespace Import означал бы Import\AllowDynamicProperties. Неизвестное имя атрибута само по себе не обязано вызвать ошибку до Reflection, поэтому неверный импорт легко принять за неработающее разрешение.
Deprecated::__constructСохранить сообщение о замене и отметку начала устаревания.
public function __construct(?string $message = null, ?string $since = null)Сохранить сообщение о замене и отметку начала устаревания.
| Параметр / результат | Договор |
|---|---|
| message | Причина/рекомендуемая замена или null. |
| since | Строка версии, даты или другого принятого обозначения; PHP её формат не проверяет. |
| Свойства | Публичные readonly ?string message и since. |
- message: используйте новый метод
- since: версия вашего API
- Диагностика устаревшего вызова содержит эти сведения
<?php
$metadata = new Deprecated('Используйте findOrder()', '2.0');
echo $metadata->message, "\n", $metadata->since, "\n";
Ожидаемый результат
Используйте findOrder()
2.0
Создание объекта не вызывает deprecated: оно только создаёт метаданные. Предупреждение относится к использованию помеченного API.
<?php
#[\Deprecated(message: 'Используйте newReport()', since: '2026-10')]
function oldReport(): string { return 'Старый отчёт'; }
$reflection = new ReflectionFunction('oldReport');
$metadata = $reflection->getAttributes(Deprecated::class)[0]->newInstance();
echo $reflection->isDeprecated() ? "Старый вызов отмечен устаревшим\n" : "Вызов актуален\n";
echo $metadata->message, "\n";
echo $metadata->since, "\n";
Ожидаемый результат
Старый вызов отмечен устаревшим
Используйте newReport()
2026-10
Reflection прочитал метку, не выполняя oldReport. message даёт действие для миграции, since — версию/дату, принятую библиотекой. Значение since не является встроенным условием автоматического удаления функции.
Рядом: NoDiscard::__construct, Attribute::__construct. Карта встроенных атрибутов.
NoDiscard::__constructЗадать объяснение, почему результат нельзя терять.
public function __construct(?string $message = null)Задать объяснение, почему результат нельзя терять.
| Параметр / результат | Договор |
|---|---|
| message | Необязательная строка пояснения, по умолчанию null. |
| Свойство | Публичное readonly ?string message. |
| Доступность | PHP 8.5+, цели — функция и метод. |
- Операция возвращает сведения об отказах
- NoDiscard хранит причину важности результата
- Отброшенный вызов даёт warning с пояснением
<?php
$withReason = new NoDiscard('Обработайте элементы с ошибками');
$withoutReason = new NoDiscard();
echo $withReason->message, "\n";
var_dump($withoutReason->message);
Ожидаемый результат
Обработайте элементы с ошибками
NULL
Сформулируйте причину с точки зрения вызывающего кода. «Потому что надо» не поможет исправить потерянную обработку.
<?php
#[\NoDiscard('Проверьте успешность локальной проверки')]
function validateLocalRecord(): bool { return false; }
if (!validateLocalRecord()) { echo "Запись отклонена\n"; }
Ожидаемый результат
Запись отклонена
Использование в условии считается использованием результата. Атрибут не требует присваивать его именно переменной и не гарантирует, что ваша логика if правильна.
Рядом: Deprecated::__construct, Attribute::__construct. Карта встроенных атрибутов.
Override::__constructСоздать маркер намеренного переопределения.
public function __construct()Создать маркер намеренного переопределения.
| Параметр / результат | Договор |
|---|---|
| Аргументы | Нет. |
| Цели | Метод с PHP 8.3; свойство с PHP 8.5. Конструктор __construct исключён. |
| Проверка | Совпадающее объявление должно существовать у родителя/интерфейса; нарушение обнаруживается при компиляции. |
- Родитель определил title()
- Потомок пометил свой title()
- PHP проверяет существование переопределяемого объявления
<?php
class BaseLabel { public function label(): string { return 'База'; } }
class ProductLabel extends BaseLabel {
#[\Override]
public function label(): string { return 'Товар'; }
}
$method = new ReflectionMethod(ProductLabel::class, 'label');
echo get_class($method->getAttributes(Override::class)[0]->newInstance()), "\n";
echo (new ProductLabel())->label(), "\n";
Ожидаемый результат
Override
Товар
Маркер уже участвовал в проверке объявления; Reflection только создал его объект. Он не переключает вызов на родителя и не выполняет parent::label.
<?php
$marker = new Override();
class PlainLabel { public function label(): string { return 'Обычный'; } }
echo get_class($marker), "\n";
echo count((new ReflectionMethod(PlainLabel::class, 'label'))->getAttributes()), "\n";
Ожидаемый результат
Override
0
Объект маркера — отдельное значение. Для проверки метода пометка должна находиться в его объявлении. Это общее различие new AttributeClass и синтаксиса #[AttributeClass].
Рядом: Attribute::__construct, ReturnTypeWillChange::__construct. Карта встроенных атрибутов.
ReturnTypeWillChange::__constructСоздать переходный маркер для предварительных возвращаемых типов внутренних методов.
public function __construct()Создать переходный маркер для предварительных возвращаемых типов внутренних методов.
| Параметр / результат | Договор |
|---|---|
| Аргументы | Нет. |
| Цель | Метод, PHP 8.1+. |
| Граница | Подавляет tentative-return-type deprecated, но не несовместимость строгого пользовательского типа. |
- Старый count() пока без : int
- ReturnTypeWillChange подавляет конкретную диагностику
- После обновления минимального PHP объявите совместимый тип
<?php
class OldItems implements Countable {
#[\ReturnTypeWillChange]
public function count() { return 3; }
}
$method = new ReflectionMethod(OldItems::class, 'count');
echo get_class($method->getAttributes(ReturnTypeWillChange::class)[0]->newInstance()), "\n";
echo count(new OldItems()), "\n";
Ожидаемый результат
ReturnTypeWillChange
3
Это пример миграции, а не рекомендуемый старт нового класса. Подходящий современный тип исключает необходимость маркера.
<?php
$method = new ReflectionMethod(Countable::class, 'count');
var_dump($method->hasTentativeReturnType());
echo $method->getTentativeReturnType(), "\n";
Ожидаемый результат
bool(true)
int
Так инструмент анализа отличает tentative-договор от отсутствия сведений о результате. Проверка выполнена на PHP 8.5.11; переход к строгому типу в будущей версии потребует обновления библиотечного кода.
Рядом: Override::__construct, Attribute::__construct. Карта встроенных атрибутов.
SensitiveParameter::__constructСоздать маркер параметра, скрываемого в трассировках.
public function __construct()Создать маркер параметра, скрываемого в трассировках.
| Параметр / результат | Договор |
|---|---|
| Аргументы | Нет. |
| Цель | Параметр функции/метода, PHP 8.2+. |
| Эффект | Значение аргумента в стеке заменяется SensitiveParameterValue, само тело получает исходное значение. |
- #[SensitiveParameter] string $token
- Вызов получает обычную строку
- В трассировке аргумент представлен оболочкой
<?php
function connectService(#[\SensitiveParameter] string $token): void {}
$parameter = (new ReflectionFunction('connectService'))->getParameters()[0];
echo get_class($parameter->getAttributes(SensitiveParameter::class)[0]->newInstance()), "\n";
Ожидаемый результат
SensitiveParameter
Метаданные не содержат конкретный токен; это свойство объявления параметра. Не смешивайте объект маркера и объект SensitiveParameterValue, который хранит конкретное значение.
<?php
function inspectCall(#[\SensitiveParameter] string $token): string {
$argument = debug_backtrace()[0]['args'][0];
return get_class($argument);
}
echo inspectCall('demo-token'), "\n";
Ожидаемый результат
SensitiveParameterValue
Пометка действует и в обычной трассировке вызовов. Если выбран режим DEBUG_BACKTRACE_IGNORE_ARGS, аргументы вообще отсутствуют; нельзя читать [args][0] без учёта настроек используемого сборщика.
Рядом: SensitiveParameterValue::__construct, SensitiveParameterValue::__debugInfo, SensitiveParameterValue::getValue. Карта встроенных атрибутов.
SensitiveParameterValue::__constructОбернуть конкретное значение для безопасного обычного отладочного представления.
public function __construct(mixed $value)Обернуть конкретное значение для безопасного обычного отладочного представления.
| Параметр / результат | Договор |
|---|---|
| value | Значение любого типа. |
| Хранение | private readonly mixed value внутри объекта. |
| Граница | Обёртка не шифрует содержимое; getValue может его вернуть. |
- Исходная учебная строка
- new SensitiveParameterValue(value)
- Обычный var_dump показывает оболочку, не её секрет
<?php
$protected = new SensitiveParameterValue('demo-secret');
echo get_class($protected), "\n";
var_dump($protected->__debugInfo());
Ожидаемый результат
SensitiveParameterValue
array(0) {
}
Класс final. Пустой отладочный массив скрывает поле от стандартного var_dump; он не делает само значение недоступным доверенному коду.
<?php
declare(strict_types=1);
$data = (object) ['enabled' => false];
$protected = new SensitiveParameterValue($data);
$protected->getValue()->enabled = true;
echo $data->enabled ? "Исходная настройка тоже включена\n" : "Исходная настройка не изменилась\n";
Ожидаемый результат
Исходная настройка тоже включена
Переданный объект и объект из getValue — один экземпляр. Изменение через один доступ видно через другой. Для опыта взято несекретное поле enabled: содержимое защищённого токена выводить для проверки идентичности не требуется.
Рядом: SensitiveParameterValue::__debugInfo, SensitiveParameterValue::getValue, SensitiveParameter::__construct. Карта встроенных атрибутов.
SensitiveParameterValue::__debugInfoВернуть пустое отладочное представление.
public function __debugInfo(): arrayВернуть пустое отладочное представление.
| Параметр / результат | Договор |
|---|---|
| Аргументы | Нет. |
| Возврат | Пустой массив. |
| Автоматический вызов | var_dump использует этот метод для представления объекта. |
- var_dump получает оболочку
- __debugInfo() возвращает []
- Содержимое private value не попадает в этот вывод
<?php
$protected = new SensitiveParameterValue(['password' => 'demo-secret']);
var_dump($protected->__debugInfo());
Ожидаемый результат
array(0) {
}
Метод одинаково скрывает строку, массив и другие значения. Он не выполняет выборочное маскирование полей: отладочное представление оболочки целиком пустое.
<?php
$protected = new SensitiveParameterValue('demo-secret');
ob_start();
var_dump($protected);
$dump = ob_get_clean();
var_dump(str_contains($dump, 'SensitiveParameterValue'));
var_dump(str_contains($dump, 'demo-secret'));
Ожидаемый результат
bool(true)
bool(false)
Буфер вывода здесь позволяет проверить диагностический текст, не печатая его. Номер объекта меняется между запусками, поэтому проверяем смысловые признаки. Касты, Reflection и ручное извлечение не являются тем же самым отладочным представлением.
Рядом: SensitiveParameterValue::getValue, SensitiveParameterValue::__construct. Карта встроенных атрибутов.
SensitiveParameterValue::getValueЯвно получить исходное чувствительное значение.
public function getValue(): mixedЯвно получить исходное чувствительное значение.
| Параметр / результат | Договор |
|---|---|
| Аргументы | Нет. |
| Возврат | Изначально сохранённое значение с исходным типом. |
| Риск использования | После извлечения его легко снова вывести в лог; автоматическая защита оболочки на новую переменную не распространяется. |
- Обёртка хранит значение
- getValue() возвращает его
- Дальнейшая обработка снова отвечает за конфиденциальность
<?php
declare(strict_types=1);
$protected = new SensitiveParameterValue('demo-token');
$matches = $protected->getValue() === 'demo-token';
echo $matches ? "Учебная проверка токена прошла\n" : "Учебная проверка токена отклонена\n";
Ожидаемый результат
Учебная проверка токена прошла
getValue возвращает исходное значение, поэтому обёртка не является шифрованием или запретом доступа. Здесь выводится только решение, токен не печатается. Это локальная демонстрация API, а не полноценная схема аутентификации.
<?php
$protected = new SensitiveParameterValue(['id' => 7]);
$value = $protected->getValue();
echo get_debug_type($value), "\n";
echo $value['id'], "\n";
Ожидаемый результат
array
7
Возврат mixed не преобразует всё в строку. При массиве действуют обычные правила значений PHP, при объекте возвращается тот же объект. Не сериализуйте извлечённые пароли ради удобства отладки.
Рядом: SensitiveParameterValue::__debugInfo, SensitiveParameterValue::__construct. Карта встроенных атрибутов.
Источники и обсуждения
- PHP: reserved.attributes
- PHP: class.attribute
- PHP: class.allowdynamicproperties
- PHP: class.delayedtargetvalidation
- PHP: class.deprecated
- PHP: class.nodiscard
- PHP: class.override
- PHP: class.returntypewillchange
- PHP: class.sensitiveparameter
- PHP: class.sensitiveparametervalue
- PHP 8.5.11: NoDiscard и уровень диагностики
- Пароль оказался в трассировке — обсуждение проблемы
- Скрыть только чувствительные аргументы — обсуждение проблемы
- Атрибут не сработал из-за namespace — обсуждение проблемы
- Заменить случайные динамические поля — обсуждение проблемы