PHP Conference Nagoya 2025

Ленивые объекты

Ленивый объект — объект, инициализация которого откладывается до тех пор, пока объект не начнёт отслеживаться или не изменится состояние объекта. Отдельные примеры работы с ленивыми объектами включают: а) компоненты внедрения зависимостей, которые предоставляют отложенные службы, которые инициализируются на 100 % только когда требуются б) ORM-инструменты, которые предоставляют ленивые объекты, которые гидрируются значениями из базы данных только при обращении к ORM-объекту или в) JSON-парсер, который откладывает разбор до тех пор, пока к элементам не обратятся.

Поддерживаются две стратегии ленивых объектов: объекты-призраки (англ. Ghost Objects) и виртуальные прокси (англ. Virtual Proxies), которые здесь и дальше будем называть «ленивые призраки» и «ленивые прокси». В обеих стратегиях ленивый объект прикрепляется к инициализатору или фабрике, которая вызывается автоматически, когда состояние объекта начинают отслеживать или изменяют в первый раз. С точки зрения абстракции ленивые объекты-призраки неотличимы от неленивых: с такими объектами работают, не зная, что они ленивые, что разрешает коду передавать и обрабатывать такие объекты без знания о лени объектов. Ленивые прокси тоже прозрачны, но когда потребуется отличить ленивый объекты-прокси от реального экземпляра, соблюдают осторожность, поскольку у объекта-прокси и его реального экземпляра разные идентификаторы.

Создание ленивых объектов

Разрешается создавать ленивые экземпляры пользовательских классов или стандартного PHP-класса stdClass (другие внутренние классы не поддерживаются) или сбрасывать экземпляры этих классов, чтобы сделать объект ленивым. Точки входа, через которые создают ленивые объекты, — методы ReflectionClass::newLazyGhost() и ReflectionClass::newLazyProxy() methods.

Оба метода принимают callback-функцию, которая вызывается, когда требуется инициализация объекта. Поведение, которого ждут от функции обратного вызова, меняется, и зависит от стратегии. Стратегии описывает справочная документация к методам.

Пример #1 Пример создания ленивого призрака

<?php

class Example
{
public function
__construct(public int $prop)
{
echo
__METHOD__, "\n";
}
}

$reflector = new ReflectionClass(Example::class);
$lazyObject = $reflector->newLazyGhost(function (Example $object) {
// Инициализируем объект позже, на месте — по требованию
$object->__construct(1);
});

var_dump($lazyObject);
var_dump(get_class($lazyObject));

// Запускаем инициализацию
var_dump($lazyObject->prop);

?>

Результат выполнения приведённого примера:

lazy ghost object(Example)#3 (0) {
["prop"]=>
uninitialized(int)
}
string(7) "Example"
Example::__construct
int(1)

Пример #2 Пример создания ленивого прокси

<?php

class Example
{
public function
__construct(public int $prop)
{
echo
__METHOD__, "\n";
}
}

$reflector = new ReflectionClass(Example::class);
$lazyObject = $reflector->newLazyProxy(function (Example $object) {
// Создаём и возвращаем реальный экземпляр
return new Example(1);
});

var_dump($lazyObject);
var_dump(get_class($lazyObject));

// Запускаем инициализацию
var_dump($lazyObject->prop);

?>

Результат выполнения приведённого примера:

lazy proxy object(Example)#3 (0) {
  ["prop"]=>
  uninitialized(int)
}
string(7) "Example"
Example::__construct
int(1)

Доступ к свойствам ленивого объекта запускает инициализацию ленивого объекта, включая доступ через класс ReflectionProperty. Однако отдельные свойства иногда известны заранее и требуется сделать так, чтобы они не вызывали инициализацию при доступе:

Пример #3 Пример энергичной инициализации свойств

<?php

class BlogPost
{
public function
__construct(
private
int $id,
private
string $title,
private
string $content,
) {}
}

$reflector = new ReflectionClass(BlogPost::class);

