Ленивый объект — объект, инициализация которого откладывается до тех пор, пока объект не начнёт отслеживаться или не изменится состояние объекта. Отдельные примеры работы с ленивыми объектами включают: а) компоненты внедрения зависимостей, которые предоставляют отложенные службы, которые инициализируются на 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 % прозрачными для потребителей, поэтому обычные операции, которые отслеживают или изменяют состояние объекта, автоматически инициализируют ленивый объект перед выполнением операции. Инициализацию запускает следующий неполный список операций:
Вызовы методов, которые не обращаются к состоянию объекта, не инициализируют ленивый объект. Аналогично, взаимодействия с объектом, которые вызывают магические методы или функции хука, не инициализируют ленивый объект, если для этих методов или функций не открыт доступ к состоянию объекта.
Следующие специальные методы или низкоуровневые операции получают доступ к ленивым объектам или изменяют ленивые объекты без запуска инициализации:
ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE
,
если только функция
__serialize()
или __sleep() не запустила инициализацию.
Раздел описывает характерные для стратегий последовательности операций, которые выполняются при запуске инициализации.
null
или не вернёт никакого значения.
На этом этапе объект перестаёт быть ленивым, поэтому функция получает прямой доступ
к свойствам реального объекта.
После инициализации объект неотличим от объекта, который никогда не был ленивым.
После инициализации доступ к свойствам объекта-прокси даст тот же результат, что и доступ к тому же свойству реального экземпляра; обращения к свойствам объекта-прокси перенаправляются на реальный экземпляр, включая объявленные, динамические, несуществующие свойства или свойства, которые пометили методом ReflectionProperty::skipLazyInitialization() или ReflectionProperty::setRawValueWithoutLazyInitialization().
Сам объект-прокси не заменяется и не подменяет собой реальный экземпляр.
Хотя фабрика получает прокси как первый аргумент, ожидается, что фабричная функция не станет изменять объект-прокси. Изменения разрешаются, но потеряются на заключительном этапе инициализации. Однако прокси иногда помогает принимать решения на основе значений инициализированных свойств, класса, самого объекта или его идентификатора. Например, инициализатор создаёт реальный экземпляр на основе значения инициализированного свойства.
Область действия и контекст переменной $this инициализатора или фабричной функции остаются неизменными, и применяются стандартные ограничения видимости.
После успешной инициализации объект больше не ссылается на инициализатор или фабричную функцию и доступен для освобождения, если на объект не осталось других ссылок.
Состояние объекта возвращается к состоянию до инициализации, а объект снова помечается как ленивый, если инициализатор генерирует исключение. Другими словами, каждое воздействие на сам объект отменяется. Другие побочные эффекты наподобие воздействия на другие объекты не отменяются. Это предотвращает предоставление частично инициализированного экземпляра при ошибке.
Клонирование ленивого объекта запускает его инициализацию раньше, чем создаётся клон, поэтому объект инициализируется.
При клонировании объектов-прокси клонируется как прокси, так и его реальный экземпляр,
и возвращается клон прокси.
Магический метод __clone
вызывается на реальном экземпляре, не на прокси.
Клонированный прокси и реальный экземпляр связываются так, как они связались
при инициализации, поэтому доступ к клону объекта-прокси перенаправляется
на клон реального экземпляра.
Такое поведение гарантирует, что клон и исходный объект сохраняют разные состояния. Изменения состояния исходного объекта или состояния его инициализатора после клонирования не влияют на клон. Клонирование и прокси, и его реального экземпляра вместо возврата только клона реального экземпляра гарантирует, что операция клонирования стабильно возвращает объект того же класса.
Для ленивых призраков деструктор вызывается, только если объект инициализировали. Для прокси деструктор вызывается только для реального экземпляра, если реальный экземпляр существует.
Методам ReflectionClass::resetAsLazyGhost() и ReflectionClass::resetAsLazyProxy() разрешается вызывать деструктор сбрасываемого объекта.