rfc:lazy-objects

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
rfc:lazy-objects [2024/07/24 14:50] lbarnaudrfc:lazy-objects [2024/08/30 15:32] (current) – Status: Implemented lbarnaud
Line 4: Line 4:
   * Date: 2024-06-03   * Date: 2024-06-03
   * Author: Arnaud Le Blanc <arnaud.lb@gmail.com>, Nicolas Grekas <nicolasgrekas@php.net>   * Author: Arnaud Le Blanc <arnaud.lb@gmail.com>, Nicolas Grekas <nicolasgrekas@php.net>
-  * Status: Under Discussion+  * Status: Implemented
   * First Published at: https://wiki.php.net/rfc/lazy-objects   * First Published at: https://wiki.php.net/rfc/lazy-objects
  
Line 174: Line 174:
   * Calling <php>ReflectionProperty::get[Raw]Value()</php> and <php>set[Raw]Value()</php>   * Calling <php>ReflectionProperty::get[Raw]Value()</php> and <php>set[Raw]Value()</php>
   * Calling <php>ReflectionObject::getProperties()</php> or <php>ReflectionObject::getProperty()</php>   * Calling <php>ReflectionObject::getProperties()</php> or <php>ReflectionObject::getProperty()</php>
 +  * Cloning (see the "Cloning" section)
  
 This behavior makes lazy objects fully transparent to their consumers. This behavior makes lazy objects fully transparent to their consumers.
Line 186: Line 187:
   * Calls to <php>var_dump($lazyObject)</php>, unless ''%%__debugInfo()%%'' is implemented and accesses a property.   * Calls to <php>var_dump($lazyObject)</php>, unless ''%%__debugInfo()%%'' is implemented and accesses a property.
   * Calls to <php>debug_zval_dump($lazyObject)</php>   * Calls to <php>debug_zval_dump($lazyObject)</php>
-  * Cloning, unless ''%%__clone()%%'' is implemented and accesses a property. 
  
 By excluding these cases from triggering initialization, developers can perform certain operations on lazy objects without causing them to initialize, providing finer control over the initialization process. By excluding these cases from triggering initialization, developers can perform certain operations on lazy objects without causing them to initialize, providing finer control over the initialization process.
Line 206: Line 206:
   - Before calling the initializer, the object is marked as non-lazy by detaching it from its initializer. Unlike ghost objects, non-skipped properties are not set to their default value as this would cost some CPU cycles for no practical reasons.   - Before calling the initializer, the object is marked as non-lazy by detaching it from its initializer. Unlike ghost objects, non-skipped properties are not set to their default value as this would cost some CPU cycles for no practical reasons.
   - The initializer is called with the lazy proxy as first parameter.   - The initializer is called with the lazy proxy as first parameter.
-  - The return value of the initializer has to be an instance the same class as the lazy-object, or a parent as long as the proxy does not declare additional properties and doesn't override the <php>__destructor</php> or <php>__clone</php> methods. If these conditions are not true, a <php>TypeError</php> is thrown. See the "Notes" section.+  - The return value of the initializer has to be an instance the same class as the lazy-object, or a parent as long as the proxy does not declare additional properties and doesn't override the <php>__destruct()</php> or <php>__clone()</php> methods. If these conditions are not true, a <php>TypeError</php> is thrown. See the "Notes" section.
   - The real instance is set to the return value.   - The real instance is set to the return value.
-  - The value of properties used with <php>ReflectionProperty::skipLazyInitialization()</php> or <php>setRawValueWithoutLazyInitialization()</php> is discarded. +  - The value of properties used with <php>ReflectionProperty::skipLazyInitialization()</php> or <php>setRawValueWithoutLazyInitialization()</php> is discarded as if <php>unset()</php> was called
-  - Properties that are declared on the real instance are bound to the proxy instanceso that accessing any of these properties on the proxy forwards the operation to the corresponding property on the real instance. This includes properties used with <php>ReflectionProperty::skipLazyInitialization()</php> or <php>setRawValueWithoutLazyInitialization()</php>.+  - After that, any property access on the proxy is forwarded to the real instance. This includes declared, dynamic, non-existing, or properties used with <php>ReflectionProperty::skipLazyInitialization()</php> or <php>setRawValueWithoutLazyInitialization()</php>.
  
 The proxy object is _not_ replaced or substituted for the real instance. After initialization, property accesses on the proxy are forwarded to the real instance. Observing properties of the real instance has the same result as observing the corresponding properties of the proxy. The proxy object is _not_ replaced or substituted for the real instance. After initialization, property accesses on the proxy are forwarded to the real instance. Observing properties of the real instance has the same result as observing the corresponding properties of the proxy.
