Глава 41 · От основ к архитектуре
Конструкторы и жизненный цикл: объект сразу пригоден к работе
Проверяем инварианты при создании, выбираем именованные фабрики и разбираем вызов родительского конструктора, ошибки и освобождение ресурсов.
Создать карточку, у которой название уже заполнено
В каталоге забыли заполнить название одной карточки. Вместо пустого объекта с последующим набором присваиваний потребуем название прямо при создании. Конструктор — метод, который PHP вызывает во время создания объекта.
Что получится. new ProductCard("Чай") сразу даёт карточку, готовую вывести Чай.
- new ProductCard("Чай") начинает создание
- Параметр $title конструктора получает строку Чай
- $this->title сохраняет её в создаваемой карточке
- Завершившийся new возвращает готовый объект в $card
Сначала: свойство, метод и $this.
<?php
declare(strict_types=1);
class ProductCard
{
public string $title;
public function __construct(string $title)
{
$this->title = $title;
}
}
$card = new ProductCard('Чай');
echo $card->title, "\n";
Ожидаемый результат
Чай
Локальный $title существует в вызове конструктора; $this->title остаётся в объекте после его завершения. Они могут называться одинаково, но это разные места хранения. Имя __construct пишется с двумя подчёркиваниями.
| Запись в примере | Что делает каждый элемент |
|---|---|
__construct(string $title) | Специальное имя и обязательный строковый параметр. Вызов new передаёт сюда Чай. |
$this->title = $title; | Справа входной аргумент, слева свойство нового объекта. Присваивание сохраняет значение. |
new ProductCard('Чай') | Создаёт экземпляр и запускает доступный конструктор. Это не вызов отдельного HTTP-сервера. |
public function __construct(...) | У конструктора не указывают возвращаемый тип, даже void. Его return не подменяет объект, возвращаемый new. |
Непустое название — правило, которое должно выполняться всегда
Цена проверяется до получения готового товара
Чтобы товар нельзя было использовать без цены, передаём обязательные данные сразу при создании.
- Вход: название и цена
new Product('Чай', 15000) передаёт значения конструктору; один new не придумывает цену.
- __construct проверяет и записывает
Название после trim непустое; цена от 0 до 1000000. Только затем заполняются поля.
- Два возможных исхода
Результат зависит от проверенных данных, а не от последующей дисциплины вызывающего кода.
- 15000 → готовый объект
Присваивание завершено; priceMinor() возвращает 15000.
- −1 → исключение
Вызов создания не вернул готовый товар; вызывающий код обрабатывает ошибку.
Проверка полей не требует сетевого запроса. Деструктор не является продолжением этого сценария: оплату, сохранение заказа и другие обязательные действия в него не откладывают.
Пустая строка тоже имеет тип string. Нужна проверка смысла: после удаления крайних пробелов должен остаться текст. Такое правило допустимого состояния называют инвариантом. Проверяем до записи. Исключение прерывает создание; вызывающий код выбирает сообщение пользователю.
<?php
declare(strict_types=1);
class ProductCard
{
public string $title;
public function __construct(string $title)
{
$title = trim($title);
if ($title === '') {
throw new InvalidArgumentException('Укажите название товара');
}
$this->title = $title;
}
}
try {
$card = new ProductCard(' ');
} catch (InvalidArgumentException $error) {
echo $error->getMessage(), "\n";
}
$card = new ProductCard(' Чай ');
echo 'Готово: ', $card->title, "\n";
Ожидаемый результат
Укажите название товара
Готово: Чай
trim возвращает строку без крайних пробелов. === сравнивает с пустой строкой. throw создаёт исключение и прерывает обычный путь; catch получает его. После успешного new значение гарантированно заполнено. Пока свойство public, поздняя запись всё ещё способна нарушить правило — это исправляет следующая глава о видимости.
Цена: договориться о единицах до проверки диапазона
Каталог передаёт цену целым числом копеек: 25900 означает 259 рублей. Название priceKopecks делает единицы явными. Конструктор не угадывает, что означало число 259. Тип int исключает строку, но отрицательную цену приходится проверять отдельно.
<?php
declare(strict_types=1);
class Product
{
public function __construct(
public string $title,
public int $priceKopecks
) {
if ($priceKopecks < 0) {
throw new InvalidArgumentException('Цена не может быть отрицательной');
}
}
}
$tea = new Product('Чай', 25900);
echo $tea->title, ': ', $tea->priceKopecks, " коп.\n";
try {
new Product('Кофе', -100);
} catch (InvalidArgumentException $error) {
echo $error->getMessage(), "\n";
}
Ожидаемый результат
Чай: 25900 коп.
Цена не может быть отрицательной
Здесь используется сокращение объявления свойств, которое подробно разобрано следующим шагом. Нулевая цена разрешена выбранным правилом: например, бесплатный пробник. Для платного товара условие было бы <= 0. Проверку названия из предыдущего шага добавляют тем же способом.
Сократить повторяющиеся объявления: promotion — PHP 8.0+
В предыдущем примере public внутри параметров конструктора одновременно объявляет свойство и сохраняет переданный аргумент. Это называют продвижением параметров, или promotion. Без public/private/protected параметр остаётся обычным локальным входом. Присваивание promoted-свойств выполняется перед телом конструктора.
| Запись в примере | Что делает каждый элемент |
|---|---|
public string $title | Параметр title и свойство title создаются одной записью. |
public int $priceKopecks | То же сокращение для цены; затем тело проверяет её значение. |
string $title без видимости | Только параметр. Свойство следует объявить и присвоить отдельно, как в первом примере. |
Если нужно сначала нормализовать значение, отдельное свойство часто яснее. Это особенно полезно с readonly: сначала подготовили строку, затем записали итог один раз. readonly изучается в главе о неизменяемых свойствах. Promotion не разрешён у abstract-конструктора; тип callable не допускается для свойства. Один и тот же член нельзя объявить повторно и вручную, и через promotion.
Фабрика даёт понятное имя способу создания
Внешняя запись каталога содержит price_kopecks. Не смешиваем распаковку массива с любым созданием объекта: отдельный статический метод fromRow проверит вход и создаст карточку. Статический означает вызов у класса через ::, без уже готового объекта; здесь он нужен как именованная входная дверь.
<?php
declare(strict_types=1);
class Product
{
public function __construct(public string $title, public int $priceKopecks)
{
if ($title === '' || $priceKopecks < 0) {
throw new InvalidArgumentException('Некорректный товар');
}
}
public static function fromRow(array $row): self
{
if (!isset($row['title'], $row['price_kopecks'])
|| !is_string($row['title']) || !is_int($row['price_kopecks'])) {
throw new InvalidArgumentException('Нужны title:string и price_kopecks:int');
}
return new self(trim($row['title']), $row['price_kopecks']);
}
}
$tea = Product::fromRow(['title' => ' Чай ', 'price_kopecks' => 25900]);
echo $tea->title, ': ', $tea->priceKopecks, " коп.\n";
Ожидаемый результат
Чай: 25900 коп.
array — массив входных полей; isset проверяет наличие непустых относительно null значений, is_string/is_int — реальные типы. self внутри Product означает этот класс. Фабрика возвращает объект, поэтому у неё есть : self; конструктор возвращаемого типа не имеет. Строка "25900" намеренно отклоняется: политику преобразования внешних чисел нужно задать явно.
Собственный конструктор потомка не вызывает родительский автоматически
Наследование изучается отдельно: потомок расширяет договор родителя. Здесь достаточно понять правило инициализации. Если потомок не объявил собственный конструктор, он может унаследовать доступный родительский. Если объявил, нужный parent::__construct вызывают явно.
<?php
declare(strict_types=1);
class ExportOptions
{
public function __construct(public string $title) {}
}
class CsvOptions extends ExportOptions
{
public function __construct(string $title, public string $separator)
{
parent::__construct($title);
}
}
$options = new CsvOptions('Каталог', ';');
echo $options->title, ' — разделитель ', $options->separator, "\n";
Ожидаемый результат
Каталог — разделитель ;
parent выбирает родителя текущего класса; :: обращается к его конструктору. Сначала аргументы потомка приняты, затем явный вызов заполняет title по правилу родителя. Без вызова title останется неинициализированным.
Конструкторы в общем случае исключены из обычной проверки совместимости сигнатур переопределённых методов. Поэтому new static(...) в родителе опасен, если потомки требуют другие аргументы. Не вызывайте parent::__construct, если у родителя его нет. Конструктор не должен обращаться к переопределяемому методу, который требует ещё не заполненное состояние потомка.
Создание клиента не равно отправке запроса
Клиент Bitrix24 получает адрес портала, транспорт и настройки в конструкторе; listMethod выполняет конкретную операцию. Так создание можно проверить без сети. Авторизация может быть отдельной процедурой получения токена, а запрос использует уже полученный токен. new само по себе не требует ни HTTP, ни входа в систему.
Для заказа также различайте создать новый и восстановить старый: восстановление из БД не должно сбрасывать оплаченный статус или пересчитывать историческую цену. Именованные фабрики задают эти два договора; внутренние проверки непротиворечивости остаются в обоих.
Попробуйте. Добавьте к первой карточке обязательный sku — код товара — без promotion.
Разобрать решение
Объявите public string $sku; добавьте string $sku в параметры __construct; сохраните $this->sku = $sku. В new передайте второй аргумент. Теперь пропущенный код станет ошибкой вызова при создании.
Закрытие ресурса и окончание жизни объекта
__destruct вызывается при уничтожении объекта или во время завершения скрипта. Не поручайте ему оплату, отправку заказа и обязательное сохранение: время уничтожения зависит от ссылок, циклов и завершения процесса. Для ресурса задавайте явную границу try/finally; finally выполняет очистку при обычном выходе и исключении.
<?php
declare(strict_types=1);
$stream = fopen('php://temp', 'w+');
if ($stream === false) {
throw new RuntimeException('Не удалось открыть отчёт');
}
try {
fwrite($stream, "Товар;Цена\nЧай;25900\n");
rewind($stream);
echo stream_get_contents($stream);
} finally {
fclose($stream);
echo "Отчёт закрыт\n";
}
Ожидаемый результат
Товар;Цена
Чай;25900
Отчёт закрыт
php://temp хранит временный поток; w+ разрешает запись и чтение. rewind возвращает позицию к началу, поэтому читается весь отчёт. finally закрывает поток. Даже finally не является гарантией при аварийном завершении процесса: важные бизнес-операции требуют отдельного надёжного протокола.
Дальше: полный договор языка и специальные случаи
После обычного создания можно уточнить значения new по умолчанию, promotion вместе с hooks, жизненный цикл и уничтожение объектов. Эти правила расширяют договор, но не меняют назначение конструктора: подготовить допустимое состояние.
Разделы ниже сохраняют специальные правила официального PHP Manual: New в значении по умолчанию: чей это объект и когда он создаётся, Promotion задаёт тип параметра, даже если set принимает больше — PHP 8.4+, Объект в default, static и const: место записи определяет время жизни, Последняя переменная и последний доступ к объекту — не одно и то же, Сборщик циклов внутри Fiber: деструктор может выполняться в отдельном Fiber — PHP 8.4+, Деструктор в конце скрипта: последняя ссылка ещё могла существовать.
New в значении по умолчанию: чей это объект и когда он создаётся
Большинство отчётов использует стандартные настройки csv. Не хочется писать new ExportOptions при каждом простом вызове. В параметре функции можно указать new как значение по умолчанию, но сначала важно понять: новый объект появляется для конкретного вызова, в котором аргумент пропущен.
- Первый draft() создаёт собственные Options
- Второй draft() создаёт другие Options
- Изменение первых настроек не затрагивает вторые
- Явно переданный объект используется тем же экземпляром
<?php
declare(strict_types=1);
class DraftOptions
{
public string $format = 'csv';
}
function draft(DraftOptions $options = new DraftOptions()): DraftOptions
{
return $options;
}
$first = draft();
$second = draft();
$first->format = 'json';
echo $first->format, ', ', $second->format, "\n";
echo $first === $second ? "Общие настройки\n" : "Разные объекты\n";
$provided = new DraftOptions();
echo draft($provided) === $provided ? "Переданный объект сохранён\n" : "Создана копия\n";
Ожидаемый результат
json, csv
Разные объекты
Переданный объект сохранён
Функция специально возвращает настройки без другой работы, чтобы было видно владение. При пропущенном аргументе каждый вызов вычисляет new заново. Явно переданный объект не клонируется: функция получает тот же экземпляр, и его изменение внутри было бы видно вызывающему коду.
New в допустимых инициализаторах поддерживается с PHP 8.1: это также определённые static-переменные, глобальные константы и аргументы атрибутов. Момент и число вычислений у этих мест отличаются; пример выше говорит только о пропущенном параметре. Не объявляйте по аналогии любое обычное свойство как public $options = new Options: для него такая запись не разрешена.
В этом контексте нельзя использовать анонимный или динамически выбранный класс и распаковку аргументов. Для сложного выбора зависимости оставьте явную фабрику или сборку объекта снаружи. Конструктор значения по умолчанию не должен неожиданно отправлять HTTP-запрос.
При constructor promotion значение по умолчанию относится к параметру конструктора, а не к отдельному начальному значению свойства. Само присваивание promoted-поля происходит при создании объекта. Старый метод с именем класса больше не считается конструктором в PHP 8; используйте __construct. Конструктор и деструктор не объявляют возвращаемый тип.
Promotion задаёт тип параметра, даже если set принимает больше — PHP 8.4+
В импорте строка yes превращается в bool через set. Возникает ожидание, что конструктор с promoted-полем также примет yes. Но promotion одновременно объявляет параметр и свойство: тип параметра остаётся типом свойства. Более широкий вход хука не переписывает сигнатуру конструктора.
- new Flag(false) проходит bool-параметр конструктора
- Внутреннее присваивание попадает в set
- Поздняя запись flag->enabled = yes сразу попадает в расширенный set
- new Flag(yes) останавливается на параметре до set
<?php
declare(strict_types=1);
class PromotedFlag
{
public function __construct(
public bool $enabled {
set(bool|string $value) {
$this->enabled = is_bool($value) ? $value : match ($value) {
'yes' => true,
'no' => false,
default => throw new InvalidArgumentException('Неизвестный флаг'),
};
}
}
) {}
}
$flag = new PromotedFlag(false);
$flag->enabled = 'yes';
echo $flag->enabled ? "Запись через set включила флаг\n" : "Флаг выключен\n";
try {
new PromotedFlag('yes');
} catch (TypeError $error) {
echo "Конструктор ожидает bool\n";
}
Ожидаемый результат
Запись через set включила флаг
Конструктор ожидает bool
Пример использует strict_types=1. Set имеет bool|string, но параметр конструктора объявлен bool. Внутренняя декомпозиция promotion напоминает отдельное свойство с хуком плюс __construct(bool $enabled), в котором выполняется присваивание.
Если конструктор тоже должен принимать два типа, откажитесь от promotion для этого параметра: отдельно объявите свойство bool с set и обычный параметр bool|string. Присваивание $this->enabled = $enabled тогда проходит вашу нормализацию. Это яснее, чем надеяться на неявное приведение строки к bool.
Проверьте, нужен ли широкий вход самой модели. Часто удобнее разобрать yes/no в импортере и передать в конструктор уже bool: модель остаётся независимой от формата конкретного файла. Расширенный set уместен, когда обе входные формы действительно часть устойчивого API класса.
Объект в default, static и const: место записи определяет время жизни
Мы уже видели отдельный объект default-параметра на вызов. Похожее new разрешено и в static-переменной функции, и в глобальной константе. Однако эти места не обещают новый экземпляр при каждом обращении. Для настроек повторное использование может быть полезно, а незамеченная общая изменяемость — причиной ошибки.
- Глобальная константа получает объект при выполнении объявления
- Static внутри функции инициализируется при первом достижении
- Повторный вызов возвращает сохранённый static-объект
- Const защищает имя привязки, но не делает внутренние поля readonly
<?php
declare(strict_types=1);
class ExportDefaults
{
public static int $created = 0;
public function __construct(public string $format)
{
self::$created++;
}
}
const DEFAULT_EXPORT = new ExportDefaults('csv');
function cachedDefaults(): ExportDefaults
{
static $options = new ExportDefaults('csv');
return $options;
}
$first = cachedDefaults();
$second = cachedDefaults();
$first->format = 'json';
echo $second->format, "\n";
echo DEFAULT_EXPORT->format, "\n";
$shared = DEFAULT_EXPORT;
$shared->format = 'xml';
echo DEFAULT_EXPORT->format, "\n";
echo 'Создано объектов: ', ExportDefaults::$created, "\n";
Ожидаемый результат
json
csv
xml
Создано объектов: 2
New в этих инициализаторах доступен с PHP 8.1. CachedDefaults действительно кэширует один объект в текущем выполнении; first и second — не копии. В глобальной константе хранится другой объект. Для записи мы сначала получили его в переменную shared: прямая запись через выражение константы не является разрешённым контекстом записи. Изменилось публичное поле того же объекта, сама константа не переобъявлялась.
Если defaults должны быть неизменяемыми, используйте неизменяемый объект и контролируйте вложенные значения. В тестах static-настройки могут пережить несколько вызовов одного процесса и связать независимые сценарии. В обычном PHP-FPM это не общий кэш всех запросов сайта.
New также допускается в аргументах атрибутов: там нужно отдельно учитывать момент вычисления аргумента Reflection. Подробнее об аргументах атрибутов. Одинаковый синтаксис new не обещает одинаковый момент создания во всех контекстах.
Последняя переменная и последний доступ к объекту — не одно и то же
Обработчик содержит ссылку на самого себя или пару связанных объектов. Unset удаляет переменную, но внутренний цикл всё ещё существует. Для такого мусора PHP использует сборщик циклов. Это одна из причин, почему обязательное сохранение результата нельзя откладывать до деструктора.
- node указывает на объект
- Поле link указывает на этот же объект
- unset(node) убирает внешнюю переменную
- Сборщик распознаёт недостижимый цикл и вызывает очистку
<?php
declare(strict_types=1);
class CyclicImportNote
{
public static int $released = 0;
public ?self $link = null;
public function __destruct()
{
self::$released++;
}
}
$node = new CyclicImportNote();
$node->link = $node;
unset($node);
echo 'До сборки: ', CyclicImportNote::$released, "\n";
gc_collect_cycles();
echo 'После сборки: ', CyclicImportNote::$released, "\n";
Ожидаемый результат
До сборки: 0
После сборки: 1
В изолированном опыте создан один маленький цикл; явный gc_collect_cycles делает момент наблюдения определённым. Не вызывайте его после каждого заказа только ради такого вывода: в приложении сборщик запускается по своим условиям, и цену принудительных обходов следует измерять.
<?php
declare(strict_types=1);
class BasePreview
{
public function __destruct()
{
echo "Очистка базового буфера\n";
}
}
class DetailedPreview extends BasePreview
{
public function __destruct()
{
echo "Очистка деталей\n";
parent::__destruct();
}
}
$preview = new DetailedPreview();
unset($preview);
echo "Работа продолжается\n";
Ожидаемый результат
Очистка деталей
Очистка базового буфера
Работа продолжается
Если потомок вообще не объявляет __destruct, он может унаследовать родительский. Если объявляет свой, автоматического второго вызова родителя нет. Порядок очистки в опыте выбран автором явно. Для ресурса с обязательным закрытием надёжнее область try/finally, уже разобранная в этой главе.
При завершении скрипта деструкторы тоже вызываются, но контекст отличается: HTTP-заголовки уже могут быть отправлены, рабочий каталог некоторых SAPI может измениться, а исключение в shutdown-деструкторе заканчивается фатальной ошибкой. Не начинайте там новую оплату, редирект или бизнес-транзакцию.
Если деструктор сохраняет новую ссылку на свой объект, объект может снова стать достижимым, однако повторного вызова деструктора при следующем исчезновении ссылок не будет. Exit внутри деструктора способен оборвать дальнейшую очистку. Это причины держать деструктор коротким и предсказуемым, а обязательную операцию выполнять явно до него.
Сборщик циклов внутри Fiber: деструктор может выполняться в отдельном Fiber — PHP 8.4+
Этот раздел нужен автору инфраструктуры с Fiber, а не для первой формы сайта. Если сборщик находит циклический объект во время Fiber, в PHP 8.4 его деструктор выполняется в отдельном gc_destructor_fiber. Нельзя уверенно считать текущий Fiber в деструкторе тем же рабочим контекстом, который создал объект.
- Рабочий Fiber создаёт цикл
- Внутри него вызван сборщик
- Сборщик выполняет деструктор в другом Fiber
- После очистки рабочий Fiber заканчивается
<?php
declare(strict_types=1);
class FiberCycleNote
{
public static ?Fiber $worker = null;
public static bool $separate = false;
public ?self $link = null;
public function __destruct()
{
self::$separate = Fiber::getCurrent() !== null
&& Fiber::getCurrent() !== self::$worker;
}
}
$worker = new Fiber(function (): void {
FiberCycleNote::$worker = Fiber::getCurrent();
$note = new FiberCycleNote();
$note->link = $note;
unset($note);
gc_collect_cycles();
});
$worker->start();
echo FiberCycleNote::$separate ? "Деструктор исполнялся в отдельном Fiber\n" : "Тот же контекст\n";
echo $worker->isTerminated() ? "Рабочий Fiber завершён\n" : "Работа продолжается\n";
FiberCycleNote::$worker = null;
Ожидаемый результат
Деструктор исполнялся в отдельном Fiber
Рабочий Fiber завершён
Никаких потоков или сети здесь нет: сравнивается идентичность объектов Fiber. Ссылка worker нужна только для наблюдения и затем очищена. Пример целенаправленно вызывает сборщик после удаления внешней ссылки.
Если gc_destructor_fiber приостановится, сборщик может создать другой для оставшихся деструкторов. Приостановленный объект не собирается, пока деструктор не вернётся либо сам Fiber не будет собран. Поэтому не привязывайте обязательное освобождение ресурса к надежде, что любой деструктор завершится немедленно.
В обычном коде решения прежние: явное close/dispose в определённой области, try/finally и ясное владение ресурсами. Этот механизм объясняет редкие различия после обновления PHP, а не предлагает организовывать прикладной процесс через деструкторы.
Деструктор в конце скрипта: последняя ссылка ещё могла существовать
Если объект всё ещё доступен к концу скрипта, его деструктор не обязан был сработать раньше. PHP выполняет завершающую обработку процесса и освобождает оставшиеся объекты. Посмотрим на один объект, чтобы не выдавать порядок нескольких деструкторов за надёжный договор.
Register_shutdown_function регистрирует отдельный завершающий callback. В этом опыте обычный код доходит до конца, затем исполняется callback, затем деструктор оставшегося объекта. Это наблюдение жизненного цикла; вызывать оплату или обязательное сохранение заказа из этих стадий не следует.
- Объект остаётся в переменной note
- Основная часть печатает свою строку
- PHP выполняет зарегистрированный shutdown callback
- Освобождение note вызывает __destruct
<?php
declare(strict_types=1);
class FinishNote
{
public function __destruct() { echo "Деструктор оставшегося объекта\n"; }
}
$note = new FinishNote();
register_shutdown_function(static function (): void {
echo "Зарегистрированный shutdown callback\n";
});
echo "Конец основного кода\n";
Ожидаемый результат
Конец основного кода
Зарегистрированный shutdown callback
Деструктор оставшегося объекта
Здесь объект один и цикл отсутствует; в произвольном графе порядок деструкторов не используйте для бизнес-зависимостей. Если добавить unset($note) перед echo, деструктор выполнится раньше, когда исчезнет последняя ссылка, а shutdown callback всё равно дождётся завершения.
Exit также запускает обычную завершающую обработку PHP, но аварийное завершение процесса ОС не даёт такой гарантии. Исключение, вышедшее из деструктора при shutdown, приводит к фатальной ошибке; вызов exit внутри деструктора мешает оставшейся завершающей работе. В веб-запросе заголовки к этому моменту уже могут быть отправлены.
Для открытия/закрытия локального ресурса используйте явный try/finally в месте владения. Для очереди и платёжного исхода — явное сохранение до успешного ответа. Упражнение: перенесите unset перед последней строкой. Решение: поменяется момент деструктора, но его код останется тем же; это свойство количества ссылок, а не название переменной.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
__constructУстановить обязательные данные и зависимости при new.
public function __construct(...$arguments) /* без return type */PHP вызывает конструктор при обычном создании экземпляра через new. Сигнатуру аргументов определяет класс. Успех означает пригодный объект; невалидные данные лучше отклонить исключением.
<?php
declare(strict_types=1);
final readonly class Quantity {
public function __construct(public int $value) {
if ($value < 1) { throw new InvalidArgumentException('Количество от1'); }
}
}
echo (new Quantity(3))->value, "\n";
try { new Quantity(0); } catch (InvalidArgumentException $error) { echo "Отклонено\n"; }
Ожидаемый результат
3
Отклонено
Объект с нулевым количеством не попадает дальше в модель.
<?php
declare(strict_types=1);
final readonly class VatConfig { public function __construct(public int $percent) {} }
final class InvoiceText {
public function __construct(private VatConfig $config) {}
public function taxLabel(): string { return 'Ставка:'.$this->config->percent; }
}
echo (new InvoiceText(new VatConfig(20)))->taxLabel(), "\n";
Ожидаемый результат
Ставка:20
Число 20 — условная учебная конфигурация, не утверждение об актуальной налоговой ставке. Внешняя конфигурация видна в сборке.
__destructВыполнить финальную очистку объекта, но не критический сценарий.
public function __destruct() // без аргументов и return typeДеструктор вызывается при освобождении объекта или в процессе завершения в предусмотренных PHP условиях. Для ресурсов это может быть страховкой; критические изменения делайте явно и проверяйте результат.
<?php
declare(strict_types=1);
final class ReportBuffer
{
private $stream;
public function __construct()
{
$this->stream = fopen('php://temp', 'w+');
if ($this->stream === false) { throw new RuntimeException('Буфер недоступен'); }
fwrite($this->stream, 'Отчёт по заказу 42');
}
public function text(): string
{
rewind($this->stream);
return stream_get_contents($this->stream);
}
public function __destruct()
{
if (is_resource($this->stream)) { fclose($this->stream); }
echo "Локальный буфер освобождён\n";
}
}
$buffer = new ReportBuffer();
$reader = $buffer;
unset($buffer);
echo $reader->text(), "\n";
unset($reader);
Ожидаемый результат
Отчёт по заказу 42
Локальный буфер освобождён
Обе переменные держали один объект. После первого unset читатель ещё пользуется буфером; после второго исполняется __destruct. Вывод из деструктора служит только наблюдению в CLI. Для обязательного закрытия в конкретной точке следующий пример использует finally; оплату или отправку письма сюда не помещают.
<?php
declare(strict_types=1);
$stream=fopen('php://temp','w+');
if ($stream === false) { throw new RuntimeException('Не удалось открыть поток'); }
try { fwrite($stream,'local'); rewind($stream); echo stream_get_contents($stream), "\n"; }
finally { fclose($stream); echo "Поток закрыт\n"; }
Ожидаемый результат
local
Поток закрыт
Практическая альтернатива деструктору: момент закрытия виден даже при исключении в try. Работа только с локальным временным потоком.
Рядом: __construct, WeakMap.