延迟对象是指其初始化被推迟到其状态被观察或修改之前的对象。一些用例示例包括仅在需要时才完全初始化延迟服务的依赖注入组件、ORM 提供仅在访问时才从数据库中获取数据的延迟实体,或在访问元素之前延迟解析的 JSON 解析器。
支持两种延迟对象策略:幽灵对象和虚拟代理,以下分别称为“延迟幽灵”和“延迟代理”。在这两种策略中,延迟对象都附加到一个初始化器或工厂,当第一次观察或修改其状态时,该初始化器或工厂会自动被调用。从抽象的角度来看,延迟幽灵对象与非延迟对象没有区别:它们可以在不知道它们是延迟的情况下使用,允许它们被传递给并用于不知道延迟的代码。延迟代理也同样透明,但在使用其身份时必须小心,因为代理及其真实实例具有不同的身份。
可以创建任何用户定义类的延迟实例或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));
// 触发初始化
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() 创建。在此之后,惰性对象可以通过以下操作之一进行初始化
由于当所有属性都标记为非惰性时,惰性对象会初始化,因此如果无法将任何属性标记为惰性,上述方法不会将对象标记为惰性。
惰性对象旨在对其使用者完全透明,因此观察或修改对象状态的正常操作将在执行操作之前自动触发初始化。这包括但不限于以下操作
不访问对象状态的方法调用不会触发初始化。类似地,如果这些方法或函数不访问对象的状态,则与对象交互以调用魔术方法或钩子函数将不会触发初始化。
以下特定方法或低级操作允许访问或修改惰性对象而无需触发初始化
ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE
时使用 serialize(),除非 __serialize() 或 __sleep() 触发初始化。本节概述了触发初始化时执行的操作序列,基于所使用的策略。
null
或无值。此时对象不再是惰性的,因此函数可以直接访问其属性。初始化后,该对象与从未处于惰性状态的对象没有区别。
初始化后,访问代理上的任何属性都将产生与访问真实实例上的相应属性相同的结果;代理上的所有属性访问都将转发到真实实例,包括声明的、动态的、不存在的或使用 ReflectionProperty::skipLazyInitialization() 或 ReflectionProperty::setRawValueWithoutLazyInitialization() 标记的属性。
代理对象本身不会被替换或替代真实实例。
虽然工厂将代理作为其第一个参数接收,但它不需要修改它(允许修改,但在最终初始化步骤期间会丢失)。但是,代理可用于基于已初始化属性的值、类、对象本身或其身份的决策。例如,初始化程序在创建真实实例时可能会使用已初始化属性的值。
初始化程序或工厂函数的作用域和 $this 上下文保持不变,并且适用通常的可见性约束。
成功初始化后,初始化程序或工厂函数不再被对象引用,如果它没有其他引用,则可能会被释放。
如果初始化程序抛出异常,则对象状态将恢复到其初始化前的状态,并且对象再次被标记为惰性。换句话说,对对象本身的所有影响都将恢复。其他副作用,例如对其他对象的影响,不会恢复。这可以防止在发生故障时公开部分初始化的实例。
克隆 惰性对象会在创建克隆之前触发其初始化,从而产生已初始化的对象。
对于代理对象,代理及其真实实例都将被克隆,并返回代理的克隆。在真实实例上调用 __clone
方法,而不是在代理上调用。克隆的代理和真实实例在初始化期间与它们链接在一起,因此对代理克隆的访问将转发到真实实例克隆。
此行为确保克隆和原始对象维护不同的状态。克隆后对原始对象或其初始化程序状态的更改不会影响克隆。克隆代理及其真实实例,而不是仅返回真实实例的克隆,可以确保克隆操作始终返回相同类的对象。
对于惰性幽灵,只有在对象已初始化时才会调用析构函数。对于代理,只有在存在真实实例时,才会在真实实例上调用析构函数。
ReflectionClass::resetAsLazyGhost() 和 ReflectionClass::resetAsLazyProxy() 方法可能会调用正在重置的对象的析构函数。