延迟对象是指其初始化推迟到状态被观察或修改时才进行的对象。一些用例示例包括依赖项注入组件(仅在需要时提供完全初始化的延迟服务)、ORM(提供仅在访问时才从数据库获取数据的延迟实体)或 JSON 解析器(延迟解析直到访问元素)。
支持两种延迟对象策略:幽灵对象(Ghost Object)和虚拟代理(Virtual Proxies),以下称为"延迟幽灵"和"延迟代理"。 在这两种策略中,延迟对象都附加到初始化程序或工厂,当第一次观察或修改其状态时会自动调用。从抽象的角度来看,延迟幽灵对象与非延迟幽灵对象没有区别:都可以在不知道自己是延迟的情况下使用,从而允许将其传递给不知道延迟的代码并由其使用。延迟代理同样是透明的,但在使用它们的标识(identity)时必须小心,因为代理和其真实实例具有不同的标识。
可以创建任何用户定义类或 stdClass 类的延迟实例(不支持其他内部类),或重置这些类的实例以使其成为延迟实例。创建延迟对象的入口点是 ReflectionClass::newLazyGhost() 和 ReflectionClass::newLazyProxy() 方法。
这两种方法都接受函数,在对象需要初始化时调用该函数。该函数的预期行为因所使用的策略而异,如每种方法的参考文档中所述。
示例 #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));
// Triggers initialization
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']);
});
// Without this line, the following call to ReflectionProperty::setValue() would
// trigger initialization.
$reflector->getProperty('id')->skipLazyInitialization($post);
$reflector->getProperty('id')->setValue($post, 123);
// Alternatively, one can use this directly:
$reflector->getProperty('id')->setRawValueWithoutLazyInitialization($post, 123);
// The id property can be accessed without triggering initialization
var_dump($post->id);
?>
ReflectionProperty::skipLazyInitialization() 和 ReflectionProperty::setRawValueWithoutLazyInitialization() 方法提供了在访问属性时绕过延迟初始化的方法。
Lazy ghosts are objects that initialize in-place and, once initialized, are indistinguishable from an object that was never lazy. This strategy is suitable when we control both the instantiation and initialization of the object, making it unsuitable if either of these is managed by another party.
Lazy proxies, once initialized, act as proxies to a real instance: any operation on an initialized lazy proxy is forwarded to the real instance. The creation of the real instance can be delegated to another party, making this strategy useful in cases where lazy ghosts are unsuitable. Although lazy proxies are nearly as transparent as lazy ghosts, caution is needed when their identity is used, as the proxy and its real instance have distinct identities.
Objects can be made lazy at instantiation time using ReflectionClass::newLazyGhost() or ReflectionClass::newLazyProxy(), or after instantiation by using ReflectionClass::resetAsLazyGhost() or ReflectionClass::resetAsLazyProxy(). Following this, a lazy object can become initialized through one of the following operations:
As lazy objects become initialized when all their properties are marked non-lazy, the above methods will not mark an object as lazy if no properties could be marked as lazy.
Lazy objects are designed to be fully transparent to their consumers, so normal operations that observe or modify the object's state will automatically trigger initialization before the operation is performed. This includes, but is not limited to, the following operations:
Method calls that do not access the object state will not trigger initialization. Similarly, interactions with the object that invoke magic methods or hook functions will not trigger initialization if these methods or functions do not access the object's state.
The following specific methods or low-level operations allow access or modification of lazy objects without triggering initialization:
ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE
is set, unless
__serialize() or
__sleep() trigger initialization.
This section outlines the sequence of operations performed when initialization is triggered, based on the strategy in use.
null
or no value. The object is no
longer lazy at this point, so the function can access its properties
directly.
After initialization, the object is indistinguishable from an object that was never lazy.
After initialization, accessing any property on the proxy will yield the same result as accessing the corresponding property on the real instance; all property accesses on the proxy are forwarded to the real instance, including declared, dynamic, non-existing, or properties marked with ReflectionProperty::skipLazyInitialization() or ReflectionProperty::setRawValueWithoutLazyInitialization().
The proxy object itself is not replaced or substituted for the real instance.
While the factory receives the proxy as its first parameter, it is not expected to modify it (modifications are allowed but will be lost during the final initialization step). However, the proxy can be used for decisions based on the values of initialized properties, the class, the object itself, or its identity. For instance, the initializer might use an initialized property's value when creating the real instance.
The scope and $this context of the initializer or factory function remains unchanged, and usual visibility constraints apply.
After successful initialization, the initializer or factory function is no longer referenced by the object and may be released if it has no other references.
If the initializer throws an exception, the object state is reverted to its pre-initialization state and the object is marked as lazy again. In other words, all effects on the object itself are reverted. Other side effects, such as effects on other objects, are not reverted. This prevents exposing a partially initialized instance in case of failure.
Cloning a lazy object triggers its initialization before the clone is created, resulting in an initialized object.
For proxy objects, both the proxy and its real instance are cloned, and
the clone of the proxy is returned.
The __clone
method
is called on the real instance, not on the proxy.
The cloned proxy and real instance are linked as they are during
initialization, so accesses to the proxy clone are forwarded to the real
instance clone.
This behavior ensures that the clone and the original object maintain separate states. Changes to the original object or its initializer's state after cloning do not affect the clone. Cloning both the proxy and its real instance, rather than returning a clone of the real instance alone, ensures that the clone operation consistently returns an object of the same class.
For lazy ghosts, the destructor is only called if the object has been initialized. For proxies, the destructor is only called on the real instance, if one exists.
The ReflectionClass::resetAsLazyGhost() and ReflectionClass::resetAsLazyProxy() methods may invoke the destructor of the object being reset.