ConFoo 2025

延迟对象

延迟对象是指其初始化推迟到状态被观察或修改时才进行的对象。一些用例示例包括依赖项注入组件(仅在需要时提供完全初始化的延迟服务)、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:

初始化序列

This section outlines the sequence of operations performed when initialization is triggered, based on the strategy in use.

幽灵对象

After initialization, the object is indistinguishable from an object that was never lazy.

代理对象

  • The object is marked as non-lazy.
  • Unlike ghost objects, the properties of the object are not modified at this stage.
  • The factory function is called with the object as its first parameter and must return a non-lazy instance of a compatible class (see ReflectionClass::newLazyProxy()).
  • The returned instance is referred to as the real instance and is attached to the proxy.
  • The proxy's property values are discarded as though unset() was called.

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.

添加备注

用户贡献的备注

此页面尚无用户贡献的备注。
To Top