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

Дерево руководства
Глава 40 · От основ к архитектуре
Классы и объекты: состояние, идентичность и методы
Понимаем, что хранится в переменной объекта, почему присваивание не копирует заказ и как не оставить типизированное свойство неинициализированным.
Первый объект: отдельная карточка товара
Для каталога нужна карточка, у которой есть название. Создадим одну карточку, прочитаем название и поменяем его. Сначала не нужны ни конструктор, ни наследование, ни исключения.
Что получится. В выводе сначала Чай, затем Зелёный чай. Это изменение одной конкретной карточки.
- class ProductCard задаёт поле title
- new создаёт карточку в $card
- Запись $card->title меняет её поле
- echo читает текущее значение
<?php
declare(strict_types=1);
class ProductCard
{
public string $title = 'Чай';
}
$card = new ProductCard();
echo $card->title, "\n";
$card->title = 'Зелёный чай';
echo $card->title, "\n";
Ожидаемый результат
Чай
Зелёный чай
Свойство title относится к объекту. $card — переменная, содержащая этот объект; имя свойства после -> пишется без $. Строка присваивания меняет значение, а echo только читает. Описание класса осталось прежним.
| Запись в примере | Что делает каждый элемент |
|---|---|
public string $title | public разрешает внешний доступ, string ограничивает значение строкой, $title объявляет имя свойства. |
$card->title | У объекта в $card выбираем его свойство title. Это не ключ массива: у массива были бы квадратные скобки. |
new ProductCard() | Каждое выполненное new создаёт новый экземпляр. Объявить класс и создать объект — разные действия. |
Метод использует данные той карточки, у которой его вызвали
Один класс — два отдельных экземпляра товара
Одинаковые свойства и методы не означают общие значения: каждый вызов new создаёт отдельный экземпляр.
- Product — описание устройства
Класс объявляет title и конструктор. Это ещё не конкретный товар.
- $original → первый объект
title = «Зелёный чай». Значение хранится в этом экземпляре.
- $alias → второй объект после new
title = «Кофе». Замена этой переменной не переименовала первый товар.
Раньше $alias = $original указывало на тот же первый объект: правка через alias была видна через original. Только последующее new Product('Кофе') создало второй экземпляр.
<?php
declare(strict_types=1);
class ProductCard
{
public string $title = 'Чай';
public function label(): string
{
return 'Товар: ' . $this->title;
}
}
$card = new ProductCard();
echo $card->label(), "\n";
$card->title = 'Кофе';
echo $card->label(), "\n";
Ожидаемый результат
Товар: Чай
Товар: Кофе
function внутри класса объявляет метод. Пустые () означают, что дополнительных аргументов нет. $this появляется при вызове метода у объекта и указывает на него. Каждый вызов читает текущее title: label не является заранее сохранённой второй копией текста.
- $card->label() передаёт управление телу label
- Внутри $this означает $card, его title уже Кофе
- return отдаёт строку Товар: Кофе
- echo печатает её, выполнение идёт дальше
Два новых объекта независимы; присваивание не копирует
<?php
declare(strict_types=1);
class ProductCard
{
public string $title = 'Чай';
}
$first = new ProductCard();
$second = new ProductCard();
$same = $first;
$same->title = 'Кофе';
echo 'Первая: ', $first->title, "\n";
echo 'Вторая: ', $second->title, "\n";
Ожидаемый результат
Первая: Кофе
Вторая: Чай
Два new создали две карточки. Присваивание $same = $first сохранило доступ к уже существующей первой карточке. Поэтому запись через $same видна через $first. Это правило объектов; оно не означает, что переменные связаны оператором &.
Независимая копия создаётся отдельным new или оператором clone. Значения двух карточек могут совпадать, хотя это разные объекты: сравнение содержимого и идентичности разобрано в следующем специальном разделе.
Сначала записать обязательное значение, потом читать
Тип string не придумывает начальную строку. Если объявить поле без значения, оно остаётся неинициализированным. Следующий пример показывает ошибку и исправление в одном небольшом сценарии; catch принимает ошибку, чтобы урок мог продолжиться.
<?php
declare(strict_types=1);
class ProductCard
{
public string $title;
}
$card = new ProductCard();
try {
echo $card->title;
} catch (Error $error) {
echo "Сначала задайте название.\n";
}
$card->title = 'Чай';
echo $card->title, "\n";
Ожидаемый результат
Сначала задайте название.
Чай
try выполняет действие, которое может не удасться; catch перехватывает Error от чтения незаполненного типизированного свойства. Так диагностируют проблему, но удобнее обязать передать название конструктору. Запись ?string допускает null, однако сама по себе тоже не устанавливает начальное значение.
Метод получает аргумент и меняет осмысленное состояние
<?php
declare(strict_types=1);
class ProductCard
{
public string $title = 'Чай';
public function rename(string $newTitle): void
{
$this->title = $newTitle;
}
}
$card = new ProductCard();
$card->rename('Чёрный чай');
echo $card->title, "\n";
Ожидаемый результат
Чёрный чай
Чёрный чай передаётся в локальный параметр $newTitle. $this->title — поле объекта, $newTitle — временное имя входа в текущем вызове. void означает отсутствие возвращаемого значения; полезное действие — изменение названия.
Пока прямую запись никто не запретил: public позволяет обойти rename. Чтобы операция действительно контролировала изменение, свойство надо закрыть и добавить проверку нового названия. Это следующий шаг в главе о видимости.
Карточка, клиент API и запись JSON — разные обязанности
Наш ProductCard хранит данные товара и действия над ними. Клиент CRM хранит настройки соединения и умеет выполнять запросы. Обычная запись JSON, полученная от API, может остаться массивом: не каждый внешний объект JSON должен превращаться в пользовательский класс PHP. Выбирайте структуру по тому, какие операции и правила действительно нужны.
Попробуйте. Создайте карточки Чай и Кофе. Назовите первую Чёрный чай через rename и выведите обе подписи.
Разобрать решение
Дважды выполните new ProductCard(), у второй задайте title = "Кофе"; вызовите rename только у первой. Независимость определяется двумя созданными экземплярами, а не разными именами переменных.
Границы первого класса
Открытое поле полезно для первого знакомства, но не защищает состояние от неверных значений. Тип проверяет вид значения, а не смысл названия. Новый HTTP-запрос создаёт своё окружение; ProductCard сам не сохраняется в БД. Не добавляйте динамические поля из-за опечатки: объявляйте ожидаемые свойства. С PHP 8.2 большинство неразрешённых динамических свойств deprecated; stdClass имеет отдельный договор.
Для большого набора товаров стоимость определяют число объектов, объём полей и действия над ними. Не обещайте, что объект всегда экономнее массива или всегда медленнее: измеряйте свой сценарий.
Дальше: полный договор языка и специальные случаи
Теперь известны class, new, свойство, метод, $this, независимый экземпляр и общий доступ после присваивания. На этой основе можно разбирать сравнение, динамические свойства, анонимные и ленивые объекты, nullsafe-вызовы и автозагрузку. Эти специальные правила не требуются для первой карточки, но остаются частью полного справочника.
Разделы ниже сохраняют специальные правила официального PHP Manual: Одинаковые данные, тот же экземпляр или тот же заказ в базе?, Опечатка в свойстве: обнаружить динамическую запись, а не скрыть её, Анонимный класс: маленькая одноразовая реализация рядом с тестом, PHP 8.4: отложенная загрузка объекта, ghost и proxy, Не у каждого клиента есть адрес: чтение цепочки через ?->, Имя класса в переменной и выражение new: что именно создаётся, Свойство и метод с одинаковым именем: круглые скобки меняют действие, Автозагрузка: PHP просит объявление, загрузчик находит известный файл, Анонимный readonly-класс тоже может наследовать поведение — PHP 8.3+, Ленивый объект: ID уже известен, остальные данные ещё не загружены, Ошибка initializer: состояние объекта откатилось, внешний счётчик — нет, Clone и деструктор ленивого объекта: когда ресурс действительно существовал, Reset и mark: заменить жизненный цикл или заполнить объект самостоятельно, Readonly, массив и наследование не исчезают из-за lazy.
Одинаковые данные, тот же экземпляр или тот же заказ в базе?
Два репозитория вернули заказ №7. Это может быть один и тот же PHP-объект, два объекта с одинаковыми полями или две разные версии одной записи. Выберите вопрос до выбора оператора. Базовый опыт с присваиванием объекта уже показал общий экземпляр; теперь сравним отдельно созданные снимки.
Для обычных пользовательских объектов == сопоставляет класс и свойства, сравнивая значения свойств через ==. Оператор === требует именно тот же экземпляр. Для предметной идентичности часто нужен явный ID, а для равенства значений — собственный понятный equals, учитывающий только нужные поля. У объектов расширений правила == могут отличаться.
| Вопрос | Проверка в программе |
|---|---|
| Это та же карточка в памяти? | $first === $second |
| Это одинаковые свойства экземпляров одного пользовательского класса? | $first == $second; учитывать нестрогое сравнение значений |
| Это версии одного заказа в БД? | Сравнить проверенные идентификаторы заказа; версия и поля могут различаться |
- first и second созданы отдельно с одинаковыми полями
- same указывает на экземпляр first
- Статус second меняется независимо
- ID заказа может совпадать при разных снимках состояния
<?php
declare(strict_types=1);
class OrderView
{
public function __construct(public int $id, public string $status) {}
}
$first = new OrderView(7, 'new');
$second = new OrderView(7, 'new');
$alias = $first;
echo $first == $second ? "Данные равны\n" : "Данные различаются\n";
echo $first === $second ? "Один экземпляр\n" : "Два экземпляра\n";
echo $first === $alias ? "Alias ведёт к первому\n" : "Другая карточка\n";
$second->status = 'paid';
echo $first == $second ? "Данные равны\n" : "Данные различаются\n";
echo $first->id === $second->id ? "Один ID заказа\n" : "Разные ID\n";
Ожидаемый результат
Данные равны
Два экземпляра
Alias ведёт к первому
Данные различаются
Один ID заказа
Изменение second не затронуло first: они созданы разными new. Сравнение ID продолжает находить ту же запись, хотя её снимки расходятся. Сам ID не выбирает, какая версия новее; для конкурентного сохранения нужна версия/транзакция хранилища.
Не используйте spl_object_id как ID заказа: он обозначает текущий экземпляр, а после его уничтожения может использоваться вновь. Не сравнивайте большие графы объектов через == ради одного бизнес-ключа: обход свойств может оказаться дорогим, а рекурсивные связи требуют особой осторожности. Для простого объекта-значения обычно яснее метод equals, явно сравнивающий сумму и валюту через ===.
Попробуйте поменять только status у second и отдельно id. Предскажите каждый результат до запуска. Так вы отделите три вопроса, которые в разговоре часто называют одним словом «одинаковые». Правила сравнения не заменяют защиту обновления в БД.
Опечатка в свойстве: обнаружить динамическую запись, а не скрыть её
Импорт меняет название товара. Разработчик случайно написал titel вместо title, но программа продолжила работу. Это опаснее явного отказа: отчёт выглядит успешным, а нужное поле осталось старым. После обычного свойства и состояния без инициализации разберём другой случай — поле вообще не объявлено.
С PHP 8.2 создание большинства динамических свойств выдаёт E_DEPRECATED. В текущих PHP 8.x это ещё не означает автоматический запрет записи. В опыте обработчик фиксирует именно предупреждение об устаревшей возможности; в рабочем проекте правильное действие — исправить опечатку и схему, а не подавлять все предупреждения.
- Есть объявленное title = Старое
- Присваивание titel создаёт другое поле и выдаёт deprecation
- Чтение title всё ещё возвращает Старое
- Исправление должно обращаться к объявленному title
<?php
declare(strict_types=1);
class ImportProduct
{
public string $title = 'Старое';
}
$warnings = [];
set_error_handler(function (int $level, string $message) use (&$warnings): bool {
if ($level !== E_DEPRECATED) {
return false;
}
$warnings[] = 'Обнаружено устаревшее создание свойства';
return true;
});
try {
$product = new ImportProduct();
$product->titel = 'Новое';
} finally {
restore_error_handler();
}
echo $warnings[0], "\n";
echo 'Объявленное поле: ', $product->title, "\n";
echo 'Ошибочное поле: ', $product->titel, "\n";
$product->title = 'Новое';
echo 'После исправления: ', $product->title, "\n";
Ожидаемый результат
Обнаружено устаревшее создание свойства
Объявленное поле: Старое
Ошибочное поле: Новое
После исправления: Новое
Обработчик временный и ограничен учебным блоком; finally восстанавливает прежний. Use (&$warnings) позволяет обработчику добавить запись в тот же массив — механизм объяснён в главе ссылок. Мы не выводим внутренний текст предупреждения, содержащий путь файла. На будущей версии, меняющей динамические свойства, поведение нужно сверить заново.
StdClass — предусмотренный PHP контейнер для динамических полей; он подходит для заранее выбранной модели данных без методов. Классы с __set могут перехватывать запись в отсутствующие или недоступные поля. #[AllowDynamicProperties] разрешает динамическое расширение класса и наследников, но не обнаруживает опечатки и не проверяет бизнес-поля.
Если импорт действительно содержит произвольные дополнительные поля, храните их в отдельном массиве extra с проверенными ключами. Основные title, price и paid должны иметь явный договор. Особенно нельзя присваивать все ключи внешнего JSON свойствам по циклу: неизвестное поле не обязано становиться разрешённым действием над товаром.
В этой главе обработчик ошибки нужен для наблюдения. Для его самостоятельного изучения прочитайте ошибки и исключения и передачу связи с переменной.
Анонимный класс: маленькая одноразовая реализация рядом с тестом
Тесту отчёта нужен источник ровно двух названий. Создавать отдельный именованный класс и файл ради одного маленького поддельного источника необязательно. Запись new class одновременно описывает безымянный для вашего кода класс и создаёт объект. Нужны implements и договор метода и аргументы конструктора.
- Функция отчёта требует TitleSource
- new class получает два готовых названия через конструктор
- Объект реализует titles и возвращает именно эти данные
- Обычный потребитель интерфейса формирует строку
<?php
declare(strict_types=1);
interface TitleSource
{
public function titles(): array;
}
function reportTitles(TitleSource $source): string
{
return implode(', ', $source->titles());
}
$source = new class(['Чай', 'Кофе']) implements TitleSource {
private array $titles;
public function __construct(array $titles)
{
$this->titles = $titles;
}
public function titles(): array
{
return $this->titles;
}
};
echo reportTitles($source), "\n";
echo $source instanceof TitleSource ? "Договор выполнен\n" : "Другой тип\n";
Ожидаемый результат
Чай, Кофе
Договор выполнен
Аргумент после class передаётся конструктору. Implements имеет тот же смысл, что у именованного класса. Внешняя функция получает обычный объект интерфейсного типа; автоматической загрузки названий и скрытого HTTP здесь нет.
Анонимный класс может наследоваться, реализовать интерфейсы и включать трейты. Но вложенность его текста в другой класс не даёт доступа к приватному состоянию внешнего объекта: нужное значение передают явно. В отличие от замыкания, здесь нет use для захвата локальных переменных; use внутри класса означает трейт.
Движок выдаёт такому классу техническое имя. Не сохраняйте его как стабильное имя для конфигурации, сериализации или автозагрузки. Повторное выполнение одного объявления new class создаёт новые экземпляры одного и того же анонимного класса; это не глобальный singleton.
С PHP 8.3 можно объявить new readonly class; ограничения readonly при этом сохраняются. Для повторно используемого адаптера банка или понятного публичного типа лучше именованный класс с отдельным тестом. Анонимный класс выигрывает, когда его задача мала и понятна именно в этом месте.
PHP 8.4: отложенная загрузка объекта, ghost и proxy
Страница содержит карточку автора, но некоторые варианты ответа её не используют. Можно отложить заполнение карточки до обращения к данным. Это продвинутая инфраструктурная возможность PHP 8.4, а не обязательный способ создавать обычный товар. До примера нужны объект и свойство, замыкание с захватом объекта и объект Reflection.
Lazy ghost заполняет тот самый экземпляр на месте. Lazy proxy получает от фабрики другой реальный экземпляр и перенаправляет к нему работу со свойствами. Оба механизма создаются через ReflectionClass, а не одним дополнительным словом у new. Источник ниже — фиксированная строка, счётчик позволяет увидеть момент и число загрузок.
- Создан ghost: свойство ещё не заполнено, загрузок 0
- Чтение title вызывает initializer
- Initializer заполняет этот объект и увеличивает счётчик
- Повторное чтение получает готовое значение без второго initializer
<?php
declare(strict_types=1);
class AuthorCard
{
public string $title;
public function __construct(string $title) { $this->title = $title; }
}
class LoadCounter { public int $count = 0; }
$counter = new LoadCounter();
$reflection = new ReflectionClass(AuthorCard::class);
$card = $reflection->newLazyGhost(function (AuthorCard $card) use ($counter): void {
$counter->count++;
$card->__construct('Редактор каталога');
});
echo 'До чтения: ', $counter->count, "\n";
echo $card->title, "\n";
echo $card->title, "\n";
echo 'После чтений: ', $counter->count, "\n";
Ожидаемый результат
До чтения: 0
Редактор каталога
Редактор каталога
После чтений: 1
NewLazyGhost не вызывает наш обычный конструктор автоматически. Callback здесь сознательно вызывает __construct для заполнения текущего объекта. Обычное new AuthorCard всё ещё работает немедленно; в ленивой схеме момент подготовки изменён инфраструктурой.
<?php
declare(strict_types=1);
class ProfileCard
{
public function __construct(public string $title) {}
}
$real = new ProfileCard('Первая подпись');
$reflection = new ReflectionClass(ProfileCard::class);
$proxy = $reflection->newLazyProxy(function (ProfileCard $proxy) use ($real): ProfileCard {
return $real;
});
echo $proxy->title, "\n";
echo $proxy === $real ? "Один экземпляр\n" : "Разные экземпляры\n";
$proxy->title = 'Изменено через proxy';
echo $real->title, "\n";
Ожидаемый результат
Первая подпись
Разные экземпляры
Изменено через proxy
Для ясности настоящая карточка создана заранее; практическая фабрика может получить её позднее. После инициализации proxy перенаправляет доступ к real, но не становится тем же экземпляром по ===. Поэтому кэш, построенный на идентичности объектов, должен учитывать выбранную стратегию.
При стандартных настройках инициализацию вызывают операции со состоянием, в том числе обычное чтение и запись свойств, isset, клонирование и сериализация. Для отдельных операций существуют специальные флаги и исключения низкоуровневого API, которые здесь не включены. Сам вызов метода, который не обращается к состоянию, может не запускать её. Не объясняйте lazy как «любой метод сначала обращается к БД»: конкретный триггер важен для предсказуемого кода.
Если инициализатор выбросил исключение, объект возвращается к неинициализированному состоянию; внешние побочные эффекты callback этим не отменяются. Следующее обращение может снова попробовать инициализацию. Поэтому платежи и обязательные сообщения нельзя прятать в initializer. В реальном репозитории нужны политика ошибок и наблюдаемое число запросов.
Ленивые карточки не устраняют N+1: сто прочитанных объектов могут вызвать сто отдельных загрузок. Когда все карточки точно нужны, пакетная загрузка часто понятнее. Низкоуровневые skipLazyInitialization/resetAsLazyGhost предназначены для инфраструктуры с контролем жизненного цикла; не используйте их как обход проверки модели.
Не у каждого клиента есть адрес: чтение цепочки через ?->
В карточке клиента нужен город. Иногда клиент ещё не найден, иногда найден, но адрес не заполнен. Для этого можно написать несколько if. Оператор ?-> делает короткую цепочку чтения понятнее, когда отсутствие объекта — нормальное состояние, а не ошибка запроса.
Обычное -> требует объект слева. Запись ?-> проверяет именно null: если слева null, дальнейшая часть цепочки не выполняется и результат равен null. Затем ?? выбирает подпись для отсутствующего значения. Это два разных оператора с разными обязанностями.
- Клиент может отсутствовать
- У найденного клиента может не быть адреса
- ?-> пропускает оставшуюся цепочку после null
- Город берётся из адреса или заменяется понятной подписью
<?php
declare(strict_types=1);
class DeliveryAddress
{
public function __construct(public string $city) {}
}
class DeliveryCustomer
{
public function __construct(public ?DeliveryAddress $address) {}
}
$customers = [
null,
new DeliveryCustomer(null),
new DeliveryCustomer(new DeliveryAddress('Москва')),
];
foreach ($customers as $customer) {
echo $customer?->address?->city ?? 'Город не указан';
echo "\n";
}
Ожидаемый результат
Город не указан
Город не указан
Москва
Вопросительный знак перед DeliveryAddress в типе разрешает объект или null. Вопросительный знак в ?-> относится уже к операции чтения. Мы поставили его в обеих точках возможного отсутствия: клиент и адрес. Само имя поля city и его существование оператор не проверяет за модель.
Nullsafe не превращает false, строку или произвольный неправильный тип в отсутствующий объект. Он не перехватывает исключение, которое выбросил вызываемый метод. Если API обещал обязательного клиента, молчаливое Город не указан может скрыть отказ интеграции: тогда сначала разберите ошибку ответа.
При остановке цепочки аргументы пропущенного вызова тоже не вычисляются. Поэтому побочные эффекты внутри аргументов могут не произойти; не прячьте там обязательное сохранение. Через ?-> нельзя присваивать свойство или получать ссылку на него: это инструмент условного чтения. Оператор доступен с PHP 8.0.
Имя класса в переменной и выражение new: что именно создаётся
Формат отчёта выбирают из двух разрешённых вариантов. Выбор должен привести к конкретному классу экспортёра, а не к исполнению произвольной строки пользователя. После обычного new посмотрим, как имя класса становится данными.
CsvSummary::class возвращает полное имя класса строкой. New $className() создаёт объект класса, названного этой строкой. Круглые скобки после new передают аргументы конструктору, а не вызывают саму строку как функцию.
- Вход содержит выбранный формат csv
- Таблица разрешённых форматов возвращает CsvSummary::class
- new $className() создаёт CsvSummary
- Метод render возвращает строку отчёта
<?php
declare(strict_types=1);
interface SummaryRenderer
{
public function render(): string;
}
class CsvSummary implements SummaryRenderer
{
public function render(): string { return 'id;title'; }
}
class JsonSummary implements SummaryRenderer
{
public function render(): string { return '{"id":42}'; }
}
$allowed = ['csv' => CsvSummary::class, 'json' => JsonSummary::class];
$selected = 'csv';
$className = $allowed[$selected] ?? throw new InvalidArgumentException('Неизвестный формат');
$renderer = new $className();
echo $renderer::class, "\n";
echo $renderer->render(), "\n";
$another = new $renderer();
echo $another === $renderer ? "Тот же объект\n" : "Новый объект того же класса\n";
Ожидаемый результат
CsvSummary
id;title
Новый объект того же класса
::class у уже существующего объекта сообщает его фактический класс. New $renderer() использует класс этого объекта, но создаёт новый экземпляр: состояние прежнего не копируется. Здесь конструкторов нет; если бы конструктор требовал настройки, их пришлось бы передать и при таком создании.
<?php
declare(strict_types=1);
class ReportCaption
{
public function __construct(private string $month) {}
public function text(): string { return 'Отчёт: ' . $this->month; }
}
echo (new ReportCaption('октябрь'))->text(), "\n";
echo new ReportCaption('октябрь')->text(), "\n";
Ожидаемый результат
Отчёт: октябрь
Отчёт: октябрь
Весь этот пример требует PHP 8.4 из-за второй строки. Первая форма с внешними скобками работает в базовом PHP 8.2: скобки явно объединяют создание объекта перед ->. Обе записи вызывают конструктор и затем один метод; более короткая форма не делает объект постоянным между HTTP-запросами.
Текст от HTTP-клиента не должен становиться именем класса без проверки по вашей таблице. Создание класса способно вызвать автозагрузчик и конструктор с побочными эффектами. Когда набор вариантов мал, обычный match с new CsvSummary/new JsonSummary может быть ещё понятнее.
Сам CsvSummary::class не проверяет существование объявления и не запускает автозагрузку. Это удобный способ писать имена для контейнера или Reflection, но не доказательство, что класс установлен. Конструкции class/interface/trait/enum занимают одно пространство имён: разные виды объявления не разрешают одинаковое полное имя.
Имя класса начинается с буквы или подчёркивания, а не цифры; зарезервированное слово использовать нельзя. Одинокое имя _ устарело в PHP 8.4. В проекте выбирайте осмысленное имя по назначению и придерживайтесь одинакового регистра в файлах и PSR-4, даже если отдельные операции поиска класса нечувствительны к регистру.
Свойство и метод с одинаковым именем: круглые скобки меняют действие
Свойство хранит значение, метод выполняет действие. PHP допускает одинаковое имя в этих двух пространствах, но это может затруднить чтение. Полезно узнать правило, чтобы понимать чужой код и callback в свойстве.
- title читает значение поля
- title() вызывает объявленный метод
- (formatter) сначала читает Closure
- Последние () запускают полученную Closure
<?php
declare(strict_types=1);
class ReportView
{
public string $title = 'Сырые данные';
public Closure $formatter;
public function __construct()
{
$this->formatter = fn (string $text): string => '[' . $text . ']';
}
public function title(): string
{
return 'Готовый отчёт';
}
}
$view = new ReportView();
echo $view->title, "\n";
echo $view->title(), "\n";
echo ($view->formatter)('Заказы'), "\n";
Ожидаемый результат
Сырые данные
Готовый отчёт
[Заказы]
У formatter нет объявленного метода. Внешние скобки в ($view->formatter) сначала получают объект Closure из свойства, а следующие запускают его с текстом. Запись $view->formatter() искала бы метод formatter. Тип Closure разрешён для свойства; тип callable для свойства не поддерживается.
Для первой собственной модели лучше не называть поле и метод одинаково без ясной причины. Например, rawTitle и displayTitle() явно различают хранимую строку и её представление. Callback-свойство уместно для небольшого настраиваемого преобразования; обязательные действия полноценного клиента обычно удобнее видеть как именованные методы.
Автозагрузка: PHP просит объявление, загрузчик находит известный файл
Класс отчёта вынесен в отдельный файл. Хотим, чтобы его объявление загрузилось тогда, когда понадобится этот класс. Автозагрузка не скачивает пакет и не создаёт объект сама: она даёт программе шанс подключить объявление по запрошенному имени.
Spl_autoload_register получает callback и добавляет его в очередь загрузчиков. PHP передаст полное имя ещё неизвестного класса. Если загрузчик этим именем не занимается, он просто возвращает управление; следующий получает шанс. Успешное подключение файла определяет нужный class.
- main.php регистрирует два callback
- Строка CsvReport::class ещё не читает файл
- new запрашивает неизвестное объявление ReportDemo\CsvReport
- Второй загрузчик подключает разрешённый CsvReport.php
- После объявления new создаёт экземпляр
<?php
namespace ReportDemo;
class CsvReport
{
public function header(): string { return 'id;title'; }
}
Это сопутствующий файл следующего исполняемого примера. Сохраните его как CsvReport.php в той же папке; namespace является частью полного имени класса.
<?php
declare(strict_types=1);
$first = function (string $name): void {
echo "Первый загрузчик пропустил имя\n";
};
$second = function (string $name): void {
if ($name === 'ReportDemo\\CsvReport') {
require __DIR__ . '/CsvReport.php';
echo "Второй загрузчик подключил файл\n";
}
};
spl_autoload_register($first);
spl_autoload_register($second);
try {
$name = ReportDemo\CsvReport::class;
echo class_exists($name, false) ? "Класс уже загружен\n" : "Есть имя, объявления ещё нет\n";
$report = new $name();
echo $report->header(), "\n";
echo class_exists($name, false) ? "Теперь объявление есть\n" : "Нет объявления\n";
} finally {
spl_autoload_unregister($first);
spl_autoload_unregister($second);
}
Ожидаемый результат
Есть имя, объявления ещё нет
Первый загрузчик пропустил имя
Второй загрузчик подключил файл
id;title
Теперь объявление есть
В class_exists второй аргумент false запрещает запуск загрузки: это чистая проверка уже известных объявлений. Само ::class даёт строку. Загрузчики работают при new, а не при регистрации. После опыта они снимаются с очереди, чтобы не вмешиваться в другую программу.
<?php
declare(strict_types=1);
$laterCalls = 0;
$broken = function (string $name): void {
throw new RuntimeException('Учебный сбой загрузчика');
};
$later = function (string $name) use (&$laterCalls): void {
$laterCalls++;
};
spl_autoload_register($broken);
spl_autoload_register($later);
try {
try {
new MissingReportDemo();
} catch (RuntimeException $error) {
echo "Сбой прервал загрузку\n";
}
echo 'Вызовов следующего: ', $laterCalls, "\n";
} finally {
spl_autoload_unregister($broken);
spl_autoload_unregister($later);
}
Ожидаемый результат
Сбой прервал загрузку
Вызовов следующего: 0
Неизвестное вашей библиотеке имя — обычный пропуск, а не исключение. Здесь ошибка специально выброшена, чтобы показать остановку очереди. Настоящая поломка вашего известного файла должна быть диагностирована, но её нельзя спутать с отсутствием принадлежности имени этому загрузчику.
Файл выбирается из явно известного соответствия, а не путём конкатенации имени из HTTP с каталогом. Автозагрузка применяется также к интерфейсам, трейтам и enum. Установкой библиотек занимается Composer или другой менеджер; регистрацией созданного Composer загрузчика — require vendor/autoload.php. Дальше: класс → namespace → PSR-4 → файл.
Старая функция __autoload устарела в PHP 7.2 и удалена в PHP 8.0. Не добавляйте её в новый проект. Также не очищайте все чужие зарегистрированные загрузчики ради своего: несколько библиотек должны уметь сосуществовать. В обычном приложении используйте Composer, а ручной пример сохраняйте как объяснение механизма.
Анонимный readonly-класс тоже может наследовать поведение — PHP 8.3+
Нужен маленький неизменяемый вид заказа для одного теста. Ему полезен готовый конструктор родителя и общий метод подписи, но отдельное глобальное имя не требуется. Анонимный класс поддерживает extends, implements и use по обычным правилам совместимости.
- Родитель хранит readonly-номер
- Трейт умеет составлять подпись из этого номера
- new readonly class получает ORD-42
- Локальный объект работает по собранному договору
<?php
declare(strict_types=1);
readonly class OrderNumberView
{
public function __construct(public string $number) {}
}
trait HasOrderCaption
{
public function caption(): string
{
return 'Заказ ' . $this->number;
}
}
$view = new readonly class('ORD-42') extends OrderNumberView {
use HasOrderCaption;
};
echo $view->caption(), "\n";
try {
$view->number = 'ORD-43';
} catch (Error $error) {
echo "Номер уже задан\n";
}
Ожидаемый результат
Заказ ORD-42
Номер уже задан
Анонимный readonly-класс доступен с PHP 8.3; обычный readonly-класс — с PHP 8.2. Родитель тоже readonly, а трейт не приносит несовместимых свойств. Поскольку дочерний конструктор не объявлен, используется унаследованный.
Создание анонимного класса внутри метода внешнего объекта не даёт ему автоматического доступа к private-полям внешнего класса и не захватывает переменные как closure use. Передайте необходимые данные конструктору или используйте явный публичный договор. Новому объекту не нужно знать весь внешний клиент, если для подписи достаточно номера.
Все экземпляры одной анонимной декларации принадлежат одному техническому классу, но это не стабильное публичное имя для БД или конфигурации. Если тип нужен нескольким модулям, сериализованным заданиям или общей документации, именованный класс обычно яснее.
Ленивый объект: ID уже известен, остальные данные ещё не загружены
В строке заказа уже есть ID товара. Для печати ссылок полная карточка товара не нужна, а для заголовка нужна. Это конкретный повод откладывать загрузку. Сначала пройдите различие ghost и proxy. Методы ниже доступны с PHP 8.4; это инструменты инфраструктуры, а не обязательная оболочка каждого простого объекта.
SetRawValueWithoutLazyInitialization записывает известное значение и исключает поле из ленивой загрузки. SkipLazyInitialization исключает поле, используя его объявленное начальное значение, если оно есть. Обычная запись через ReflectionProperty::setValue, напротив, может запустить загрузку. Эти специальные операции не подходят для static, dynamic и virtual свойств.
- ID 42 записывается без загрузки
- Поле source получает объявленный default
- Чтение ID и source оставляет счётчик загрузок нулевым
- Чтение title один раз запускает initializer
<?php
declare(strict_types=1);
class LazyProduct
{
public int $id;
public string $source = 'catalog';
public string $title;
}
$loads = 0;
$class = new ReflectionClass(LazyProduct::class);
$product = $class->newLazyGhost(static function (LazyProduct $product) use (&$loads): void {
++$loads;
$product->title = 'Товар ' . $product->id;
});
$class->getProperty('id')->setRawValueWithoutLazyInitialization($product, 42);
$class->getProperty('source')->skipLazyInitialization($product);
echo $product->id, ': ', $product->source, '; загрузок ', $loads, "\n";
echo $product->title, '; загрузок ', $loads, "\n";
echo $product->title, '; загрузок ', $loads, "\n";
Ожидаемый результат
42: catalog; загрузок 0
Товар 42; загрузок 1
Товар 42; загрузок 1
В initializer нет сети: это счётчик и формирование строки. В реальном репозитории тут могла бы быть загрузка из БД с обработкой отсутствия строки. Вызов без состояния, например метод, возвращающий имя класса, сам по себе не обязан инициализировать объект. Если исключить из ленивости последнее ленивое поле, PHP больше не будет хранить ожидающий initializer.
<?php
declare(strict_types=1);
class ProductCaption
{
public function __construct(public string $title) {}
}
$class = new ReflectionClass(ProductCaption::class);
$ghost = $class->newLazyGhost(static function (ProductCaption $object): void {
$object->__construct('Ghost-карточка');
});
$proxy = $class->newLazyProxy(static fn (): ProductCaption => new ProductCaption('Proxy-карточка'));
$readyGhost = $class->initializeLazyObject($ghost);
$real = $class->initializeLazyObject($proxy);
echo $readyGhost === $ghost ? "Ghost остался собой\n" : "Иной объект\n";
echo $real === $proxy ? "Одинаковая идентичность\n" : "Proxy и real различаются\n";
$real->title = 'Обновлённая карточка';
echo $proxy->title, "\n";
Ожидаемый результат
Ghost остался собой
Proxy и real различаются
Обновлённая карточка
InitializeLazyObject вызывает штатную инициализацию. Для ghost возвращается тот же объект, для proxy — лежащий за ним реальный. Повторная инициализация готового объекта ничего не запускает заново; это отличается от reset.
Raw-запись обходит хуки поля. Такое наполнение должно находиться в контролируемом слое восстановления проверенных данных, иначе вы обойдёте собственную валидацию. Для обычной формы вызовите конструктор или предметный метод.
Упражнение: оставьте только поле id и исключите его из ленивости. Решение: незагруженных полей не останется, объект перестанет считаться ленивым; рассчитывать на будущий initializer для стороннего действия нельзя.
Ошибка initializer: состояние объекта откатилось, внешний счётчик — нет
Ленивая карточка впервые загружается в момент чтения title. Если загрузчик оборвался после частичного заполнения, читатель не должен получить половину состояния. PHP восстанавливает исходное состояние самого ленивого объекта и оставляет возможность следующей попытки. Однако уже отправленный запрос или запись во внешний журнал этим не отменяются.
Сначала проверьте маленький опыт с локальным счётчиком. Try/catch ловит RuntimeException вокруг именно того чтения, которое запустило работу. Ссылка use (&$attempts) даёт callback возможность менять внешнюю переменную.
- Первое чтение запускает попытку 1
- Initializer меняет поле и выбрасывает исключение
- Поле откатывается, объект снова ожидает загрузку
- Внешний attempts остаётся 1; следующее чтение делает попытку 2
<?php
declare(strict_types=1);
class RetryCaption
{
public string $title;
}
$attempts = 0;
$class = new ReflectionClass(RetryCaption::class);
$caption = $class->newLazyGhost(static function (RetryCaption $caption) use (&$attempts): void {
++$attempts;
$caption->title = 'Промежуточное значение';
if ($attempts === 1) { throw new RuntimeException('Данные ещё не доступны'); }
$caption->title = 'Готовый отчёт';
});
try { echo $caption->title; } catch (RuntimeException $error) { echo "Первая попытка не готова\n"; }
echo $class->isUninitializedLazyObject($caption) ? "Снова lazy\n" : "Уже готов\n";
echo array_key_exists('title', get_mangled_object_vars($caption)) ? "Поле осталось\n" : "Поле откатилось\n";
echo 'Попыток снаружи: ', $attempts, "\n";
echo $caption->title, '; попыток ', $attempts, "\n";
Ожидаемый результат
Первая попытка не готова
Снова lazy
Поле откатилось
Попыток снаружи: 1
Готовый отчёт; попыток 2
Get_mangled_object_vars здесь только проверяет своё публичное поле и не инициирует новую загрузку. Счётчик доказывает отсутствие общей транзакции. Для платежа повтор initializer недопустимо считать гарантированно безопасным: нужен отдельный протокол идемпотентности и сверки исхода.
Чтение, запись, isset, unset, обычный перебор свойств и стандартная сериализация способны впервые обратиться к состоянию. Неочевидный источник задержки — даже подготовка объекта к JSON. Помещайте границу перехвата ошибки там, где приложение действительно готово обработать отказ.
Упражнение: заставьте обе попытки завершаться ошибкой. Решение: объект продолжит ожидать загрузку, но число внешних попыток будет расти. Ограничение повторов и пауза не возникают из lazy-механизма; они проектируются отдельно.
Clone и деструктор ленивого объекта: когда ресурс действительно существовал
Нельзя считать clone дешёвым способом скопировать ещё не загруженную карточку: он сначала делает её готовой. У proxy затем копируется и реальный объект. Здесь полезно вспомнить поверхностную копию и время вызова деструктора.
В опыте класс считает собственные копирования и освобождения. Мы явно удаляем ссылки через unset, чтобы результат не зависел от завершения процесса. Никакие файлы, платежи или соединения в деструкторе не обрабатываются.
- Неинициализированный ghost удаляется без __destruct
- Clone proxy вызывает factory и получает real
- Копируется real, его __clone выполняется один раз
- Удаление каждой пары proxy/real освобождает её real
<?php
declare(strict_types=1);
class CountedCaption
{
public static int $copies = 0;
public static int $released = 0;
public function __construct(public string $title) {}
public function __clone(): void { ++self::$copies; }
public function __destruct() { ++self::$released; }
}
$class = new ReflectionClass(CountedCaption::class);
$unused = $class->newLazyGhost(static function (CountedCaption $object): void {
$object->__construct('Не запрашивали');
});
unset($unused);
echo 'Освобождено без загрузки: ', CountedCaption::$released, "\n";
$loads = 0;
$proxy = $class->newLazyProxy(static function () use (&$loads): CountedCaption {
++$loads;
return new CountedCaption('Исходная подпись');
});
$copy = clone $proxy;
$copy->title = 'Подпись копии';
echo $proxy->title, ' / ', $copy->title, "\n";
echo 'Загрузок: ', $loads, '; копий: ', CountedCaption::$copies, "\n";
unset($proxy, $copy);
echo 'Освобождено после работы: ', CountedCaption::$released, "\n";
Ожидаемый результат
Освобождено без загрузки: 0
Исходная подпись / Подпись копии
Загрузок: 1; копий: 1
Освобождено после работы: 2
Нет отдельного третьего деструктора для обёртки proxy: деструктор принадлежит реальному экземпляру. Если другая переменная держала бы real, он продолжал бы жить после unset(proxy). Копирование вложенных объектов по-прежнему поверхностное, если __clone не задаёт другой порядок.
Отложенная загрузка полезна, когда многие объекты останутся непрочитанными. Если каждый элемент обязательно нужен, она может дать множество отдельных запросов вместо одной пакетной загрузки. Проверьте число вызовов и время на своей выборке: lazy — механизм времени выполнения, а не обещание ускорения.
Упражнение: не клонируйте proxy и сразу удалите его. Решение: factory не исполнится, real не появится, а значит, освобождать его нечего. Ожидать из такого деструктора завершающего бизнес-действия ошибочно.
Reset и mark: заменить жизненный цикл или заполнить объект самостоятельно
В инфраструктурном коде иногда уже существующий экземпляр переводят обратно в отложенное состояние. Reset не выдаёт новый объект: все переменные, которые ссылались на старый, продолжают видеть ту же идентичность. Поэтому это вмешательство во все держатели объекта, а не локальная замена одной переменной.
ResetAsLazyGhost и resetAsLazyProxy доступны с PHP 8.4. Первый сбрасывает состояние для последующего заполнения того же объекта, второй делает его обёрткой будущего real. По умолчанию перед сбросом вызывается деструктор прежнего состояния, если он есть. Флаг SKIP_DESTRUCTOR — явное отключение этой стадии, а не обычная рекомендация.
- Создан объект с подписью A
- Вторая переменная ссылается на него же
- Reset сохраняет идентичность, но убирает старое состояние
- Чтение через любую ссылку получает состояние B
<?php
declare(strict_types=1);
class ResettableCaption
{
public function __construct(public string $title) {}
}
$caption = new ResettableCaption('A');
$alias = $caption;
$class = new ReflectionClass(ResettableCaption::class);
$class->resetAsLazyGhost($caption, static function (ResettableCaption $object): void {
$object->__construct('B');
});
echo $caption === $alias ? "Ссылка сохранилась\n" : "Новый объект\n";
echo $alias->title, "\n";
$class->resetAsLazyProxy($caption, static fn (): ResettableCaption => new ResettableCaption('C'));
$real = $class->initializeLazyObject($caption);
echo $caption === $alias ? "Обёртка та же\n" : "Обёртка другая\n";
echo $real === $caption ? "Real совпал\n" : "Real отдельный\n";
echo $alias->title, "\n";
Ожидаемый результат
Ссылка сохранилась
B
Обёртка та же
Real отдельный
C
Класс примера не владеет внешним ресурсом. У реального объекта нужно сначала понять его инварианты и ответственность деструктора. Reset не предназначен для ещё не инициализированного lazy-объекта; во время инициализации или перебора его свойств действуют дополнительные ограничения.
<?php
declare(strict_types=1);
class ManualCaption
{
public string $title;
public string $format = 'text';
}
$calls = 0;
$class = new ReflectionClass(ManualCaption::class);
$caption = $class->newLazyGhost(static function (ManualCaption $object) use (&$calls): void {
++$calls;
$object->title = 'Автоматическая подпись';
});
$class->markLazyObjectAsInitialized($caption);
echo 'Вызовов initializer: ', $calls, "\n";
echo 'Формат: ', $caption->format, "\n";
try { echo $caption->title; } catch (Error $error) { echo "Title ещё не заполнен\n"; }
$caption->title = 'Ручная подпись';
echo $caption->title, "\n";
Ожидаемый результат
Вызовов initializer: 0
Формат: text
Title ещё не заполнен
Ручная подпись
MarkLazyObjectAsInitialized снимает ленивость без вызова callback. Объявленный default format появился, но обязательный title остался uninitialized. Это инструмент для слоя, который сам восстановит корректное состояние, а не замена initializeLazyObject.
При reset отражённый класс задаёт область сбрасываемых объявленных полей; дополнительные свойства потомка не следует считать автоматически сброшенными. Есть и ограничения для final readonly-полей. При сбросе готового proxy его real отсоединяется; если ссылок на real больше нет, его деструктор выполняется даже с SKIP_DESTRUCTOR.
Для простой замены настройки обычно яснее new с новым конструктором: вызывающий код видит создание другой сущности. Упражнение: после mark проверьте title через ReflectionProperty::isInitialized. Решение: получите false до ручного присваивания и true после, хотя сам объект уже не lazy.
Readonly, массив и наследование не исчезают из-за lazy
Отложенное заполнение не отменяет правила класса. В снимке заказа можно один раз заполнить readonly-массив через конструктор, но после загрузки нельзя дописать строку в это свойство. Важно не спутать «ещё не заполнено» с «можно менять всегда».
Сначала сравним два состояния на PHP 8.4+: ghost не вызывает конструктор при создании; callback явно вызывает его при первом чтении. Затем readonly продолжает защищать присваивание и изменение элементов массива, как у обычного объекта.
- Ghost содержит ещё не заполненное поле items
- Чтение количества вызывает callback
- Конструктор задаёт массив из одного артикула
- Попытка добавить второй артикул запрещена
<?php
declare(strict_types=1);
readonly class OrderSnapshot
{
public function __construct(public array $items) {}
}
$class = new ReflectionClass(OrderSnapshot::class);
$snapshot = $class->newLazyGhost(static function (OrderSnapshot $snapshot): void {
$snapshot->__construct(['P-42']);
});
echo $class->isUninitializedLazyObject($snapshot) ? "До чтения: lazy\n" : "Уже готов\n";
echo 'Позиций: ', count($snapshot->items), "\n";
try { $snapshot->items[] = 'P-43'; } catch (Error $error) { echo "Снимок нельзя дополнить\n"; }
echo implode(', ', $snapshot->items), "\n";
Ожидаемый результат
До чтения: lazy
Позиций: 1
Снимок нельзя дополнить
P-42
Массив заполнен один раз. Если элементом был бы изменяемый объект, его внутреннее состояние readonly-класс не сделал бы автоматически неизменяемым. Об этом отдельно в разделе о поверхностной неизменяемости.
<?php
declare(strict_types=1);
class BaseProduct
{
public function __construct(public string $title) {}
}
class ProductProxy extends BaseProduct {}
class ExtendedProxy extends BaseProduct
{
public string $extra = 'Дополнительное поле';
}
$class = new ReflectionClass(ProductProxy::class);
$proxy = $class->newLazyProxy(static fn (): BaseProduct => new BaseProduct('Товар P-42'));
echo $proxy->title, "\n";
echo get_class($proxy), ' / ', get_class($class->initializeLazyObject($proxy)), "\n";
$incompatibleClass = new ReflectionClass(ExtendedProxy::class);
$incompatible = $incompatibleClass->newLazyProxy(static fn (): BaseProduct => new BaseProduct('P-43'));
try { echo $incompatible->title; } catch (Error $error) { echo "Лишнее поле proxy несовместимо\n"; }
Ожидаемый результат
Товар P-42
ProductProxy / BaseProduct
Лишнее поле proxy несовместимо
Обычный безопасный договор — factory возвращает готовый экземпляр того же класса. Разрешён и показанный случай: класс proxy — потомок класса real, без дополнительных свойств и без переопределения __clone/__destruct. Это не произвольная взаимозаменяемость любых родителей и потомков.
В callback сохраняются обычные область доступа и привязка $this. PHP не дарит внешней функции доступ ко всем private-полям: вызывайте подходящий метод класса или проектируйте контролируемую фабрику. Lazy доступны пользовательским классам и stdClass, но не произвольным встроенным классам; класс без подходящих полей не станет ленивым.
Упражнение: перенесите дополнительное поле из ExtendedProxy в BaseProduct. Решение: причина именно лишнего состояния исчезнет, но совместимость всех остальных правил всё равно нужна. В прикладном проекте начинайте с одного и того же класса для proxy и real.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
get_classПолучить имя класса уже созданного объекта для диагностики.
PHP 4+ · Core
get_class(object $object = ?): stringПолучить имя класса уже созданного объекта для диагностики.
- new Courier создаёт объект доставки
- get_class получает этот экземпляр
- Возвращается строка Courier; данные объекта остаются прежними
| Вход / результат | Точный смысл |
|---|---|
| $object | Готовый экземпляр; обычно передавайте его явно. Функция не создаёт и не клонирует объект. |
| Результат | Строка полного имени объявленного класса с namespace. Alias возвращает исходное имя, а не выбранный псевдоним. |
| Без аргумента | В методе исторически возвращается класс места объявления. Такой вызов deprecated с PHP 8.3; вне класса — Error. |
| Ошибки / версии | В PHP 8 null и другое значение, не являющееся объектом, дают TypeError; до 8.0 — E_WARNING. Явный null перестал быть допустим с 7.2; ранее совпадал с отсутствием аргумента. Вне класса без аргументов до 8.0 были warning и false, теперь Error. Для произвольного значения используйте get_debug_type. |
Скобки передают объект функции, а присваивание сохраняет возвращённую строку. Имя полезно в сообщении об ошибке подключения обработчика. Не заменяйте им интерфейс: объект другого класса может выполнять тот же контракт.
<?php
declare(strict_types=1);
class Courier {}
$delivery = new Courier;
echo get_class($delivery), "\n";
Ожидаемый результат
Courier
class задаёт имя вида объекта, new создаёт экземпляр. Получение имени не вызывает ни одного метода доставки.
<?php
declare(strict_types=1);
class CrmImporter {}
class CsvImporter {}
function describeHandler(object $handler): string
{
return 'Обработчик: ' . get_class($handler);
}
echo describeHandler(new CrmImporter), "\n";
echo describeHandler(new CsvImporter), "\n";
Ожидаемый результат
Обработчик: CrmImporter
Обработчик: CsvImporter
Одна функция составляет диагностический текст для разных объектов. object у параметра требует экземпляр, : string обещает строку результата, точка соединяет строки. Здесь выводится только имя типа, а не свойства с токенами.
<?php
declare(strict_types=1);
class BaseImporter
{
public function names(): array
{
return [self::class, get_class($this), static::class];
}
}
class CsvImporter extends BaseImporter {}
echo implode(' | ', (new CsvImporter)->names()), "\n";
Ожидаемый результат
BaseImporter | CsvImporter | CsvImporter
self относится к месту объявления метода; $this — фактический экземпляр; static использует позднее связывание. Это явные альтернативы старому вызову без аргумента. Они отвечают на разные вопросы, поэтому механическая замена на self::class не всегда сохраняет смысл.
Упражнение: передайте new CsvImporter через переменную с другим именем. Результат останется CsvImporter: функция читает класс экземпляра, а не имя переменной. Подробнее: self и static.
Рядом: get_parent_class, get_called_class, is_a. Как выбрать проверку.
Дополнительные примеры из практической главы
Возвращает полное имя класса с namespace. Неверный тип аргумента вызывает TypeError. Используйте результат для диагностики или известного registry, а полиморфное поведение выражайте интерфейсом.
<?php
declare(strict_types=1);
final class SandboxGateway {}
$gateway = new SandboxGateway();
echo get_class($gateway), "\n";
Ожидаемый результат
SandboxGateway
Полезно в техническом журнале сборки зависимостей.
<?php
declare(strict_types=1);
class ExportResult {}
final class CsvResult extends ExportResult {}
function result(): ExportResult { return new CsvResult(); }
echo get_class(result()), "\n";
Ожидаемый результат
CsvResult
Объявленный возвращаемый тип базовый, фактический объект конкретный. Для рабочего ветвления лучше контракт.
Рядом: instanceof, spl_object_id.
property_existsПроверить наличие свойства, даже если оно содержит null или ещё не инициализировано.
PHP 5.1+ · Core
property_exists(object|string $object_or_class, string $property): boolПроверить наличие свойства, даже если оно содержит null или ещё не инициализировано.
- Order объявляет nullable note и typed number без значения
- property_exists отвечает, есть ли поле в структуре
- isset отвечает о доступном ненулевом значении; чтение number пока ошибочно
| Вход / результат | Точный смысл |
|---|---|
| $object_or_class | Объект или имя класса; строка может включить autoload. Динамические поля проверяйте на конкретном экземпляре. |
| $property | Имя без $, чувствительное к регистру. price и Price — разные свойства. |
| Результат | true для объявленного свойства, включая null, private, typed без значения, а с PHP 8.4 и virtual hook-свойство. Это не чтение значения. |
| Границы / ошибки | __get не считается объявлением. Неверный тип первого аргумента — TypeError; отсутствующее имя класса обычно false. Private родителя не является унаследованным свойством класса-потомка. |
Существование полезно при исследовании DTO — объекта с данными. Для приёма формы нужна собственная схема, а не право записать всё, что найдено у объекта.
<?php
declare(strict_types=1);
class Order { public ?string $note = null; }
$order = new Order();
echo property_exists($order, 'note') ? "Схема поддерживает комментарий\n" : "Поля нет\n";
echo isset($order->note) ? "Покупатель написал комментарий\n" : "Комментарий пока не задан\n";
echo property_exists($order, 'Note') ? "Найдено поле Note\n" : "Регистр имени поля имеет значение\n";
Ожидаемый результат
Схема поддерживает комментарий
Комментарий пока не задан
Регистр имени поля имеет значение
property_exists проверяет поле, а isset — доступное значение, отличное от null. В имени свойства регистр важен: note и Note различны. Ни одна из этих проверок не подтверждает, что комментарий является непустым или безопасным для HTML.
<?php
declare(strict_types=1);
class Order { public int $number; }
$order = new Order();
$property = new ReflectionProperty($order, 'number');
echo property_exists($order, 'number') ? "Поле номера объявлено\n" : "Поля нет\n";
echo $property->isInitialized($order) ? "Можно читать номер\n" : "Сначала присвойте номер\n";
$order->number = 42;
echo $property->isInitialized($order) ? 'Накладная для заказа ' . $order->number . "\n" : "Номер не готов\n";
Ожидаемый результат
Поле номера объявлено
Сначала присвойте номер
Накладная для заказа 42
Наличие типизированного свойства не гарантирует его готовность. isInitialized относится к конкретному объекту. В нормальной модели обязательный номер задают конструктором; Reflection полезен техническому коду восстановления или диагностике.
<?php
declare(strict_types=1);
$row = new stdClass;
$row->label = 'Заказ 41';
var_dump(property_exists($row, 'label'));
var_dump(property_exists(stdClass::class, 'label'));
echo $row->label, "\n";
Ожидаемый результат
bool(true)
bool(false)
Заказ 41
stdClass допускает динамические поля. У обычных классов создание необъявленных свойств deprecated с PHP 8.2, кроме предусмотренных исключений: лучше объявлять поля явно.
Найденное private-поле всё равно закрыто для обычного внешнего доступа; private родителя на потомке может дать false, хотя его отдельное хранилище существует внутри объекта. С PHP 8.4 virtual property может существовать без хранимого значения. property_exists не исполняет getter, а get_object_vars может его исполнить. Не используйте наличие поля вместо правил чтения/записи свойств и областей доступа.
Рядом: get_object_vars, get_class_vars, get_mangled_object_vars. Как выбрать проверку.
Дополнительные примеры из практической главы
Возвращает true и для свойства со значением null, и для объявленного неинициализированного свойства. Не вызывает __get для поиска виртуальных значений.
<?php
declare(strict_types=1);
final class Delivery { public ?string $comment = null; }
$delivery = new Delivery();
echo property_exists($delivery, 'comment') ? "Поле есть\n" : "Нет\n";
echo isset($delivery->comment) ? "Значение есть\n" : "Значение null\n";
Ожидаемый результат
Поле есть
Значение null
Схема и заполненность — разные вопросы.
<?php
declare(strict_types=1);
final class ImportedItem { public string $externalId; }
echo property_exists(ImportedItem::class, 'externalId') ? "Ожидаемое поле\n" : "Другая схема\n";
echo property_exists(ImportedItem::class, 'priceFromClient') ? "Есть\n" : "Не объявлено\n";
Ожидаемый результат
Ожидаемое поле
Не объявлено
Для массового присваивания недостаточно проверять существование: нужен список разрешённых полей и валидация.
get_object_varsПолучить доступные текущие не-static свойства конкретного объекта как массив.
PHP 4+ · Core
get_object_vars(object $object): arrayПолучить доступные текущие не-static свойства конкретного объекта как массив.
- Delivery fee изменён с 300 на 700
- get_object_vars($delivery) читает доступное текущее состояние
- Массив содержит fee=700, без private и static полей снаружи
| Вход / результат | Точный смысл |
|---|---|
| $object | Созданный объект. Другое значение в PHP 8 вызывает TypeError. |
| Результат | Массив имя => текущее значение для доступных из scope не-static свойств. null включается, неинициализированное typed-свойство пропускается. |
| Область доступа | Снаружи видны public, из метода — доступные там private/protected. Возвращённый массив не является глубоким клоном вложенных объектов. |
| PHP 8.4 / ошибки | Declared get hooks выполняются, включая virtual-свойства; lazy-объект инициализируется. Их ошибки/исключения могут пройти наружу. Magic __get сам по себе не создаёт перечисляемые свойства. |
Минимальный опыт использует обычные public-свойства без перехватчиков. Для публичного API лучше собирать разрешённые ключи явно: новое public-поле не должно автоматически стать частью ответа.
<?php
declare(strict_types=1);
class Delivery
{
public $fee = 300;
public $note = null;
private $internalCode = 'demo';
public static $count = 0;
}
$delivery = new Delivery;
$delivery->fee = 700;
$values = get_object_vars($delivery);
echo $values['fee'], "\n";
echo implode(', ', array_keys($values)), "\n";
Ожидаемый результат
700
fee, note
private не виден из внешней области, static не относится к состоянию экземпляра. note существует и включается, несмотря на null.
<?php
declare(strict_types=1);
class OrderRow
{
public function __construct(public int $number, public int $totalMinor) {}
}
$rows = [new OrderRow(41, 1200), new OrderRow(42, 500)];
foreach ($rows as $row) {
$values = get_object_vars($row);
echo $values['number'], ': ', $values['totalMinor'], "\n";
}
Ожидаемый результат
41: 1200
42: 500
Здесь DTO намеренно содержит только учебные данные отчёта, колонки выбраны явно. Сокращённое объявление свойств в конструкторе разобрано в главе конструкторов. Не выводите весь массив клиенту и не пишите его в журнал, если в объекте могут появиться токены или персональные данные.
<?php
declare(strict_types=1);
class Price
{
public int $minor = 1200;
public int $reads = 0;
public string $label {
get {
$this->reads++;
return $this->minor . ' коп.';
}
}
}
$price = new Price;
$values = get_object_vars($price);
echo $values['label'], "\n";
echo "Вычислений подписи: ", $price->reads, "\n";
Ожидаемый результат
1200 коп.
Вычислений подписи: 1
PHP 8.4+. Перечисление через get_object_vars учитывает доступные hooked-свойства и может вызвать get. Поэтому операция не всегда является пассивным чтением сырых полей. Здесь видно одно вычисление подписи; getter с HTTP/БД сделал бы такой экспорт неожиданно дорогим.
<?php
declare(strict_types=1);
class Order { public int $number; }
$reflection = new ReflectionClass(Order::class);
$order = $reflection->newLazyGhost(static function (Order $order): void {
echo "Инициализация\n";
$order->number = 41;
});
echo get_class($order), "\n";
$values = get_object_vars($order);
echo $values['number'], "\n";
Ожидаемый результат
Order
Инициализация
41
PHP 8.4+. get_class узнаёт тип без инициализации. get_object_vars требуется значение, поэтому запускается initializer. Это локальная модель; настоящий ORM может в этот момент обратиться к базе. Повторное исследование в большом цикле способно создать лишние запросы.
Если нужны исходные stored-значения для специальной внутренней диагностики без hooks, изучите get_mangled_object_vars и его утечку закрытых полей. Если нужен JSON-ответ, используйте явный DTO/JsonSerializable и политику полей. Подробнее: свойства и hooks, Reflection.
Рядом: get_class_vars, get_mangled_object_vars, property_exists. Как выбрать проверку.
Дополнительные примеры из практической главы
Возвращает массив свойств, доступных из места вызова. Внешний вызов обычно видит публичные поля; внутри класса доступны его закрытые поля. Неинициализированные типизированные свойства пропускаются.
<?php
declare(strict_types=1);
final class Card { public string $title = 'Чай'; private string $token = 'demo'; }
echo json_encode(get_object_vars(new Card()), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), "\n";
Ожидаемый результат
{"title":"Чай"}
Снаружи token не вошёл, но это не повод полагаться на неявную схему публичного API.
<?php
declare(strict_types=1);
final class Draft { public int $id; public ?string $note = null; }
echo implode(',', array_keys(get_object_vars(new Draft()))), "\n";
Ожидаемый результат
note
Отсутствие id в массиве не означает, что оно не объявлено в классе.
Источники и обсуждения
- Синтаксис объектов
- Объекты и ссылки
- Типизированные и динамические свойства
- Сравнение объектов
- Присваивание объекта и копия — обсуждение проблемы
- AllowDynamicProperties
- Анонимные классы
- Ленивые объекты PHP 8.4
- Автозагрузка объявлений
- Регистрация загрузчика
- Снятие загрузчика
- Известное значение без lazy initialization
- Исключение свойства из ленивости
- Явная инициализация
- Проверка lazy-состояния
- Сырые свойства без инициализации
- Фабрика реального экземпляра
- Отложенное заполнение ghost
- Правила сброса ghost
- Правила сброса proxy
- Пометка без callback
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