Line 215: Line 215:
 The real instance is allowed to escape the proxy and to create direct references to itself. This is demonstrated in the section named "About Proxies". The proxy may be released independently of the real instance when it's not referenced anymore. The proxy and real instance have distinct identities.  The real instance is allowed to escape the proxy and to create direct references to itself. This is demonstrated in the section named "About Proxies". The proxy may be released independently of the real instance when it's not referenced anymore. The proxy and real instance have distinct identities. 
  
-Although the initializer receives the proxy object as first parameter, it is not expected to make changes to it (this is allowed, but any changes will be lost during the last step of the initialization). However, the proxy object can be used to make decisions based on the value of some initialized property, or on the class or the object, or on its identity. For example, the initializer may use the value of an initialized property:+Although the initializer receives the proxy object as first parameter, it is not expected to make changes to it (this is allowed, but any changes will be lost during the last step of the initialization). However, the proxy object can be used to make decisions based on the value of some initialized property, or on the class or the object, or on its identity. For example, the initializer may use the value of an initialized property when instantiating the real instance:
  
 <PHP> <PHP>
Line 222: Line 222:
 }); });
 </PHP> </PHP>
- 
-<php>get_class()</php> or the <php>::class</php> constant evaluate to the class name of the proxy, regardless of the actual instance. 
  
 === Common Behavior === === Common Behavior ===
Line 404: Line 402:
 </PHP> </PHP>
  
-If the object is already lazy, a <php>ReflectionException</php> is thrown with the message "Object is already lazy".+ 
 +If the object is an initialized lazy proxy, it is marked as non-lazy before resetting it, and the reference count of the real instance is decreased. If this causes the real instance to be destroyed, its destructor may be called (regardless of the <php>SKIP_DESTRUCTOR</php> flag). 
 + 
 +If the object is lazy and non-initialized, a <php>ReflectionException</php> is thrown with the message "Object is already lazy".
  
 Objects whose all properties were initialized are not lazy anymore, as specified in the "Lifecycle of Lazy Objects" section. It follows that calling this when the class has no properties does not make an object lazy. Objects whose all properties were initialized are not lazy anymore, as specified in the "Lifecycle of Lazy Objects" section. It follows that calling this when the class has no properties does not make an object lazy.
Line 446: Line 447:
 The <php>markLazyObjectAsInitialized()</php> method can be used to mark an object as initialized without calling the initializer. It has no effect if the object is already initialized. The <php>markLazyObjectAsInitialized()</php> method can be used to mark an object as initialized without calling the initializer. It has no effect if the object is already initialized.
  
-Its behavior is the same as described for Ghost Objects in the Initialization Sequence section, except that the initializer is not called.+Its behavior is the same as described for Ghost Objects in the Initialization Sequence section, except that the initializer is not called. After that, the object is indistinguishable from an object that was never lazy, and was created with <php>ReflectionClass::newInstanceWithoutConstructor()</php>, except for the value of properties that were already initialized.
  
 The return value is the object itself. The return value is the object itself.
Line 509: Line 510:
 ==== Cloning ==== ==== Cloning ====
  
-Cloning a lazy object triggers its initialization. After that, the result depend on the strategy being used:+Cloning a lazy object triggers its initialization before cloning itThe result is an initialized object.
  
-For ghost objects: The object is cloned and the clone is returned.+For proxy objects, the proxy and its real instance are clonedand the proxy clone is returned. The <php>__clone()</php> method is called on the real instance and not on the proxy. Accessing any property (declared, dynamic, non-existing) on the proxy clone forwards the operation to the corresponding property on the real instance clone.
  
-For proxy objects: The proxy object and its real instance are cloned. The <php>__clone()</php> method is called on the real instance and not on the proxy. +Rationale: Initialization before cloning ensures that a clone and the original object have separate states. That is, updating the original object or the state of its initializer after cloning should not have an impact on the clone. Cloning the proxy and its real instance, rather than returning a clone of the real instance, ensures that the <php>clone</php> operator always returns an object of the same class.
- +
-Rationale: Initialization before cloning ensures that a clone and the original object have separate states. That is, updating the original object or the state of its initializer after cloning should not have an impact on the clone.+
  
 ==== Readonly properties ==== ==== Readonly properties ====
