レイジーオブジェクト

レイジーオブジェクトは、状態が参照または変更されるまで 初期化が遅延されるオブジェクトです。ユースケースの例として、 必要な時だけ初期化される遅延DIコンポーネント、 必要な時だけデータを読み込む遅延ORM、 必要な時だけ解析を行う遅延JSONパーサーなどがあります。

レイジーオブジェクトには、ゴーストオブジェクトとバーチャルプロキシの 2つの戦略があります。以降 "レイジーゴースト"、 "レイジープロキシ" と呼びます。 どちらの戦略の場合も、レイジーオブジェクトには 最初に状態が参照または変更されたときに自動的に呼び出されるイニシャライザまたはファクトリが 接続されています。抽象的な観点では、レイジーゴーストオブジェクトは 非レイジーなものと区別がつかず、レイジーであること意識せず使用できます。 レイジープロキシも同様に透過的ですが、実体を 参照する際は注意が必要です。プロキシと実インスタンスは異なる実体を 持つからです。

レイジーオブジェクトの作成

任意のユーザー定義クラスや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を使用して、 レイジーにできます。その後、以下のいずれかの操作により初期化されます:

  • 自動初期化をトリガーする方法でオブジェクトと対話する。 初期化トリガーを 参照してください。
  • ReflectionProperty::skipLazyInitializationまたは ReflectionProperty::setRawValueWithoutLazyInitializationを使用して、 すべてのプロパティを非レイジーとしてマークする。
  • ReflectionClass::initializeLazyObjectまたは ReflectionClass::markLazyObjectAsInitializedを 明示的に呼び出す。

すべてのプロパティが非レイジーとしてマークされると、レイジーオブジェクトは 初期化済とみなされます。従って上記のメソッドは、非レイジーなプロパティがない場合、 オブジェクトをレイジーとみなしません。

初期化トリガー

レイジーオブジェクトは、利用者に対して透過的に設計されているため、 オブジェクトの状態を参照または変更する通常の操作は、 その実行の前に自動的に初期化をトリガーします。これには以下の操作が含まれますが、 これらに限定されません:

  • プロパティの読み取りまたは書き込み。
  • プロパティが設定されているかテスト、またはプロパティの削除。
  • ReflectionProperty::getValueReflectionProperty::getRawValueReflectionProperty::setValueReflectionProperty::setRawValue によるプロパティの参照または変更。
  • ReflectionObject::getPropertiesReflectionObject::getPropertyget_object_vars によるプロパティの取得。
  • IteratorIteratorAggregateを 実装していないオブジェクトを foreachでイテレーション。
  • serializejson_encodeなどでシリアライズ。
  • クローンの作成。

オブジェクトの状態にアクセスしないメソッド呼び出しは初期化を トリガーしません。同様に、マジックメソッドやフック関数を呼び出す オブジェクトとの対話も、これらのメソッドや関数がオブジェクトの状態に アクセスしない限りトリガーしません。

初期化をトリガーしない操作

以下の特定のメソッドや低レベルの操作は、初期化をトリガーせずにレイジー オブジェクトへのアクセスや変更を可能にします:

  • ReflectionProperty::skipLazyInitializationReflectionProperty::setRawValueWithoutLazyInitialization でプロパティを非レイジーとしてマーク。
  • get_mangled_object_vars や、 配列への変換による プロパティ内部表現の取得。
  • ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE が設定された状態での serialize 。 ただし __serialize() または __sleep() が初期化をトリガーしない場合。
  • ReflectionObject::__toString の呼び出し。
  • var_dump または debug_zval_dump 。ただし、 __debugInfo() が初期化をトリガー しない場合に限る。

初期化シーケンス

このセクションでは、初期化がトリガーされたときに実行される操作の順序を 使用する戦略に応じて説明します。

