延遲物件是指其初始化被延遲到其狀態被觀察或修改時才進行的物件。一些用例範例包括:僅在需要時才完全初始化的延遲服務的依賴注入元件、僅在存取時才從資料庫中進行水合的延遲實體的 ORM,或延遲解析直到元素被存取的 JSON 解析器。
PHP 支援兩種延遲物件策略:Ghost 物件和虛擬代理,以下分別簡稱為「延遲 Ghost」和「延遲代理」。在這兩種策略中,延遲物件都附加到一個初始化器或工廠函式,當物件的狀態第一次被觀察或修改時,該函式會自動被呼叫。從抽象的角度來看,延遲 Ghost 物件與非延遲物件沒有區別:它們可以在不知道其延遲性的情況下使用,允許它們被傳遞給並被不了解延遲性的程式碼使用。延遲代理同樣是透明的,但在使用其識別性時必須小心,因為代理及其真實實例具有不同的識別性。
可以建立任何使用者定義類別或 stdClass 類別的延遲實例(不支援其他內建類別),或者重設這些類別的實例以使其延遲載入。建立延遲物件的入口點是 ReflectionClass::newLazyGhost() 和 ReflectionClass::newLazyProxy() 方法。
這兩個方法都接受一個在物件需要初始化時呼叫的函式。該函式的預期行為會根據所使用的策略而有所不同,如每個方法的參考文件中所述。
範例 #1 建立延遲 Ghost
<?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() 方法提供了在存取屬性時繞過延遲初始化的方法。
「*延遲虛擬物件 (Lazy ghosts)*」是指在原地初始化的物件,一旦初始化完成,便與從未延遲初始化的物件沒有區別。這種策略適用於我們同時控制物件的實例化和初始化的情況,如果其中任何一個是由另一方管理,則不適用。
延遲代理 一旦初始化後,就會作為真實實例的代理:任何在已初始化的延遲代理上的操作都會轉發到真實實例。真實實例的創建可以委託給另一方,這使得此策略在延遲幽靈不適用時非常有用。雖然延遲代理幾乎與延遲幽靈一樣透明,但在使用它們的識別碼時仍需謹慎,因為代理及其真實實例具有不同的識別碼。
物件可以在實例化時使用 ReflectionClass::newLazyGhost() 或 ReflectionClass::newLazyProxy() 設為延遲物件,或者在實例化後使用 ReflectionClass::resetAsLazyGhost() 或 ReflectionClass::resetAsLazyProxy() 設為延遲物件。之後,延遲物件可以透過以下操作之一進行初始化:
由於當所有屬性都標記為非延遲時,延遲物件會被初始化,因此如果沒有屬性可以標記為延遲,上述方法將不會將物件標記為延遲。
延遲物件的設計旨在對其使用者完全透明,因此觀察或修改物件狀態的正常操作將在操作執行之前自動觸發初始化。這包括但不限於以下操作:
不讀取物件狀態的方法呼叫不會觸發初始化。同樣地,與物件的互動(呼叫魔術方法或鉤子函式)如果這些方法或函式不讀取物件的狀態,也不會觸發初始化。
以下特定方法或低階操作允許在不觸發初始化的情況下訪問或修改惰性物件 (Lazy Object)。
ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE
時,使用 serialize(),除非 __serialize() 或 __sleep() 觸發初始化。本節概述根據所使用的策略,觸發初始化時執行的操作順序。
null
或不回傳任何值。此時物件不再是惰性的,因此該函式可以直接存取其屬性。初始化之後,該物件與從未惰性的物件沒有區別。
初始化之後,存取代理物件上的任何屬性將產生與存取真實實例上對應屬性相同的結果;代理物件上的所有屬性存取都會轉發到真實實例,包括已宣告、動態、不存在或標記有 ReflectionProperty::skipLazyInitialization() 或 ReflectionProperty::setRawValueWithoutLazyInitialization() 的屬性。
代理物件本身並*不會*被真實實例取代或替換。
雖然工廠方法會接收代理物件作為其第一個參數,但不應修改它(允許修改,但會在最終初始化步驟中遺失)。然而,代理物件可以用於根據已初始化屬性的值、類別、物件本身或其識別碼來做出決策。例如,初始化器可以在創建實際實例時使用已初始化屬性的值。
初始化器或工廠函數的作用域和 $this 上下文保持不變,並且適用通常的 visibilité 限制。
初始化成功後,物件將不再引用初始化器或工廠函數,如果它沒有其他引用,則可能會被釋放。
如果初始化器拋出異常,物件狀態將恢復到初始化前的狀態,並且物件將再次標記為延遲初始化。換句話說,所有對物件本身的影響都將被還原。其他副作用,例如對其他物件的影響,則不會被還原。這可以防止在初始化失敗時暴露部分初始化的實例。
複製(Cloning)延遲物件會在創建副本之前觸發其初始化,從而產生一個已初始化的物件。
對於代理物件,代理物件及其真實實例都會被複製,並返回代理物件的副本。__clone
方法會在真實實例上調用,而不是在代理物件上調用。複製的代理物件和真實實例之間的連結方式與初始化期間相同,因此對代理物件副本的訪問會轉發到真實實例副本。
此行為確保副本和原始物件保持獨立的狀態。複製後對原始物件或其初始化器狀態的更改不會影響副本。複製代理物件及其真實實例,而不是僅返回真實實例的副本,可確保複製操作始終返回相同類別的物件。
對於延遲虛設物件 (lazy ghost),只有在物件已初始化時才會調用解構器。對於代理物件,僅在存在真實實例時才會在其上調用解構器。
ReflectionClass::resetAsLazyGhost() 和 ReflectionClass::resetAsLazyProxy() 方法可能會調用正在重置的物件的解構器。