|
Lazy ObjectsA lazy object is an object whose initialization is deferred until its state is observed or modified. Some use-case examples include dependency injection components that provide lazy services fully initialized only if needed, ORMs providing lazy entities that hydrate themselves from the database only when accessed, or a JSON parser that delays parsing until elements are accessed. Two lazy object strategies are supported: Ghost Objects and Virtual Proxies, hereafter referred to as "lazy ghosts" and "lazy proxies". In both strategies, the lazy object is attached to an initializer or factory that is called automatically when its state is observed or modified for the first time. From an abstraction point of view, lazy ghost objects are indistinguishable from non-lazy ones: they can be used without knowing they are lazy, allowing them to be passed to and used by code that is unaware of laziness. Lazy proxies are similarly transparent, but care must be taken when their identity is used, as the proxy and its real instance have different identities. Creating Lazy ObjectsIt is possible to create a lazy instance of any user defined class or the stdClass class (other internal classes are not supported), or to reset an instance of these classes to make it lazy. The entry points for creating a lazy object are the ReflectionClass::newLazyGhost and ReflectionClass::newLazyProxy methods. Both methods accept a function that is called when the object requires initialization. The function's expected behavior varies depending on the strategy in use, as described in the reference documentation for each method. Example #1 Creating a Lazy Ghost
The above example will output: lazy ghost object(Example)#3 (0) { ["prop"]=> uninitialized(int) } string(7) "Example" Example::__construct int(1) Example #2 Creating a Lazy Proxy
The above example will output: lazy proxy object(Example)#3 (0) { ["prop"]=> uninitialized(int) } string(7) "Example" Example::__construct int(1) Any access to properties of a lazy object triggers its initialization (including via ReflectionProperty). However, certain properties might be known in advance and should not trigger initialization when accessed: Example #3 Initializing Properties Eagerly
The ReflectionProperty::skipLazyInitialization and ReflectionProperty::setRawValueWithoutLazyInitialization methods offer ways to bypass lazy-initialization when accessing a property. About Lazy Object StrategiesLazy 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. Lifecycle of Lazy ObjectsObjects 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. Initialization TriggersLazy 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. Non-Triggering OperationsThe following specific methods or low-level operations allow access or modification of lazy objects without triggering initialization:
Initialization SequenceThis section outlines the sequence of operations performed when initialization is triggered, based on the strategy in use. Ghost Objects
After initialization, the object is indistinguishable from an object that was never lazy. Proxy Objects
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. Common BehaviorThe 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. CloningCloning 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 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. DestructorsFor 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. |