Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:123938 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by qa.php.net (Postfix) with ESMTPS id 0C2CC1A009C for ; Thu, 27 Jun 2024 14:27:18 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1719498515; bh=NmyDgEXH9b6Xa4o4Ow0xyxdTfOmCt/aydeRjTsDdG5s=; h=References:In-Reply-To:From:Date:Subject:To:Cc:From; b=ERo5Fc0er/l205jAUJLGgbuHjblaMiEpsqyybDJK7vJWLp0FIUCn+j1MjIojLEn71 dn3WCEB8jkWGectcBpSve07f9PeiTTHQPREnQQchs50WQUEInr/CGXG9MPRAvYMcTb 4KANQToKIWO52ABuLu/E+QJdtmyhkdV1ilSHKklDaSVkbE8Wc2soLNFL73Nj21fNaU ZHXHnHBGy8+4G9CcR6TSFhG/Rwcpll/H064CjXT/xK5HTwW5AMw5aehSQ7UdfMj1lr 8ealnvPynAJrGfUi1Mdhe4oUDubpyrPFLkzHgOhgCX4Rzkmq26v2fZ8jWP8Bzd4XC4 Ov7aDLB/mLQbw== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 8B45E180003 for ; Thu, 27 Jun 2024 14:28:34 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.0 (2022-12-13) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=0.6 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_PASS,FREEMAIL_FROM, HTML_MESSAGE,RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H3,RCVD_IN_MSPIKE_WL, SPF_HELO_NONE,SPF_PASS,T_SCC_BODY_TEXT_LINE autolearn=no autolearn_force=no version=4.0.0 X-Spam-Virus: Error (Cannot connect to unix socket '/var/run/clamav/clamd.ctl': connect: Connection refused) X-Envelope-From: Received: from mail-ej1-f42.google.com (mail-ej1-f42.google.com [209.85.218.42]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Thu, 27 Jun 2024 14:28:34 +0000 (UTC) Received: by mail-ej1-f42.google.com with SMTP id a640c23a62f3a-a724cd0e9c2so593480666b.3 for ; Thu, 27 Jun 2024 07:27:15 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1719498435; x=1720103235; darn=lists.php.net; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=fgb9hCor+xA5hAOecClEwyHr343wNgz8Pqv4jQRbNuI=; b=XaGDAFvkXNuvU5wHgFb0iQuhu5hk68SkbmPYwwpEaoYGk2MQ1ORPqlPk9wgkmH3JUa vUC7JIvSR/+D/9ASklbrGDC42U0Rf/rO8jbh+PklOCS43FG3v3wF6f1sad0cN09uaK6t uzWKkXVWrSqsfDKH3poB8jrli6MbGqY4zQdHN/+ykOENqyPKhpdyjUN4AjtcbZ6B1rg9 4hJ9+dOeqX8jR7O2Y3C7uPViuWFtm7jWfon/pkim/VoRq54ZBYqALtYNgvCuxQkjJDQe r4qwJiL7sFi3x0GIZR9lfxYnxJ7JmTUWV+3nKxkQ8Mec6h8gsTl5rsO7MOvoov+D2uob oNFQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1719498435; x=1720103235; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=fgb9hCor+xA5hAOecClEwyHr343wNgz8Pqv4jQRbNuI=; b=sZEfOD8b7haSjGIL1tj38pEEmu2reJGbpnp+zWQPvi8tw3XxtFUle3me3p3mF426C0 AQnGaDj14+tPUzgNXLNlej+LEFCx6/+DzEbuvGV5ffFCcFmaGT55pFU2kA4Eza+x+enT xirrnABJ6HLU/IyH7GPFPItGb9ZUkdYYoD4TaWfM99t7niRRJ1aSRyeFiAX/BOBT4HNn zUvNQmzwrAgCgkc1StGFrIJtQ7Bx7shch05Lg1k3FYw6rYPDrkaW0FH0nkpnZi3QXXk2 8pCBcNzMIKembuYhOrjz/CXmvwzXljthxIcICsDpdSduIPj4JiwbyGXqAaEGRdbZb/5p Ee5w== X-Forwarded-Encrypted: i=1; AJvYcCVtMyqNwL/qH0qkyA+XmPvNXyveeUYD5DadW3ewsQZ5PdKyyIFSaZQ0TJVVIcoyBzpUzCbJS0LswjxOr1dXvhZQtr7d0PEw3g== X-Gm-Message-State: AOJu0YwSONEgQxze04V9apduwXuplXIgA9OhM3u+n/zFDZ3DpVI3nCec OaKRbKfB+CociaHaLsRwAPIriFlPeOS172qUD3Pxc8AfNJscAEPutxAwjwYVcCb9kXmP/sKteGz i7VvDcqNAFKz31ZkcRD3Ybz0e0rWGPqMC X-Google-Smtp-Source: AGHT+IEwIwyw0kncmrDguPfv4PTZA34HehLlTo+uFpC4/fuZS5O3XMTH2fO+B4Z9t0SGLo1GNRhMAHtqcYhV8xHaaNI= X-Received: by 2002:a17:907:c242:b0:a72:8f73:a95a with SMTP id a640c23a62f3a-a728f7407cfmr431587566b.65.1719498434211; Thu, 27 Jun 2024 07:27:14 -0700 (PDT) Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net MIME-Version: 1.0 References: In-Reply-To: Date: Thu, 27 Jun 2024 16:27:02 +0200 Message-ID: Subject: Re: [PHP-DEV] [RFC] Lazy Objects To: Marco Pivetta Cc: Nicolas Grekas , PHP Internals List Content-Type: multipart/alternative; boundary="0000000000005cf979061bdfed39" From: arnaud.lb@gmail.com (Arnaud Le Blanc) --0000000000005cf979061bdfed39 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Hi Marco, Thank you for the very detailed feedback. Please find my answers below. Nicolas will follow-up on some points. On Mon, Jun 24, 2024 at 1:03=E2=80=AFAM Marco Pivetta = wrote: > * lazy proxies should probably explore replacing object identity further > We have thought about a few approaches for that, but none is really concluding: Fusioning the identity of the proxy and the real instance, such that both objects are considered to have the same identity with regard to spl_object_id(), SplObjectStorage, WeakMap, and strict equality after initialization will lead to weird effects. For example, we don't know what the behavior of this should be: ``` $reflector =3D new ReflectionClass(MyClass::class); $realInstance =3D new MyClass(); $proxy =3D $reflector->newLazyProxy(function () use ($realInstance) { return $realInstance; }); $store =3D new SplObjectStorage(); $store[$realInstance] =3D 1; $store[$proxy] =3D 2; $reflector->initialize($proxy); var_dump(count($store)); // 1 or 2 ? ``` A second approach, suggested by Benjamin, would be that properties of the instance returned by the factory are copied to the proxy, the proxy marked as initialized (so it's not a proxy anymore), and the instance returned by the factory discarded. One problem of this approach is that the instance may continue to exist independently of the proxy if it's referenced somewhere. So, proper usage of the lazy proxy API would require the factory to be aware of that, and to return an object that is not referenced anywhere. As we implemented the proxy strategy primarily for use-cases where we don't control the factory, this approach would not work. A third approach would be to replace all references to the proxy by references to the backing instance after initialization. Implementing this approach would be prohibitively slow as it requires to scan the entire object graph of the process. None of these approaches are concluding unfortunately. Having two distinct identities for the proxy and the real instance doesn't appear to be an issue in practice, however (e.g. in Symfony). * don't like expanding `ReflectionClass` further: `LazyGhost` and > `LazyProxy` classes (or such) instead > The rationale for expanding ReflectionClass and ReflectionProperty is that code creating lazy objects tend to also use these two classes, for introspection or to initialize the object. Also, we feel that the new methods are directly related to existing methods. E.g. newLazyGhost() is just another variant of newInstance() and newInstanceWithoutConstructor(), and setRawValueWithoutLazyInitialization() is just another variant of setValue() and setRawValue() (added in the hooks RFC). * `initialize()` shouldn't have a boolean behavioral switch parameter: make > 2 methods > Agreed. We have updated the RFC to split the method into `initalize()` and `markAsInitialized()`. > * flags should be a `list` instead. A bitmask for > a new API feels unsafe and anachronistic, given the tiny performance hit. > Unfortunately this leads to a 30% slowdown in newLazyGhost() when switching to an array of enums, in a micro benchmark. I'm not sure how this would impact a real application, but given this is a performance critical feature, the slowdown is an issue. Besides that, no existing API in core is using an array of enums, and we don't want to introduce this concept in this RFC. > From an abstraction point of view, lazy objects from this RFC are > indistinguishable from non-lazy ones * do the following aspects always apply? I understand they don't for lazy > proxies, just for ghosts? > * `spl_object_id($object) =3D=3D=3D spl_object_id($proxy)`? > * `get_class($object) =3D=3D=3D get_class($proxy)`? > Good catch, this phrase is not true for proxies, with relation to the identity of a proxy and its real instance. Apart from that, the main point of this phrase remains: interaction with a proxy has the same behavior as interaction with a real instance, and they can be used without knowing they are lazy. > Proxies: The initializer returns a new instance, and interactions with > the proxy object are forwarded to this instance > * another note on naming: I used "value holder" inside ProxyManager, > because using just "proxy" as a name led to a lot of confusion. This also > applies to me trying to distinguish proxy types inside the RFC and this > discussion. > The name "Value Holder" appears to be taken for another pattern already: https://martinfowler.com/eaaCatalog/lazyLoad.html The exact name of the pattern used in this RFC is the "Virtual State-Proxy", but others have suggested using just "Proxy" in the RFC. In the API we use "LazyProxy". > Internal objects are not supported because their state is usually not > managed via regular properties. > > * sad, but understandable limitation > * what happens if a class `extends` an internal engine class, like the > gruesome `ArrayObject`? > This also applies to sub-classes of internal objects. This is specified under ReflectionClass::newLazyGhost(), but I've clarified that here too. > > * Given the recent improvements around closures and the `...` syntax ( > https://wiki.php.net/rfc/first_class_callable_syntax), is it worth having > `Closure` only as argument type? > * should we declare generic types in the RFC/docs, even if just in the > stubs? > * they would serve as massive documentation improvement for Psalm and > PHPStan > * it would be helpful to document `$initializer` and `$factory` as > `:void` or `:object` functions > * can the engine check that, perhaps? I have no idea if `Closure` > can provide such information on return types, inside the engine. > Could you say more about the benefits of type hinting as Closure instead of callable? Is this purely to be able to type check the return type earlier? We may be able to achieve this while retaining the callable type hint, when a Closure is given, but this would be unusual. Maybe this is something we can push in a different RFC? > > * what happens if `ReflectionClass#reset*()` methods are used on a > different class instance? > * considered? > The object must be an instance of the class represented by ReflectionClass (including of a sub-class) > > ```php > $reflector->getProperty('id')->skipLazyInitialization($post); > ``` > > * perfect for partial objects / understanding why this was implemented > * would it make sense to have an API to set "bulk" values in an objec= t > this way, instead of having to do this for each property manually? > * avoids instantiating reflection properties / marking individual > properties manually > * perhaps future scope? > * thinking `(new > GhostObject($class))->initializePropertiesTo(['foo' =3D> 'bar', 'baz' =3D= > > 'tab'])` > This is something we thought about but it's not as simple as it seems: we have to support setting private properties of parent classes, so the argument has to represent that. A format that would be able to represent that is as follows: - The argument is a map of class names to properties - The properties are a map of property name to property value. To support skipping a property (and setting it to its default value), numeric keys (no key specified) denote that the value is a property name to skip. Example: class ParentOfA { private $c; private $d =3D 1; } class A extends ParentOfA { private $a; private $b; } ->initializePropertiesTo([ A::class =3D> ['a' =3D> 'value-a', 'b' =3D> 'value-b'], ParentOfA::class =3D> ['c' =3D> 'value-c', 'd'], // 'd' is init to its default value ]) An alternative is to use the same binary format as get_mangled_object_vars(). WDYT? > Initialization Triggers > > * really happy to see all these edge cases being considered here! > * how much of this new API has been tried against the test suite of > (for example) `ocramius/proxy-manager`? > * mostly asking because there's tons of edge cases noted in there > Nicolas has tested the implementation extensively on the VarExporter and DIC components, and on Doctrine. We would be happy if you could check with the ocramius/proxy-manager test suite. > > > Cloning, unless `__clone()` is implemented and accesses a property. > > * how is the initializer of a cloned proxy used? > * Is the initializer cloned too? > The initializer is not cloned. I've clarified this in the RFC. The Initialization Sequence section has an example of how an initializer can detect being called for a clone of the origin lazy object, if necessary: $init =3D function ($object) use (&$originalObject) { if ($object !=3D=3D $originalObject) { // we are initializing a clone }};$originalObject =3D $reflector->newLazyProxy($init); > * what about the object that a lazy proxy forwards state access to? I= s > it cloned too? > Cloning an initialized proxy is the same as cloning the real instance (this clones the real instance and returns it). > > The following special cases do not trigger initialization of a lazy > object: > > * Will accessing a property via a debugger (such as XDebug) trigger > initialization here? > * asking because debugging proxy initialization often led to problems= , > in the past > * sometimes even IDEs crashing, or segfaults > Good point, I will check this. The implementation is biased towards initialization, so accessing a lazy object will usually initialize it. However the internal APIs used by var_dump and get_mangled_object_vars() do not trigger initialization, so if a debugger uses that, it shouldn't trigger initialization. > > * this wording is a bit confusing: > > > Proxy Objects > > The actual instance is set to the return value. > > * considering the following paragraph: > > > The proxy object is _not_ replaced or substituted for the actual > instance. > > Indeed. "actual instance" is supposed to designate the instance that the proxy forwards to, but I see why it's confusing. I've renamed "actual instance" to "real instance". WDYT? > > > After initialization, property accesses on the proxy are forwarded to > the actual instance. > > Observing properties of the proxy has the same result as observing > properties of the actual instance. > > * This is some sort of "quantum locking" of both objects? > * How hard is it to break this linkage? > * Can properties be `unset()`, for example? > * what happens to dynamic properties? > * I don't use them myself, and I discourage their usage, but it > would be OK to just document the expected behavior > The linkage can not be broken. unset() and dynamic properties are not special, in that these are just property accesses. All property accesses on the proxy are forwarded to the real instance. > > If the initializer throws, the object properties are reverted to their > pre-initialization state and the object is > > marked as lazy again. > > * this is some sort of "transactional" behavior > * welcome API, but is it worth having this complexity? > * is there a performance tradeoff? > * is a copy of the original state kept during initializer calls? > * OK with it myself, just probing design considerations > I believe it's worth having this, as it prevents leaving an object in a corrupt state, which could then be accessed later, in case of temporary initializer failure. The performance overhead and complexity are small. A shallow copy of the original state is kept during initialization. As the copy is shallow this is just a few pointer copies and refcount increases. > * the example uses `setRawValueWithoutLazyInitialization()`, and > initialization then accesses `public` properties > * shouldn't a property that now **has** a value not trigger > initialization anymore? > * or does that require > `ReflectionProperty#skipLazyInitialization()` calls, for that to work? > setRawValueWithoutLazyInitialization() has the same effect as skipLazyInitialization(), in addition to setting the specified value. I've clarified this in the RFC. > > > `ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE`: By default, > serializing a lazy object triggers its initialization > > This flag disables that behavior, allowing lazy objects to be serialize= d > as empty objects. > > * how would one deserialize an empty object into a proxy again? > * would this understanding be deferred to the (de-)serializer of > choice? > * exercise for userland? > Yes this is left as exercise to userland when this flag is used. > > > `ReflectionClass::newLazyProxy()` > > The factory should return a new object: the actual instance. > > * what happens if the user mis-implements the factory as `function (objec= t > $proxy): object { return $proxy; }`? > * this is obviously a mistake on their end, but is it somehow > preventable? > Returning a lazy object (including an initialized proxy) is not allowed and will throw. I've clarified this in the RFC (the RFC specified that returning a lazy object was not allowed, but whether this included initialized proxies was not clear). > > > `ReflectionClass::resetAsLazyProxy()` > > The proxy and the actual instance are distinct objects, with distinct > identities. > > * When creating a lazy proxy, all property accesses are forwarded to a ne= w > instance > * are all property accesses re-bound to the new instance? > * are there any leftovers pointing to the old instance anywhere? > * thinking dynamic properties and similar > > Yes all property accesses are forwarded to the new instance after that. There can not be any leftovers to the old instance anywhere, including in dynamic properties (the resetAsLazy*() methods reset the object entirely). > > > ReflectionProperty::setRawValueWithoutLazyInitialization() > > The method does not call hooks, if any, when setting the property value= . > > * So far, it has been possible to `unset($object->property)` to force > `__get` and `__set` to be called > * will `setRawValueWithoutLazyInitialization` skip also this "unset > properties" behavior that is possible in userland? > * this is fine, just a documentation detail to note > * if it is like that, is it worth renaming the method > `setValueWithoutCallingHooks` or such? > * not important, just noting this opportunity > > Yes, setRawValueWithoutLazyInitialization() skips magic methods and hooks. The "setRawValue" part of the method name was borrowed from the ReflectionProperty::setRawValue() method introduced by the hooks RFC, which is an equivalent to setValue() but doesn't call hooks. > > > Destructors > > The destructor of proxy objects is never called. We rely on the > destructor of the proxied instance instead. > > * raising an edge case here: `spl_object_*` and object identity checks ma= y > be used inside a destructor > * for example, a DB connection de-registering itself from a connectio= n > pool somewhere, such as `$pool->deRegister($this)` > * the connection pool may have the `spl_object_id()` of the proxy= , > not the real instance > * this is not a blocker, just an edge case that may require > documentation > * it reconnects with "can we replace the object in-place?" > question above: replacing objects worth exploring > This is an interesting case. This may be an issue if the proxy itself was registered in the pool. In case the initializer or the constructor registers the connection, then the real instance will have been registered. Best Regards, Arnaud > --0000000000005cf979061bdfed39 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Hi Marco,

