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

Дерево руководства
Глава 42 · От основ к архитектуре
Видимость: public, protected, private и контролируемая запись
Защищаем правила заказа, понимаем области доступа и новые property hooks/asymmetric visibility PHP 8.4 без путаницы с авторизацией.
Не разрешать отрицательный остаток обходом проверки
В магазине осталось 3 пачки чая. Если поле quantity открыто, любой участок программы может записать -7. Закроем прямую запись и оставим понятные операции: узнать остаток и зарезервировать товар.
Что получится. После резервирования двух пачек увидим Осталось: 1; попытка взять ещё две будет отклонена.
- reserve(2) принимает количество
- Проверяет положительность и наличие
- Меняет закрытое quantity с 3 на 1
- remaining() возвращает 1
Методы и $this; готовое состояние.
<?php
declare(strict_types=1);
class Stock
{
private int $quantity = 3;
public function remaining(): int
{
return $this->quantity;
}
}
$stock = new Stock();
echo 'Осталось: ', $stock->remaining(), "\n";
try {
$stock->quantity = -7;
} catch (Error $error) {
echo "Прямая запись закрыта\n";
}
Ожидаемый результат
Осталось: 3
Прямая запись закрыта
private допускает доступ внутри объявившего класса. public делает remaining доступным вызывающему коду. Это правило организации программы; оно не шифрует данные и не заменяет авторизацию пользователя.
Три области доступа: кто имеет право вызвать или прочитать
Остаток меняется через проверяемую операцию
Покупатель резервирует два товара из пяти. Следующая попытка взять четыре должна сохранить корректный остаток.
- 1Внутри Stock: available = 5↓
Private-поле принадлежит объекту. Обычный внешний код обращается к его публичным методам.
- 2reserve(2) → available = 3↓
Метод проверяет положительное количество и достаточный запас, затем вычитает 2.
- 3reserve(4) → отказ↓
Четыре больше оставшихся трёх. Исключение возникает до записи; available остаётся 3.
- 4available() → 3
Публичное чтение сообщает результат, не выдавая внешний способ произвольно переписать поле.
Видимость регулирует доступ из PHP-кода. Она не проверяет личность покупателя и не заменяет разрешение на резервирование; эти проверки находятся на границе сценария.
| Запись в примере | Что делает каждый элемент |
|---|---|
public | Доступ из любого разрешённого контекста: внешний код может вызвать операцию. |
private | Доступ из области объявившего класса. Другой метод этого же класса может работать с private у другого его экземпляра. |
protected | Доступ из класса и совместимых областей наследования. Применять, когда расширение действительно предусмотрено. |
Не делайте все поля protected «на будущее»: потомки начнут зависеть от устройства хранения. Часто устойчивее private-данные и небольшие protected-операции. У свойств и методов без явной видимости обычно public, поэтому в учебном коде видимость пишем явно. Увеличить открытость при допустимом переопределении можно; сужение нарушает договор родителя.
Одна операция проверяет вход и меняет состояние
<?php
declare(strict_types=1);
class Stock
{
public function __construct(private int $quantity)
{
if ($quantity < 0) {
throw new InvalidArgumentException('Остаток должен быть неотрицательным');
}
}
public function reserve(int $amount): void
{
if ($amount < 1 || $amount > $this->quantity) {
throw new DomainException('Недостаточно товара или неверное количество');
}
$this->quantity -= $amount;
}
public function remaining(): int { return $this->quantity; }
}
$stock = new Stock(3);
$stock->reserve(2);
echo 'Осталось: ', $stock->remaining(), "\n";
try {
$stock->reserve(2);
} catch (DomainException $error) {
echo $error->getMessage(), "\n";
}
echo 'После отказа: ', $stock->remaining(), "\n";
Ожидаемый результат
Осталось: 1
Недостаточно товара или неверное количество
После отказа: 1
amount — сколько запрашивают сейчас; quantity — сохранённый остаток. || означает «или». -= вычитает и сохраняет новое значение. Проверка стоит перед изменением, поэтому отказ не оставляет частично изменённый остаток.
В реальном магазине два HTTP-запроса могут резервировать один остаток одновременно. private защищает правила одного объекта, но не даёт блокировку общей БД. Атомарное обновление и транзакции — отдельный уровень защиты.
Свойство с проверкой записи: property hooks — PHP 8.4+
Для обычного свойства настройки может быть удобна запись $card->title = "Чай", которая сама проверяет значение. Hook — объявленный рядом со свойством обработчик чтения get или записи set. Операцию reserve с отдельным смыслом всё равно удобнее оставить методом.
<?php
declare(strict_types=1);
class ProductCard
{
public string $title = 'Чай' {
set {
$value = trim($value);
if ($value === '') {
throw new InvalidArgumentException('Название не должно быть пустым');
}
$this->title = $value;
}
}
}
$card = new ProductCard();
$card->title = ' Чёрный чай ';
echo $card->title, "\n";
try {
$card->title = '';
} catch (InvalidArgumentException $error) {
echo $error->getMessage(), "\n";
}
Ожидаемый результат
Чёрный чай
Название не должно быть пустым
PHP 8.4+. Неявный $value — новое значение, пришедшее в set. Внутри собственного хука $this->title обращается к хранилищу свойства без повторного вызова этого же хука. В PHP 8.2–8.3 используйте private-поле и rename(). Начальное значение задавайте корректным: значение по умолчанию не является обычной внешней записью через set.
Читать снаружи, записывать внутри: private(set) — PHP 8.4+
Бывает, что чтение $stock->quantity удобно, но любая запись должна проходить через reserve. Раздельная видимость задаёт открытое чтение и закрытую запись без отдельного getter-метода.
<?php
declare(strict_types=1);
class Stock
{
public private(set) int $quantity = 3;
public function reserveOne(): void
{
if ($this->quantity < 1) {
throw new DomainException('Товар закончился');
}
--$this->quantity;
}
}
$stock = new Stock();
$stock->reserveOne();
echo 'Осталось: ', $stock->quantity, "\n";
Ожидаемый результат
Осталось: 2
Первое public относится к чтению, private(set) — к записи. -- уменьшает число на один; эта запись разрешена внутри Stock. Видимость записи не может быть шире чтения. private(set)-свойство не допускает переопределения в потомке: оно неявно final.
Выбирать доступ по операциям, которые обещает объект
| Потребность | Публичный договор | Внутреннее устройство |
|---|---|---|
| Показать остаток | remaining(): int либо public private(set) quantity | Где хранится число |
| Зарезервировать товар | reserve(amount) | Проверка диапазона и изменение |
| Сохранить результат запроса | log(event, data) | Куда и как писать журнал |
Попробуйте. Добавьте release(int $amount), чтобы вернуть отменённый резерв. Запретите ноль и отрицательное количество.
Разобрать решение
Внутри Stock проверьте $amount < 1 и бросьте InvalidArgumentException; затем увеличьте quantity на amount. Для настоящего заказа храните ещё связь резерва с заказом, иначе произвольный release способен создать товар из воздуха.
Видимость не равна проверке данных, неизменяемости и безопасности
private не проверяет цену и не спасает от неверной логики внутри класса. readonly запрещает определённые записи, но не заменяет проверку диапазона. protected не означает доступ для любого внешнего кода. __get/__set могут перехватывать недоступные обращения, но тогда их договор нужно объяснить отдельно. Для одного простого преобразования публичная функция с явными входами может быть яснее класса с десятком getter/setter.
Не обещайте ускорение от private. Польза здесь — ограниченное число мест изменения и понятные операции. Проверяйте результат успешного и отклонённого вызова, как в резервировании выше.
Дальше: полный договор языка и специальные случаи
Далее уточняются виртуальные свойства, более широкий вход setter, наследование hooks, ссылки, массивы и экспорт. Для PHP 8.2 базовый вариант с private и методами остаётся полноценным решением.
Разделы ниже сохраняют специальные правила официального PHP Manual: Вычисляемое свойство: итог строки заказа без второго хранилища — PHP 8.4+, Set принимает удобный ввод, поле хранит один нормализованный тип — PHP 8.4+, Разные права чтения и записи у static-свойства — PHP 8.5+, Метод может читать private-поле другого объекта того же класса, Наследование хуков: расширить проверку и вызвать родителя — PHP 8.4+, Почему items[] может обойти проверку set и поэтому запрещён — PHP 8.4+, Что экспортируется: backing-значение или результат get — PHP 8.4+, Protected(set): потомок меняет состояние, внешний код только читает — PHP 8.4+, Хук &get явно выдаёт право менять полученное значение — PHP 8.4+.
Вычисляемое свойство: итог строки заказа без второго хранилища — PHP 8.4+
В строке заказа уже есть цена и количество. Если отдельно хранить total, придётся не забывать менять его при каждой правке количества. Виртуальный get вычисляет представление из текущих данных. Сначала нужны свойства и методы и обычный хук с хранимым значением.
Запись public int $total { get => выражение; } объявляет свойство с чтением, у которого нет отдельной ячейки значения. Стрелка здесь относится к короткому телу get. В отличие от функции fn, мы объявляем поведение свойства; обращение остаётся $line->total, без круглых скобок.
- price = 120 и quantity = 2
- Чтение total вычисляет 240
- quantity становится 3
- Следующее чтение total вычисляет 360
<?php
declare(strict_types=1);
class BasketLine
{
public int $total {
get => $this->price * $this->quantity;
}
public function __construct(public int $price, public int $quantity) {}
}
$line = new BasketLine(120, 2);
echo $line->total, "\n";
$line->quantity = 3;
echo $line->total, "\n";
try {
$line->total = 1;
} catch (Error $error) {
echo "Итог нельзя присвоить напрямую\n";
}
Ожидаемый результат
240
360
Итог нельзя присвоить напрямую
Числа маленькие и заранее известны: пример объясняет механизм, а не проверку денег. Для внешнего ввода нужны ограничения цены, количества и переполнения. У get нет обращения к собственному $this->total — здесь нет backing storage, и отсутствующий set запрещает запись.
| Решение | Когда понятно использовать |
|---|---|
| Обычное поле | Хранить уже проверенное исходное значение: quantity. |
| Виртуальный get | Быстро и предсказуемо представить текущее состояние: total. |
| Метод total() | Сделать вычисление явным; подходит и до PHP 8.4. |
| Метод loadPrice() | Явно обозначить медленную операцию с внешним источником. |
Хуки не поддерживаются у static-свойств и несовместимы с readonly-свойствами. Хук set может принимать более широкий допустимый тип, чем хранит поле: например, принять строковую форму числа и сохранить int после проверки. Это удобство не отменяет валидацию.
Виртуальный get не означает кэш: каждое чтение выполняет его тело снова. Не прячьте SQL или HTTP в простое чтение свойства без ясного договора — цикл по товарам может внезапно стать сотней запросов. Для дорогого действия метод и отдельный кэш обычно легче заметить и измерить.
Set принимает удобный ввод, поле хранит один нормализованный тип — PHP 8.4+
В настройке уведомлений код использует bool, а импортированный файл присылает yes/no. Можно сделать явный метод setEnabled, а можно закрепить преобразование в set свойства. Внутреннее значение остаётся bool: остальной код не должен каждый раз вспоминать строковые варианты.
После слова set в скобках указываются тип и имя входного значения. Здесь bool|string шире типа самого поля bool. Это допустимо: set принимает оба вида и обязан привести их к тому, что поле может хранить. Внутри хука $this->enabled обращается к собственному хранилищу и не вызывает тот же set по кругу.
- В set приходит строка yes
- Match проверяет разрешённый вариант
- В поле enabled сохраняется bool true
- Неизвестная строка отклоняется до изменения состояния
<?php
declare(strict_types=1);
class NotificationFlag
{
public bool $enabled = false {
set(bool|string $value) {
if (is_string($value)) {
$value = match ($value) {
'yes' => true,
'no' => false,
default => throw new InvalidArgumentException('Ожидаются yes или no'),
};
}
$this->enabled = $value;
}
}
}
$flag = new NotificationFlag();
$flag->enabled = 'yes';
echo $flag->enabled ? "Уведомления включены\n" : "Уведомления выключены\n";
$flag->enabled = false;
try {
$flag->enabled = 'perhaps';
} catch (InvalidArgumentException $error) {
echo "Неизвестный вариант отклонён\n";
}
echo $flag->enabled ? "Уведомления включены\n" : "Уведомления выключены\n";
Ожидаемый результат
Уведомления включены
Неизвестный вариант отклонён
Уведомления выключены
Тип свойства ограничивает хранение, тип параметра set — вход операции. Начальное false известно заранее. Хук проверяет новое значение до присваивания; ошибка не меняет прежнее false. Случай с perhaps не превращается в true, как при неосмотрительном приведении любой непустой строки к bool.
Удобство здесь ограничено явно разрешёнными строками. Не добавляйте бесконечные неявные догадки о пользовательском вводе внутрь модели: договор импорта должен быть определён. Если преобразование зависит от языка интерфейса, версии API или нескольких полей, отдельный слой разбора входа обычно понятнее.
Для сложных операций оставьте именованный метод с явным результатом. Хуки не должны неожиданно отправлять уведомление при каждом присваивании: настройка enabled и действие send — разные решения. Чтение enabled в этом примере не имеет своего get и возвращает сохранённый bool.
Разные права чтения и записи у static-свойства — PHP 8.5+
У обработчика импорта есть счётчик уже принятых строк. Внутри класса его увеличивают, снаружи читают для отчёта. Обычный публичный static позволил бы любому коду обнулить счётчик. После асимметричной видимости экземпляра можно перенести ту же идею на состояние класса — эта возможность добавлена только в PHP 8.5.
- accept вызывается дважды
- Метод увеличивает общую accepted
- Отчёт читает accepted = 2
- Прямая запись снаружи отклоняется
<?php
declare(strict_types=1);
class ImportedRows
{
public private(set) static int $accepted = 0;
public static function accept(): void
{
self::$accepted++;
}
}
ImportedRows::accept();
ImportedRows::accept();
echo 'Принято: ', ImportedRows::$accepted, "\n";
try {
ImportedRows::$accepted = 0;
} catch (Error $error) {
echo "Снаружи запись запрещена\n";
}
Ожидаемый результат
Принято: 2
Снаружи запись запрещена
Public относится к чтению, private(set) — к изменению из объявившего класса. Static означает общую ячейку класса в текущем процессе. Это не общий счётчик всех веб-запросов: для него понадобятся хранилище и согласованное атомарное обновление.
Права записи не могут быть шире прав чтения; свойство должно иметь тип. Private(set) делает свойство final, поэтому потомок не может заменить его другим объявлением. Protected(set) разрешает изменение также потомкам и подходит, когда это действительно часть расширяемого договора.
Модификация элемента массива в свойстве и получение ссылки на изменяемое поле тоже требуют права set. Считать, что $object->items[] — это «только чтение items», ошибочно. Для readonly и хуков действуют дополнительные правила: механизмы нельзя произвольно складывать только по похожему названию.
Метод может читать private-поле другого объекта того же класса
При сложении сумм нужно убедиться, что валюты одинаковы. Поле currency закрыто, но сравнение выполняет код самого класса суммы. Private ограничивает доступ областью класса, а не отдельным экземпляром: метод может осмысленно сравнить два таких объекта, не делая поле публичным.
- first хранит закрытую валюту RUB
- Метод sameCurrency получил другой объект того же класса
- Код класса читает закрытое поле other
- Сравнение возвращает bool; внешнему коду поле не открыто
<?php
declare(strict_types=1);
class CurrencyAmount
{
public function __construct(private int $minor, private string $currency) {}
public function sameCurrency(self $other): bool
{
return $this->currency === $other->currency;
}
}
$first = new CurrencyAmount(120, 'RUB');
$second = new CurrencyAmount(240, 'RUB');
$third = new CurrencyAmount(100, 'EUR');
echo $first->sameCurrency($second) ? "Одна валюта\n" : "Разные валюты\n";
echo $first->sameCurrency($third) ? "Одна валюта\n" : "Разные валюты\n";
Ожидаемый результат
Одна валюта
Разные валюты
Self в параметре означает экземпляр текущего класса или допустимого потомка. $this — первая сумма, $other — переданная. Оба закрытых currency читаются внутри области CurrencyAmount. Числа и валюты известны заранее; полноценный Money с проверкой и операциями разобран отдельно.
Это не открывает private извне и не даёт любому потомку доступ к закрытому полю родителя. В наследовании одноимённое private-поле потомка может быть отдельным состоянием, что легко запутывает модель. Для совместной работы разных типов обычно понятнее публичный договор операций, чем попытки читать чужие внутренности.
Наследование хуков: расширить проверку и вызвать родителя — PHP 8.4+
Базовый артикул нормализуется в верхний регистр. Для конкретного поставщика дополнительно нужен префикс v-. Если дочерний set просто запишет значение, обработка родителя пропадёт. Вызов parent::$sku::set($value) явно продолжает общий алгоритм.
Здесь parent:: выбирает родительское объявление; $sku — имя свойства, а ::set(...) запускает именно его операцию записи. Это специальная запись хуков, а не доступ к обычному static-полю. Вызывать таким образом можно родительский хук своего же свойства.
- VendorSku получает v-42
- Дочерний set проверяет префикс
- parent::$sku::set передаёт нормализацию родителю
- Хранилище получает V-42; унаследованный get читает его
<?php
declare(strict_types=1);
class NormalizedSku
{
public string $sku {
final get => $this->sku;
set => strtoupper(trim($value));
}
}
class VendorSku extends NormalizedSku
{
public string $sku {
set {
if (!str_starts_with($value, 'v-')) {
throw new InvalidArgumentException('Требуется префикс v-');
}
parent::$sku::set($value);
}
}
}
$item = new VendorSku();
$item->sku = 'v-42';
echo $item->sku, "\n";
try {
$item->sku = 'other-42';
} catch (InvalidArgumentException $error) {
echo "Чужой префикс отклонён\n";
}
echo $item->sku, "\n";
Ожидаемый результат
V-42
Чужой префикс отклонён
V-42
Set в дочернем классе заменяет только set, а get наследуется. Final у get запрещает заменить конкретно чтение; это уже, чем final всего свойства. Проверка префикса намеренно требует вход v- в нижнем регистре, после чего родитель сохраняет верхний регистр. Это договор учебного поставщика, не универсальное правило всех SKU.
Если потомок переобъявляет свойство с хуками, начальное значение родителя автоматически не переносится: при необходимости задайте его заново. Родительский get или set не вызывается сам из-за совпадения имени. Добавление set в потомке также может сделать прежние операции записи несовместимыми с новым правилом — проверяйте подстановку, а не только синтаксис.
Хук выполняется в области объекта и может вызывать его закрытые вспомогательные методы. Доступ к другому свойству из хука не обходит хуки того свойства. Исключение прямого доступа к собственной backing-ячейке не означает общий «сырой доступ» ко всему объекту.
Почему items[] может обойти проверку set и поэтому запрещён — PHP 8.4+
В коллекцию допускаются только непустые названия. Set проверяет весь массив перед сохранением. Если позволить изменить отдельный элемент по ссылке, проверка целой записи могла бы не выполниться. Поэтому работа с элементами hooked-массива имеет особые ограничения.
- Поле хранит массив [Чай]
- Прямой items[] мог бы обойти set и отклоняется
- Локальная копия получает Кофе
- Присваивание целого массива снова проходит set
<?php
declare(strict_types=1);
class SelectedTitles
{
public array $items = [] {
set {
foreach ($value as $title) {
if (!is_string($title) || $title === '') {
throw new InvalidArgumentException('Нужны непустые названия');
}
}
$this->items = $value;
}
}
}
$selection = new SelectedTitles();
$selection->items = ['Чай'];
try {
$selection->items[] = 'Кофе';
} catch (Error $error) {
echo "Прямое изменение элемента запрещено\n";
}
$updated = $selection->items;
$updated[] = 'Кофе';
$selection->items = $updated;
echo implode(', ', $selection->items), "\n";
Ожидаемый результат
Прямое изменение элемента запрещено
Чай, Кофе
Чтение массива в updated получает обычное значение; оно не является ссылкой на поле. Мы меняем локальный набор и затем присваиваем весь результат, поэтому set снова проверяет каждый элемент. Для больших наборов это не обещание дешёвого изменения: приходится учитывать копирование и повторный полный обход.
Хук &get специально возвращает ссылку. У backed-свойства нельзя одновременно определить &get и set: запись через ссылку обходила бы set. У виртуального свойства правила отличаются, и изменение возвращённого массива не обязательно меняет объект; это зависит от реализации. Не переносите поведение обычного массива на любой get.
Для прикладной коллекции метод addTitle часто лучше: он явно проверяет один вход и изменяет собственное хранилище. Использование хуков ради более короткого items[] здесь не даёт выигрыша. Перед оптимизацией измерьте реальные размеры набора, а не делайте все поля ссылочными.
Что экспортируется: backing-значение или результат get — PHP 8.4+
Хук может показывать значение иначе, чем оно хранится. Это видно при сравнении JSON и внутреннего снимка PHP. Нельзя считать, что любой экспорт проходит ваш get или что любое восстановление автоматически выполняет set.
- В backing storage хранится tea
- Get показывает TEA
- JSON читает через get
- Обычная сериализация сохраняет исходное tea
<?php
declare(strict_types=1);
class ExportTitle
{
public string $title = 'tea' {
get => strtoupper($this->title);
}
}
$item = new ExportTitle();
echo $item->title, "\n";
echo ((array) $item)['title'], "\n";
echo json_encode($item, JSON_THROW_ON_ERROR), "\n";
$saved = serialize($item);
$restored = unserialize($saved, ['allowed_classes' => [ExportTitle::class]]);
if (!$restored instanceof ExportTitle) {
throw new UnexpectedValueException('Ожидался ExportTitle');
}
echo ((array) $restored)['title'], "\n";
Ожидаемый результат
TEA
tea
{"title":"TEA"}
tea
Get вычисляет TEA, а в backing storage остаётся tea. Приведение к массиву показывает сырое значение. Json_encode читает публичное свойство через get. Обычный serialize без собственных магических методов сохраняет сырой вариант; круговое восстановление использует только нашу доверенную строку.
| Операция без собственного протокола | Что берётся у hooked-свойства |
|---|---|
| var_dump, serialize, unserialize, (array), get_mangled_object_vars | Backing-значение; get/set не являются общей защитой этого пути. |
| var_export, json_encode, get_object_vars | Чтение проходит через get. |
| __serialize, __unserialize, JsonSerializable | Ваш код выбирает поля и операции; обычное чтение/запись в нём следует хукам. |
Для виртуального свойства отдельного backing-значения нет: сырые представления не должны восприниматься как полный публичный вид модели. Для устойчивого API задайте состав полей явно через JsonSerializable или DTO.
Не используйте хук как единственный барьер при восстановлении сериализованных данных. Свой __unserialize должен проверять схему и инварианты; пример такого восстановления находится в закладке импорта. Внешние сообщения по-прежнему разбирайте из безопасного формата с явной валидацией.
Protected(set): потомок меняет состояние, внешний код только читает — PHP 8.4+
Базовый импортёр показывает текущий этап, а конкретный импортёр меняет его после обработки. Если set закрыт как private, потомок не сможет выполнять это действие. Protected(set) включает в разрешённую область сам класс и его наследников.
- BaseImport объявил публично читаемый status
- CrmImport как потомок выполняет finish
- Внутренний метод записывает done
- Внешняя прямая запись не входит в protected-область
<?php
declare(strict_types=1);
class BaseImport
{
public protected(set) string $status = 'new';
}
class CrmImport extends BaseImport
{
public function finish(): void
{
$this->status = 'done';
}
}
class EditableImport extends BaseImport
{
public string $status = 'new';
}
$import = new CrmImport();
$import->finish();
echo $import->status, "\n";
try {
$import->status = 'new';
} catch (Error $error) {
echo "Внешняя запись запрещена\n";
}
$editable = new EditableImport();
$editable->status = 'review';
echo $editable->status, "\n";
Ожидаемый результат
done
Внешняя запись запрещена
review
EditableImport сознательно расширяет запись до public: права можно сделать доступнее, если свойство не final. Это не автоматическое свойство наследования; собственное объявление меняет договор. Сужать доступные права потомок не должен. Для неизменяемого публичного контракта выбирайте final или другую модель расширения.
<?php
declare(strict_types=1);
class BaseReceipt
{
public readonly string $number;
}
class ImportedReceipt extends BaseReceipt
{
public function __construct(string $number)
{
$this->number = $number;
}
public function replace(string $number): void
{
$this->number = $number;
}
}
$receipt = new ImportedReceipt('INV-42');
echo $receipt->number, "\n";
try {
$receipt->replace('INV-43');
} catch (Error $error) {
echo "Readonly уже инициализировано\n";
}
Ожидаемый результат
INV-42
Readonly уже инициализировано
С PHP 8.4 неявная область записи readonly стала protected(set), поэтому потомок может выполнить первую инициализацию. После неё запрет повторного присваивания остаётся. В PHP 8.2–8.3 этот путь нужно организовать через конструктор или метод объявившего поле родителя.
Readonly можно явно сузить до private(set), если инициализация должна принадлежать только объявившему классу. Асимметричная видимость обычного изменяемого поля и readonly отвечают на разные вопросы: кто может писать и сколько раз разрешено инициализировать значение. Проверяйте оба правила.
Хук &get явно выдаёт право менять полученное значение — PHP 8.4+
В проверяемом hooked-массиве set требовал заменить весь набор. Другой договор возможен: коллекция сознательно отдаёт изменяемую связь со своим массивом. Для этого get предваряют знаком &: получатель работает с тем же хранилищем.
Такая связь — не дополнительная проверка. Она расширяет возможности потребителя. Сначала разберите изменение источника через ссылки; для обычной коллекции с инвариантами именованный add часто безопаснее для модели.
- BackedTags хранит свой массив tags
- &get возвращает связь с ним
- tags[] записывает в ту же ячейку
- VirtualTags может вернуть связь с другим собственным полем
<?php
declare(strict_types=1);
class BackedTags
{
public array $tags = ['php'] {
&get { return $this->tags; }
}
}
class VirtualTags
{
private array $stored = ['php'];
public array $tags {
&get { return $this->stored; }
}
public function count(): int { return count($this->stored); }
}
$backed = new BackedTags();
$backed->tags[] = 'web';
echo implode(', ', $backed->tags), "\n";
$virtual = new VirtualTags();
$virtual->tags[] = 'api';
echo implode(', ', $virtual->tags), "\n";
echo 'Внутри virtual: ', $virtual->count(), "\n";
Ожидаемый результат
php, web
php, api
Внутри virtual: 2
Первый хук ссылается на собственное $this->tags: у свойства есть backing storage. Второй отдаёт ссылку на другое поле stored, поэтому tags виртуальное, но изменение всё равно достигает объекта. Место хранения определяется реализацией, а не одним наличием имени tags.
У backed-свойства &get нельзя совместить с set: изменение через ссылку обошло бы проверку set. Обычный get и &get одновременно тоже объявить нельзя. У виртуального свойства &get и set могут сосуществовать, потому что общего backing storage у них нет; автор обязан объяснить, с какими данными работают обе операции.
Если get возвращает вычисленный массив по значению, изменение полученной копии не обязано менять объект. Отдельно проверяйте поведение прямой записи в элемент такого выражения: ссылочный протокол не возникает сам. Не используйте &get просто для обхода неудобной ошибки set.
Практическое применение — адаптер существующей изменяемой структуры, где такой доступ уже является договором. Для списка заказов с проверкой прав и состояний выдача ссылки обычно чрезмерна: потребитель может добавить непроверенное значение, минуя предметную операцию.
Источники и обсуждения
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