Line 562: Line 561:
 <PHP> <PHP>
 class Connection { class Connection {
 +    public $prop;
     public function __construct() {     public function __construct() {
         $this->connect();         $this->connect();
Line 573: Line 573:
  
 $reflector = new ReflectionClass(Connection::class); $reflector = new ReflectionClass(Connection::class);
-$connection = $reflector->resetAsLazyGhost($connection); // Calls destructor+$reflector->resetAsLazyGhost($connection); // Calls destructor
  
 $connection = null; // Does not call destructor (object is not initialized) $connection = null; // Does not call destructor (object is not initialized)
Line 765: Line 765:
 Returning an instance of a parent class is not allowed if the proxy class declares additional properties. It would imply that the proxy has a state of its own, which is far-reaching in the implementation. This would impact the behavior of <php>get_object_vars()</php>, <php>foreach</php>, <php>json_encode()</php>, <php>serialize()</php>, etc. Most importantly it would lead to inconsistencies when dynamic properties are involved, as demonstrated in [[https://gist.github.com/arnaud-lb/ef41cdb33c304da9d5839b720804f225|this example]]. Returning an instance of a parent class is not allowed if the proxy class declares additional properties. It would imply that the proxy has a state of its own, which is far-reaching in the implementation. This would impact the behavior of <php>get_object_vars()</php>, <php>foreach</php>, <php>json_encode()</php>, <php>serialize()</php>, etc. Most importantly it would lead to inconsistencies when dynamic properties are involved, as demonstrated in [[https://gist.github.com/arnaud-lb/ef41cdb33c304da9d5839b720804f225|this example]].
  
-Furthermore, the proxy can not override the instance's <php>__destructor</php> or <php>__clone</php> methods. This makes it more obvious which implementation is called, and also opens the possibility of revisiting this without BC breaks in the future.+Furthermore, the proxy can not override the instance's <php>__destruct()</php> or <php>__clone()</php> methods. This makes it more obvious which implementation is called, and also opens the possibility of revisiting this without BC breaks in the future.
  
 In use-cases where the proxy and the real instance are not instances of the same class, the proxy is considered to be aware of laziness, so it can adhere to these constraints. In use-cases where the proxy and the real instance are not instances of the same class, the proxy is considered to be aware of laziness, so it can adhere to these constraints.
 +
 +The externally visible type of a lazy proxy is the type of the proxy object, even if the real object is of a parent type. This includes the get_class() function, the ::class constant, the instanceof operator and type checking in parameter, return and property types.
  
 ===== Future scope ===== ===== Future scope =====
Line 785: Line 787:
 ==== Lazy cloning ==== ==== Lazy cloning ====
  
-The RFC proposes that the <php>clone</php> operator initialize the object before cloning it. This ensures that the state of the clone is independent from the object is was cloned for and avoids inconsistencies when properties are initialized with <php>ReflectionProperty::skipLazyInitialization()</php> or <php>setRawValueWithoutLazyInitialization()</php>.+The RFC proposes that the <php>clone</php> operator initializes the object before cloning it. This ensures that the state of the clone is independent from the object it was cloned from and avoids inconsistencies when properties are initialized with <php>ReflectionProperty::skipLazyInitialization()</php> or <php>setRawValueWithoutLazyInitialization()</php>.
  
-Although it may be possible to implement lazy cloning while preserving <php>clone</php> semantics, numerous edge cases with non-lazy properties make this very complex. Furthermore, we expect that in practice, most clone operations will be closely followed by an initialization of either the clone or the original object, so the extra complexity may not be worth it.+Although it may be possible to implement lazy cloning while preserving <php>clone</php> semantics, numerous edge cases with non-lazy properties make this very complex. Furthermore, we expect that in practice, most clone operations will be closely followed by an initialization of either the clone or the original object, so that the extra complexity may not be worth it.
  
 However, it may be possible to make cloning lazy in the future by introducing a new flag (e.g. <php>DEFER_CLONE</php>). However, it may be possible to make cloning lazy in the future by introducing a new flag (e.g. <php>DEFER_CLONE</php>).
Line 803: Line 805:
  * Add lazy-objects as described to the engine: yes/no (2/3 required to pass)  * Add lazy-objects as described to the engine: yes/no (2/3 required to pass)
  
-===== Patches and Tests =====+Voting started on 2024-07-26 and will end on 2024-08-11 00:00 GMT. 
 + 
 +<doodle title="Add lazy-objects as described to the engine" voteType="single" closed="false" closeon="2024-08-11T00:00:00Z"> 
 +   * Yes 
 +   * No 
 +</doodle> 
 + 
 +===== Implementation =====
  
-https://github.com/arnaud-lb/php-src/tree/lazy-objects+https://github.com/php/php-src/pull/15019
rfc/lazy-objects.1721832610.txt.gz · Last modified: 2024/07/24 14:50 by lbarnaud