$post = $reflector->newLazyGhost(function ($post) {
$data = fetch_from_store($post->id);
$post->__construct($data['id'], $data['title'], $data['content']);
});

// Без этой строки вызов метода ReflectionProperty::setValue(), который идёт следующим,
// запустит инициализацию
$reflector->getProperty('id')->skipLazyInitialization($post);

$reflector->getProperty('id')->setValue($post, 123);

// Альтернативный способ установки значения свойства без запуска ленивой инициализации
$reflector->getProperty('id')->setRawValueWithoutLazyInitialization($post, 123);

// Доступ к свойству id возможен без запуска инициализации.
var_dump($post->id);

?>

Методы ReflectionProperty::skipLazyInitialization() и ReflectionProperty::setRawValueWithoutLazyInitialization() предлагают способы обхода инициализации ленивых объектов при доступе к свойству.

О стратегиях ленивых объектов

Ленивые призраки — объекты, которые инициализируются на месте, и после инициализации неотличимы от объекта, который никогда не был ленивым. Стратегию применяют, когда контролируют как создание экземпляра, так и инициализацию объекта, что делает стратегию непригодной, если хотя бы один из этих процессов управляется другой стороной.

Ленивые прокси после инициализации действуют как прокси до реального экземпляра: операции на инициализированном ленивом прокси перенаправляются на реальный экземпляр. Создание реального экземпляра разрешается делегировать другой стороне, что делает эту стратегию полезной, когда ленивые призраки не подходят. Хотя ленивые прокси почти так же прозрачны, как ленивые призраки, потребуется осторожность, когда потребуется отличить ленивый прокси от реального объекта, поскольку у объекта-прокси и его реального экземпляра разные идентификаторы.

Жизненный цикл ленивых объектов

Объекты делают ленивыми либо сразу — путём вызова метода ReflectionClass::newLazyGhost() или ReflectionClass::newLazyProxy(), либо создают а затем передают реальный объект в метод ReflectionClass::resetAsLazyGhost() или ReflectionClass::resetAsLazyProxy(). Следующие операции инициализируют ленивый объект:

Поскольку ленивые объекты становятся инициализированными, когда каждое свойство ленивого объекта пометили неленивым, приведённые методы не пометят объект как ленивый, если ни одно свойство нельзя пометить ленивым.

Триггеры инициализации

Ленивые объекты спроектировали на 100 % прозрачными для потребителей, поэтому обычные операции, которые отслеживают или изменяют состояние объекта, автоматически инициализируют ленивый объект перед выполнением операции. Инициализацию запускает следующий неполный список операций:

  • Чтение или запись свойства.
  • Проверка установки свойства или удаление свойства.
  • Доступ к свойству или изменение свойства методами ReflectionProperty::getValue(), ReflectionProperty::getRawValue(), ReflectionProperty::setValue() или ReflectionProperty::setRawValue().
  • Получение списка свойств методами ReflectionObject::getProperties() или ReflectionObject::getProperty(), или функцией get_object_vars().
  • Итерация по свойствам объекта, который не реализует интерфейс Iterator или IteratorAggregate, конструкцией foreach.
  • Сериализация ленивого объекта функциями serialize(), json_encode() или другими.
  • Клонирование ленивого объекта.

Вызовы методов, которые не обращаются к состоянию объекта, не инициализируют ленивый объект. Аналогично, взаимодействия с объектом, которые вызывают магические методы или функции хука, не инициализируют ленивый объект, если для этих методов или функций не открыт доступ к состоянию объекта.

Нетриггерные операции

Следующие специальные методы или низкоуровневые операции получают доступ к ленивым объектам или изменяют ленивые объекты без запуска инициализации:

Последовательность инициализации

Раздел описывает характерные для стратегий последовательности операций, которые выполняются при запуске инициализации.