ゴーストオブジェクト

  • オブジェクトは非レイジーとしてマークされます。
  • ReflectionProperty::skipLazyInitializationまたは ReflectionProperty::setRawValueWithoutLazyInitialization で初期化されていないプロパティは、デフォルト値があれば それに設定されます。結果的に、事前に初期化済のプロパティを除き、 ReflectionClass::newInstanceWithoutConstructor でと似たオブジェクトになります。
  • そのオブジェクトをパラメータとして、イニシャライザ関数が呼び出されます。 この関数は、オブジェクトの状態を初期化することが 期待されますが、必須ではありません。null を返すか、値を返さない必要があります。 オブジェクトはもはやレイジーではないので、 関数はプロパティに直接アクセスできます。

初期化後、オブジェクトはレイジーでない場合と 区別がつきません。

プロキシオブジェクト

  • オブジェクトは非レイジーとしてマークされます。
  • ゴーストオブジェクトとは異なり、この段階でオブジェクトのプロパティは 変更されません。
  • オブジェクトがファクトリ関数に入力されます。 この関数は、互換性のあるクラスの非レイジーなインスタンスを返す必要があります ( ReflectionClass::newLazyProxyを参照)。
  • 返されたインスタンスは 実インスタンス として参照され、 プロキシに接続されます。
  • プロキシのプロパティ値は、 unset と同等の方法で破棄されます。

初期化後、プロキシの任意のプロパティへのアクセスは、 実インスタンスへのアクセスと同じ結果をもたらします。 宣言済プロパティ、動的プロパティ、存在しないプロパティ、 ReflectionProperty::skipLazyInitializationReflectionProperty::setRawValueWithoutLazyInitialization で マークされたプロパティを含む、すべてのプロパティへのアクセスは 実インスタンスに転送されます。

プロキシオブジェクトが、 実インスタンスに置き換えられることはありません

ファクトリは最初のパラメータとしてプロキシを受け取りますが、 それを変更することは期待されていません(変更は許可されますが、 最終的な初期化ステップ中に失われます)。しかし、プロキシは 事前に初期化されたプロパティの値、クラス、オブジェクト自体、 その同一性に基づく決定に使用できます。例えば、イニシャライザは 実インスタンスを作成する際に初期化されたプロパティの値を利用するかもしれません。

共通の動作

イニシャライザまたはファクトリ関数のスコープと$thisの コンテキストは変更されず、通常の可視性制約が適用されます。

初期化が成功した後、イニシャライザまたはファクトリ関数は オブジェクトから参照されなくなり、他に参照がなければ 解放される場合があります。

イニシャライザが例外をスローした場合、オブジェクトの状態は 初期化前の状態に戻され、オブジェクトは再びレイジーとマークされます。つまり、 オブジェクトへの副作用はすべて破棄されます。これは、失敗した場合に 壊れたインスタンスが生成されてしまうのを防ぎます。ただし、他のオブジェクトへの 影響など、外部への副作用は元に戻されません。

クローン

レイジーオブジェクトをクローンすると、 クローンが作成される前に初期化がトリガーされ、 結果として初期化されたオブジェクトが得られます。

プロキシオブジェクトの場合、プロキシとその実インスタンスの両方がクローンされ、 プロキシのクローンが返されます。 __cloneメソッドは プロキシではなく実インスタンス上で呼び出されます。 クローンされたプロキシと実インスタンスは 初期化時にリンクされるため、クローン後のプロキシへのアクセスは クローン後の実インスタンスに転送されます。

この動作により、クローンと元のオブジェクトは独立した状態を持つことが 保証されます。クローン後に元のオブジェクトまたはそのイニシャライザの状態に 変更を加えても、クローンには影響しません。実インスタンスのみではなく 両方をクローンすることで、クローン操作は常に同じクラスのオブジェクトを 返すことを保証します。

デストラクタ

レイジーゴーストの場合、オブジェクトが初期化されている場合のみ、 プロキシの場合、実インスタンスが存在する場合のみ、 デストラクタが呼び出されます。

ReflectionClass::resetAsLazyGhostおよび ReflectionClass::resetAsLazyProxyメソッドは、 リセットされるオブジェクトのデストラクタを呼び出す場合があります。