Th= ank you for the very detailed feedback. Please find my answers below. Nicol= as will follow-up on some points.

On Mon, Jun 24, 2024 at 1:0= 3=E2=80=AFAM Marco Pivetta <ocramius@gmail.com> wrote:
<= div>
=C2=A0* lazy proxies should probably explore replacing object iden= tity further

We= have thought about a few approaches for that, but none is really concludin= g:

Fusioning the identity of the proxy and the rea= l instance, such that both objects are considered to have the same identity= with regard to spl_object_id(), SplObjectStorage, WeakMap, and strict equa= lity after initialization will lead to weird effects.

For example, we don't know what the behavior of this should be:=

```
$reflector =3D new ReflectionCl= ass(MyClass::class);

$realInstance =3D new MyClass= ();
$proxy =3D $reflector->newLazyProxy(function () use ($= realInstance) {
=C2=A0=C2=A0=C2=A0 return $realInstance;
});

$store =3D new SplObjectStorage();
$store[$realInstance] =3D 1;
$store[$proxy] =3D 2;
=

$reflector->initialize($proxy);

=
var_dump(count($store)); // 1 or 2 ?
=C2=A0```

A second approach, suggested by Benjamin, would be that pro= perties of the instance returned by the factory are copied to the proxy, th= e proxy marked as initialized (so it's not a proxy anymore), and the in= stance returned by the factory discarded. One problem of this approach is t= hat the instance may continue to exist independently of the proxy if it'= ;s referenced somewhere. So, proper usage of the lazy proxy API would requi= re the factory to be aware of that, and to return an object that is not ref= erenced anywhere. As we implemented the proxy strategy primarily for use-ca= ses where we don't control the factory, this approach would not work.

A third approach would be to replace all references= to the proxy by references to the backing instance after initialization. I= mplementing this approach would be prohibitively slow as it requires to sca= n the entire object graph of the process.

None= of these approaches are concluding unfortunately. Having two distinct iden= tities for the proxy and the real instance doesn't appear to be an issu= e in practice, however (e.g. in Symfony).

=C2=A0* don't like expanding `ReflectionClas= s` further: `LazyGhost` and `LazyProxy` classes (or such) instead

The rationale for expanding = ReflectionClass and ReflectionProperty is that code creating lazy objects t= end to also use these two classes, for introspection or to initialize the o= bject. Also, we feel that the new methods are directly related to existing = methods. E.g. newLazyGhost() is just another variant of newInstance() and n= ewInstanceWithoutConstructor(), and setRawValueWithoutLazyInitialization() = is just another variant of setValue() and setRawValue() (added in the hooks= RFC).

* `initialize()= ` shouldn't have a boolean behavioral switch parameter: make 2 methods<= /div>

Agreed. = We have updated the RFC to split the method into `initalize()` and `markAsI= nitialized()`.
=C2=A0
=C2= =A0* flags should be a `list<SomeEnumAroundProxies>` instead. A bitma= sk for a new API feels unsafe and anachronistic, given the tiny performance= hit.

Unfortuna= tely this leads to a 30% slowdown in newLazyGhost() when switching to an ar= ray of enums, in a micro benchmark. I'm not sure how this would impact = a real application, but given this is a performance critical feature, the s= lowdown is an issue. Besides that, no existing API in core is using an arra= y of enums, and we don't want to introduce this concept in this RFC.

>= ; From an abstraction point of view, lazy objects from this RFC are indisti= nguishable from non-lazy ones
* do= the following aspects always apply? I understand they don't for lazy p= roxies, just for ghosts?
=C2=A0=C2=A0 * `spl_object_id($objec= t) =3D=3D=3D spl_object_id($proxy)`?
=C2=A0=C2=A0 * `get_class($object) = =3D=3D=3D get_class($proxy)`?

Good catch= , this phrase is not true for proxies, with relation to the identity of a p= roxy and its real instance. Apart from that, the main point of this phrase = remains: interaction with a proxy has the same behavior as interaction with= a real instance, and they can be used without knowing they are lazy.

= > Proxies: The initializer returns a new instance, and interactions with= the proxy object are forwarded to this instance
* another note o= n naming: I used "value holder" inside ProxyManager, because usin= g just "proxy" as a name led to a lot of confusion. This also app= lies to me trying to distinguish proxy types inside the RFC and this discus= sion.

The name = "Value Holder" appears to be taken for another pattern already:= =C2=A0https://martinfowler.com/eaaCatalog/lazyLoad.html
The exact name of the pattern used in thi= s RFC is the "Virtual State-Proxy", but others have suggested usi= ng just "Proxy" in the RFC. In the API we use "LazyProxy&quo= t;.

<= div>
> Internal objects are not supported because their state is usu= ally not managed via regular properties.

* sad, but understandable l= imitation
* what happens if a class `extends` an internal engine = class, like the gruesome `ArrayObject`?

This= also applies to sub-classes of internal objects. This is specified under R= eflectionClass::newLazyGhost(), but I've clarified that here too.
=C2=A0

* Given the recent improvements around closures and the `...` syntax = (https://wiki.php.net/rfc/first_class_callable_syntax), is it w= orth having `Closure` only as argument type?
* should we declare generic= types in the RFC/docs, even if just in the stubs?
=C2=A0 =C2=A0 * they = would serve as massive documentation improvement for Psalm and PHPStan
= =C2=A0 =C2=A0 * it would be helpful to document `$initializer` and `$factor= y` as `:void` or `:object` functions
=C2=A0 =C2=A0 =C2=A0 =C2=A0 * can t= he engine check that, perhaps? I have no idea if `Closure` can provide such= information on return types, inside the engine.

Could you say more about the benefits of = type hinting as Closure instead of callable? Is this purely to be able to t= ype check the return type earlier? We may be able to achieve this while ret= aining the callable type hint, when a Closure is given, but this would be u= nusual. Maybe this is something we can push in a different RFC?
=C2=A0

* what happens if `Reflec= tionClass#reset*()` methods are used on a different class instance?
=C2= =A0 =C2=A0 * considered?

<= /div>
The object must be an instance of the class represented by Reflec= tionClass (including of a sub-class)
=C2=A0

```php
$reflector->getProperty('id')-&g= t;skipLazyInitialization($post);
```

* perfect for partial object= s / understanding why this was implemented
=C2=A0 =C2=A0 * would it make= sense to have an API to set "bulk" values in an object this way,= instead of having to do this for each property manually?
=C2=A0 =C2=A0 = =C2=A0 =C2=A0 * avoids instantiating reflection properties / marking indivi= dual properties manually
=C2=A0 =C2=A0 =C2=A0 =C2=A0 * perhaps future sc= ope?
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0 * thinking `(new Gh= ostObject($class))->initializePropertiesTo(['foo' =3D> 'b= ar', 'baz' =3D> 'tab'])`
=

This is something we thought about but it&= #39;s not as simple as it seems: we have to support setting private propert= ies of parent classes, so the argument has to represent that. A format that= would be able to represent that is as follows:
- The argument is= a map of class names to properties
- The properties are a map of= property name to property value. To support skipping a property (and setti= ng it to its default value), numeric keys (no key specified) denote that th= e value is a property name to skip.

Example:
=

class ParentOfA {
=C2=A0=C2=A0=C2=A0 private = $c;
=C2=A0=C2=A0=C2=A0 private $d =3D 1;
}
class A extends ParentOfA {
=C2=A0=C2=A0=C2=A0 private $a;
=C2=A0=C2=A0=C2=A0 private $b;
}

->initializePropertiesTo([
=C2=A0=C2=A0=C2=A0 A::class =3D&= gt; ['a' =3D> 'value-a', 'b' =3D> 'value-= b'],
=C2=A0=C2=A0=C2=A0 ParentOfA::class =3D> ['c'= =3D> 'value-c', 'd'], // 'd' is init to its def= ault value
])

An alternative is to use t= he same binary format as get_mangled_object_vars().

WDYT?

> Initi= alization Triggers

* really happy to see all these edge cases being = considered here!
=C2=A0 =C2=A0 * how much of this new API has been tried= against the test suite of (for example) `ocramius/proxy-manager`?
=C2= =A0 =C2=A0 =C2=A0 =C2=A0 * mostly asking because there's tons of edge c= ases noted in there

=
Nicolas has tested the implementation extensively on the VarExporter a= nd DIC components, and on Doctrine. We would be happy if you could check wi= th the ocramius/proxy-manager test suite.
=C2=A0

> Cloning, unless `__clone()` is implemented a= nd accesses a property.

* how is the initializer of a cloned proxy u= sed?
=C2=A0 =C2=A0 * Is the initializer cloned too?

The initializer is not cloned. I&#= 39;ve clarified this in the RFC. The=20 Initialization Sequence section has an example of how an initializer can detect being called for a clone of the origin lazy object, if necessary:
$init =
=3D function ($object) use =
(&$originalObject) {
    if (<=
span class=3D"gmail-re0">$object !=3D=3D $originalObject) {
        // we are initializing a clone
    }
};
$originalObject =
=3D $reflector->newLazyProxy($init);
=C2=A0
=C2=A0=C2=A0=C2=A0 * what = about the object that a lazy proxy forwards state access to? Is it cloned t= oo?

Cloning an initialized= proxy is the same as cloning the real instance (this clones the real insta= nce and returns it).
=C2=A0
> The following special cases do not trigger initialization of a lazy o= bject:

* Will accessing a property via a debugger (such as XDebug) t= rigger initialization here?
=C2=A0 =C2=A0 * asking because debugging pro= xy initialization often led to problems, in the past
=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0 * sometimes even IDEs crashing, or segfaults

Good point, I will check this= . The implementation is biased towards initialization, so accessing a lazy = object will usually initialize it. However the internal APIs used by var_du= mp and get_mangled_object_vars() do not trigger initialization, so if a deb= ugger uses that, it shouldn't trigger initialization.
=C2= =A0

* this wording is a bi= t confusing:

> Proxy Objects
> The actual= instance is set to the return value.

* considering the following paragraph:<= /div>

> The proxy object is _not_ replaced or substituted for th= e actual instance.

=
Indeed. "actual instance" is supposed to designate= the instance that the proxy forwards to, but I see why it's confusing.= I've renamed "actual instance" to "real instance".= WDYT?
=C2=A0
> After initialization, property accesses on the proxy are forwarded t= o the actual instance.
> Observing properties of the proxy has the sa= me result as observing properties of the actual instance.

* This is = some sort of "quantum locking" of both objects?
* How hard is = it to break this linkage?
=C2=A0 =C2=A0 * Can properties be `unset()`, f= or example?
=C2=A0 =C2=A0 * what happens to dynamic properties?
=C2= =A0 =C2=A0 =C2=A0 =C2=A0 * I don't use them myself, and I discourage th= eir usage, but it would be OK to just document the expected behavior

The linkage can not b= e broken. unset() and dynamic properties are not special, in that these are= just property accesses. All property accesses on the proxy are forwarded t= o the real instance.

<= br>> If the initializer throws, the object properties are reverted to th= eir pre-initialization state and the object is
> marked as lazy again= .

* this is some sort of "transactional" behavior
=C2= =A0 =C2=A0 * welcome API, but is it worth having this complexity?
=C2=A0= =C2=A0 =C2=A0 =C2=A0 * is there a performance tradeoff?
=C2=A0 =C2=A0 = =C2=A0 =C2=A0 * is a copy of the original state kept during initializer cal= ls?
=C2=A0 =C2=A0 =C2=A0 =C2=A0 * OK with it myself, just probing design= considerations

I believe it's worth having this, as it prevents leaving an object in = a corrupt state, which could then be accessed later, in case of temporary i= nitializer failure.

The performance overhead and c= omplexity are small. A shallow copy of the original state is kept during in= itialization. As the copy is shallow this is just a few pointer copies and = refcount increases.
=C2=A0
= * the example uses `setRawValueWithoutLazyInitialization()`, and initializa= tion then accesses `public` properties
=C2=A0 =C2=A0 * shouldn't a p= roperty that now **has** a value not trigger initialization anymore?
=C2= =A0 =C2=A0 =C2=A0 =C2=A0 * or does that require `ReflectionProperty#skipLaz= yInitialization()` calls, for that to work?

setRawValueWithoutLazyInitialization() has the= same effect as skipLazyInitialization(), in addition to setting the specif= ied value. I've clarified this in the RFC.
=C2=A0

> `ReflectionClass::SKIP_INITIALIZATION_O= N_SERIALIZE`: By default, serializing a lazy object triggers its initializa= tion
> This flag disables that behavior, allowing lazy objects to be = serialized as empty objects.

* how would one deserialize an empty ob= ject into a proxy again?
=C2=A0 =C2=A0 * would this understanding be def= erred to the (de-)serializer of choice?
=C2=A0=C2=A0=C2=A0 * exer= cise for userland?

= Yes this is left as exercise to userland when this flag is used.
<= div>=C2=A0

> `Reflecti= onClass::newLazyProxy()`
> The factory should return a new object: th= e actual instance.

* what happens if the user mis-implements the fac= tory as `function (object $proxy): object { return $proxy; }`?
=C2=A0 = =C2=A0 * this is obviously a mistake on their end, but is it somehow preven= table?

Returnin= g a lazy object (including an initialized proxy) is not allowed and will th= row. I've clarified this in the RFC (the RFC specified that returning a= lazy object was not allowed, but whether this included initialized proxies= was not clear).
=C2=A0
> `ReflectionClass::resetAsLazyProxy()`
> The proxy and the actua= l instance are distinct objects, with distinct identities.

* When cr= eating a lazy proxy, all property accesses are forwarded to a new instance<= br>=C2=A0 =C2=A0 * are all property accesses re-bound to the new instance?<= br>=C2=A0 =C2=A0 * are there any leftovers pointing to the old instance any= where?
=C2=A0 =C2=A0 =C2=A0 =C2=A0 * thinking dynamic properties and sim= ilar


Yes al= l property accesses are forwarded to the new instance after that. There can= not be any leftovers to the old instance anywhere, including in dynamic pr= operties (the resetAsLazy*() methods reset the object entirely).
<= div>=C2=A0

> ReflectionProperty::= setRawValueWithoutLazyInitialization()
> The method does not call hoo= ks, if any, when setting the property value.

* So far, it has been p= ossible to `unset($object->property)` to force `__get` and `__set` to be= called
=C2=A0 =C2=A0 * will `setRawValueWithoutLazyInitialization` skip= also this "unset properties" behavior that is possible in userla= nd?
=C2=A0 =C2=A0 =C2=A0 =C2=A0 * this is fine, just a documentation det= ail to note
=C2=A0 =C2=A0 =C2=A0 =C2=A0 * if it is like that, is it wort= h renaming the method `setValueWithoutCallingHooks` or such?
=C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 * not important, just noting this opportuni= ty


Yes, set= RawValueWithoutLazyInitialization() skips magic methods and hooks.

The "setRawValue" part of the method name was bo= rrowed from the ReflectionProperty::setRawValue() method introduced by the = hooks RFC, which is an equivalent to setValue() but doesn't call hooks.=
=C2=A0

>= Destructors
> The destructor of proxy objects is never called. We re= ly on the destructor of the proxied instance instead.

* raising an e= dge case here: `spl_object_*` and object identity checks may be used inside= a destructor
=C2=A0 =C2=A0 * for example, a DB connection de-registerin= g itself from a connection pool somewhere, such as `$pool->deRegister($t= his)`
=C2=A0 =C2=A0 =C2=A0 =C2=A0 * the connection pool may have the `sp= l_object_id()` of the proxy, not the real instance
=C2=A0 =C2=A0 * this = is not a blocker, just an edge case that may require documentation
=C2= =A0 =C2=A0 =C2=A0 =C2=A0 * it reconnects with "can we replace the obje= ct in-place?" question above: replacing objects worth exploring

This is an interestin= g case. This may be an issue if the proxy itself was registered in the pool= . In case the initializer or the constructor registers the connection, then= the real instance will have been registered.=C2=A0

Best Regards,
Arnaud
--0000000000005cf979061bdfed39--