Объекты-призраки

  • Ленивый призрак помечается как неленивый.
  • Свойствам, которые не инициализировали методом ReflectionProperty::skipLazyInitialization() или ReflectionProperty::setRawValueWithoutLazyInitialization(), устанавливаются значения по умолчанию, если такие определили в классе. На этом этапе объект напоминает объект, который создали методом ReflectionClass::newInstanceWithoutConstructor(), за исключением свойств, которые уже инициализировались.
  • Затем вызывается callback-функция инициализатора с объектом в качестве первого аргумента. Ожидается, что функция обратного вызова — хотя это и не обязательно — инициализирует состояние объекта и вернёт null или не вернёт никакого значения. На этом этапе объект перестаёт быть ленивым, поэтому функция получает прямой доступ к свойствам реального объекта.

После инициализации объект неотличим от объекта, который никогда не был ленивым.

Объекты-прокси

  • Ленивый прокси помечается как неленивый.
  • В отличие от ленивых призраков, свойства ленивых прокси на этом этапе не изменяются.
  • Фабричная callback-функция вызывается с объектом в качестве первого аргумента и возвращает неленивый экземпляр совместимого класса. Смотрите описание метода ReflectionClass::newLazyProxy().
  • Экземпляр, который вернула функция обратного вызова, называется реальным экземпляром и прикрепляется к прокси.
  • Значения свойств ленивого прокси отбрасываются так, как если бы вызвали языковую конструкцию unset().

После инициализации доступ к свойствам объекта-прокси даст тот же результат, что и доступ к тому же свойству реального экземпляра; обращения к свойствам объекта-прокси перенаправляются на реальный экземпляр, включая объявленные, динамические, несуществующие свойства или свойства, которые пометили методом ReflectionProperty::skipLazyInitialization() или ReflectionProperty::setRawValueWithoutLazyInitialization().

Сам объект-прокси не заменяется и не подменяет собой реальный экземпляр.

Хотя фабрика получает прокси как первый аргумент, ожидается, что фабричная функция не станет изменять объект-прокси. Изменения разрешаются, но потеряются на заключительном этапе инициализации. Однако прокси иногда помогает принимать решения на основе значений инициализированных свойств, класса, самого объекта или его идентификатора. Например, инициализатор создаёт реальный экземпляр на основе значения инициализированного свойства.

Общее поведение

Область действия и контекст переменной $this инициализатора или фабричной функции остаются неизменными, и применяются стандартные ограничения видимости.

После успешной инициализации объект больше не ссылается на инициализатор или фабричную функцию и доступен для освобождения, если на объект не осталось других ссылок.

Состояние объекта возвращается к состоянию до инициализации, а объект снова помечается как ленивый, если инициализатор генерирует исключение. Другими словами, каждое воздействие на сам объект отменяется. Другие побочные эффекты наподобие воздействия на другие объекты не отменяются. Это предотвращает предоставление частично инициализированного экземпляра при ошибке.

Клонирование

Клонирование ленивого объекта запускает его инициализацию раньше, чем создаётся клон, поэтому объект инициализируется.

При клонировании объектов-прокси клонируется как прокси, так и его реальный экземпляр, и возвращается клон прокси. Магический метод __clone вызывается на реальном экземпляре, не на прокси. Клонированный прокси и реальный экземпляр связываются так, как они связались при инициализации, поэтому доступ к клону объекта-прокси перенаправляется на клон реального экземпляра.

Такое поведение гарантирует, что клон и исходный объект сохраняют разные состояния. Изменения состояния исходного объекта или состояния его инициализатора после клонирования не влияют на клон. Клонирование и прокси, и его реального экземпляра вместо возврата только клона реального экземпляра гарантирует, что операция клонирования стабильно возвращает объект того же класса.

Деструкторы

Для ленивых призраков деструктор вызывается, только если объект инициализировали. Для прокси деструктор вызывается только для реального экземпляра, если реальный экземпляр существует.

Методам ReflectionClass::resetAsLazyGhost() и ReflectionClass::resetAsLazyProxy() разрешается вызывать деструктор сбрасываемого объекта.

Добавить

Примечания пользователей

Пользователи ещё не добавляли примечания для страницы
To Top