Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:124178 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 B69B61A009C for ; Tue, 2 Jul 2024 14:49:05 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1719931826; bh=J0vBuq+FrLtwdy3I0qYKpW4zSFifmoviJ9sNVHJqcUw=; h=References:In-Reply-To:From:Date:Subject:To:Cc:From; b=hkEj9Uxs4lrVUzs0USHF9rVAtKnZjpc4RHipuuHZf/vTQmCicPHmKeVPMEPJKEPVK YAfq31r1pdSF6/P7ZKUuYuvTiJ9WnnQ4Wbrn/gcaSnIqKoeAQfbnJdwWHm1TQ+fveW V6mPI0YBJwJB81z2qh7RWbonq/BfMjvuq081TNwaEXkkZRhrt7byeO91wDW+PZHdr9 cRSRI/zuIhMbpW+KxlYV/J5R3GH3QVT+YfL4McqFUJtC4lTJ8acJXqp5QZRuTwmL2q BI5d4un5apkAPlN7ogOMthg4HwGKfdiICx3GgDEFet7PsugWxhmZyrn+f4cwf//c/h RG5AQEX4SG88A== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 55DDD180057 for ; Tue, 2 Jul 2024 14:50:25 +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_H2,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-lf1-f48.google.com (mail-lf1-f48.google.com [209.85.167.48]) (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 ; Tue, 2 Jul 2024 14:50:24 +0000 (UTC) Received: by mail-lf1-f48.google.com with SMTP id 2adb3069b0e04-5295e488248so5547655e87.2 for ; Tue, 02 Jul 2024 07:49:03 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1719931742; x=1720536542; 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=ct6+Bk5efTX1V7chcUQg9t7uRxVkNZEIqmRMlZi50pg=; b=RH+ElJAdTv9f5fV8PeXTvx62FG7jYY3OKphXvaoxdBnwfR0fxoKyHS3Fp8c6rvLjcy vpiBEDY/dkGM1hOM28o776q5dP99azDO4pugTY147fo62MT+hL25W12zXvmH5aABM8da 7ENsf1cwUnalelnTnyW5LBYIIGJNMDhnbf4xycl5AxX/7AlqJ7W2/Ubhtz/Fu9KmHxuf DKkvyEJHEVpeoh6TPwI6oGfNvPrhgi6A+BczBBSpVDD3vH158TdTld+S7iHQ7GplVPkq 8IkgDD6fVwpiV/gl3KhY6utJe61tN2YGiElUvB6wJ0OKOIY2RRYEirodqcjqzKcQf+eh kqHA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1719931742; x=1720536542; 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=ct6+Bk5efTX1V7chcUQg9t7uRxVkNZEIqmRMlZi50pg=; b=F4r4kM/gLqvde11FQHDrKXhbBKj8FO3yCchwFZR9egoL5hjUXqI26QMOE1EC7S1wRU lDk8Dm4Qgt7cLAJC5IkyUHhRyuXEy/coiguiPoV3TcsU3yLtOuUMzJFU6DpaUIOtPqMc zy9S+gm01l1PivuXJWUM77nlat8OtJQ6N2RKcmgs7YVaencILcIqUDS61NrTSsLaXn++ /I66dn4I4ccASansOko0IDqxvmkHjnWUKfvLYBfuZwwHF+SJmVvFSHBMFm2040owZZWJ owETKadbKArhrY8zw9pBohAmM7ySVgBYSjDanaAVZJu18wbJOPtEWtoQ+rJnNvYOYtNR wUOg== X-Forwarded-Encrypted: i=1; AJvYcCU4/x+yxwbhINuv4H/5W8uoC8SklwDgaiSbfIEDXQUcQehfzsymlifhpL7L1wXV1e6DJJSjlcyjsTjts8lPaxrRHP4H1FStpQ== X-Gm-Message-State: AOJu0YzB2H7vIpfRozDM82l0UI+NjTqTMkahLYF0D85Mjri2aE1PuW/9 DtO5R6Lq2yD0G9NNn9i6mhudxWCPnHRC296DzBSBAsHPiugIMNLSaEIVZhBZPkV10o8pZFCV9Ik oc9Aku7J3RHc6Hw2LNMYf1coNORE= X-Google-Smtp-Source: AGHT+IEFJXZzGFnVUBz67sIw9lvjcd2F0jYEE6//3Xd6q8/3GPoX0H1MVtv4DRvNxhxeU+LgqomPjwXePaTyUg7jlw4= X-Received: by 2002:a05:6512:4887:b0:52c:e047:5c38 with SMTP id 2adb3069b0e04-52e82671a01mr4837989e87.15.1719931741626; Tue, 02 Jul 2024 07:49:01 -0700 (PDT) Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net MIME-Version: 1.0 References: <1118bbcd-a7b4-47bf-bf35-1a36ab4628e1@bastelstu.be> <45847b93-02bf-459f-bcd2-81ba35a12c24@bastelstu.be> In-Reply-To: <45847b93-02bf-459f-bcd2-81ba35a12c24@bastelstu.be> Date: Tue, 2 Jul 2024 16:48:49 +0200 Message-ID: Subject: Re: [PHP-DEV] [RFC] Lazy Objects To: =?UTF-8?Q?Tim_D=C3=BCsterhus?= , Benjamin Eberlei , Rob Landers , Valentin Udaltsov , Marco Pivetta Cc: Arnaud Le Blanc , PHP Internals List Content-Type: multipart/alternative; boundary="0000000000007f6a5d061c44d0f0" From: nicolas.grekas+php@gmail.com (Nicolas Grekas) --0000000000007f6a5d061c44d0f0 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Hi Valentin, Marco, Benjamin, Tim, Rob, Thanks for the detailed feedback again, it's very helpful! Let me try to answer many emails at once, in chronological order: The RFC says that Virtual state-proxies are necessary because of circular > references. It's difficult to accept this reasoning, because using > circular references is a bad practice and the given example is something = I > try to avoid by all means in my code. > While discussing this argument about circular references with Arnaud, we realized that with this reasoning, we wouldn't have a garbage collector in the engine. Yet and fortunately, there is one because circular references are an important thing that exists in practice. We have to account for circular references, that's not an option. don't touch `readonly` because of lazy objects: this feature is too niche > to cripple a major-major feature like `readonly`. I would suggest deferri= ng > until after the first bits of this RFC landed. > Following Marco's advice, we've decided to remove all the flags related to the various ways to handle readonly. This also removes the secondary vote. The behavior related to readonly properties is now that they are skipped if already initialized when calling resetAsLazy* methods, throw in the initializer as usual, and are resettable only if the class is not final, as already allowed in userland (and as explained in the RFC). I finally got around to giving the RFC another read. Please apologize if > this email asks questions that have already been answered elsewhere, as > the current mailing list volume makes it hard for me to keep up. > > On 6/14/24 14:13, Arnaud Le Blanc wrote: > >> Is there any reason to call the makeLazyX() methods on an object that > >> was not just freshly created with ->newInstanceWithoutConstructor() > >> then? > > > > There are not many reasons to do that. The only indented use-case that > > doesn't involve an object freshly created with > > ->newInstanceWithoutConstructor() is to let an object manage its own > > laziness by making itself lazy in its constructor: > > > > Okay. But the RFC (and your email) does not explain why I would want do > that. It appears that much of the RFC's complexity (e.g. around readonly > properties and destructors) stems from the wish to support turning an > existing object into a lazy object. If there is no strong reason to > support that, I would suggest dropping that. It could always be added in > a future PHP version. > This capability is needed for two reasons: 1. completeness and 2. feature parity with what can be currently done using magic methods (so that it's already used to solve real-world problems). This relates to Benjamin's question about using a static factory instead of a constructor. This is a valid alternative, but it can be used only when you are in control of the instantiation logic. That's not always the case. E.g. Doctrine uses the "new $class" pattern in its configuration system. Whether this is a good idea or not is not the topic. But this pattern means that as a user of Doctrine, you sometimes have to provide a class name and can't use any other constructor. Doctrine is just an example of course. Another example is when you have a library that wants to make one of its classes lazy: let's say __ construct() is the way for the users of this lib to use it (pretty common), then moving to a static factory is not possible without a BC break. So yes, turning an existing instance lazy is definitely needed. About readonly, see the simplification above. > >>>> - The return value of the initializer has to be an instance of a > parent > >>>> or a child class of the lazy-object and it must have the same > properties. > >>>> > >>>> Would returning a parent class not violate the LSP? Consider the > >>>> following example: > >>>> > >>>> class A { public string $s; } > >>>> class B extends A { public function foo() { } } > >>>> > >>>> $o =3D new B(); > >>>> ReflectionLazyObject::makeLazyProxy($o, function (B $o) { > >>>> return new A(); > >>>> }); > >>>> > >>>> $o->foo(); // works > >>>> $o->s =3D 'init'; > >>>> $o->foo(); // breaks > >>> > >>> $o->foo() calls B::foo() in both cases here, as $o is always the prox= y > >>> object. We need to double check, but we believe that this rule doesn'= t > >>> break LSP. > >> > >> I don't understand what happens with the 'A' object then, but perhaps > >> this will become clearer once you add the requested examples. > > > > The 'A' object is what is called the "actual instance" in the RFC. $o > > acts as a proxy to the actual instance: Any property access on $o is > > forwarded to the actual instance A. > > I've read the updated RFC and it's still not clear to me that returning > an arbitrary =E2=80=9Cactual instance=E2=80=9D object is sound. Especiall= y when private > properties - which for all intents and purposes are not visible outside > of the class - are involved. Consider the following: > > class A { > public function __construct( > public string $property, > ) {} > } > > class B extends A { > public function __construct( > string $property, > private string $foo, > ) { parent::__construct($property); } > > public function getFoo() { > return $this->foo; > } > } > > $r =3D new ReflectionClass(B::class); > $obj =3D $r->newLazyProxy(function ($obj) { > return new A('value'); > }); > var_dump($obj->property); // 'value' > var_dump($obj->getFoo()); // Implicitly accesses A::${'\0B\0foo'} > (i.e. the mangled B::$foo property)? > > Now you might say that B does not have the same properties as A and > creating the proxy is not legal, but then the addition of a new private > property would immediately break the use of the lazy proxy, which > specifically is something that private properties should not be able to d= o. > True, thanks for raising this point. After brainstorming with Arnaud, we improved this behavior by: 1. allowing only parent classes, not child classes 2. requiring that all properties from a real instance have a corresponding one on the proxy OR that the extra properties on the proxy are skipped/set before initialization. This means that it's now possible for a child class to add a property, private or not. There's one requirement: the property must be skipped or set before initialization. For the record, with magic methods, we currently have no choice but to create an inheritance proxy. This means the situation of having Proxy extend Real like in your example is the norm. While doing so, it's pretty common to attach some interface so that we can augment Real with extra capabilities (let's say Proxy implements LazyObjectInterface). Being able to use class Real as a backing store for Proxy gives us a very smooth upgrade path (the implementation of the laziness can remain an internal detail), and it's also sometimes the only way to leverage a factory that returns Real, not Proxy. The cloning behavior appears to be unsound to me. Consider the following: > > class A { > public function __construct( > public string $property, > ) {} > } > class B extends A { > public function foo() { } > } > > function only_b(B $b) { $b->foo(); } > > $r =3D new ReflectionClass(B::class); > $b =3D $r->newLazyProxy(function ($obj) { > return new A('value'); > }); > > $b->property =3D 'init_please'; > > $notActuallyB =3D clone $b; > only_b($b); // legal > only_b($notActuallyB); // illegal > > I'm cloning what I believe to be an instance of B, but get back an A. That is very true. I had a look at the userland implementation and indeed, we keep the wrapper while cloning the backing instance (it's not that we have the choice, the engine doesn't give us any other options). RFC updated. We also updated the behavior when an uninitialized proxy is cloned: we now postpone calling $real->__clone to the moment where the proxy clone is initialized. On 6/27/24 16:27, Arnaud Le Blanc wrote: > >> * 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 > > I'm curious, how did the implementation look like? I'll let Arnaud answer this one. Any access to a non-existant (i.e. dynamic) property will trigger > initialization and this is not preventable using > 'skipLazyInitialization()' and 'setRawValueWithoutLazyInitialization()' > because these only work with known properties? > > While dynamic properties are deprecated, this should be clearly spelled > out in the RFC for voters to make an informed decision. Absolutely. From a behavioral PoV, dynamic vs non-dynamic properties doesn't matter: both kinds are uninitialized at this stage and the engine will trigger object handlers in the same way (it will just not trigger the same object handlers). > If the object is already lazy, a ReflectionException is thrown with > the message =E2=80=9CObject is already lazy=E2=80=9D. > > What happens when calling the method on a *initialized* proxy object? > i.e. the following: > > class Obj { public function __construct(public string $name) {} } > $obj1 =3D new Obj('obj1'); > $r->resetAsLazyProxy($obj, ...); > $r->initialize($obj); > $r->resetAsLazyProxy($obj, ...); > > What happens when calling it for the actual object of an initialized > proxy object? Once initialized, a lazy object should be indistinguishable from a non-lazy one. This means that the second call to resetAsLazyProxy will just do that: reset the object like it does for any regular object. > It's probably not possible to prevent this, but will this > allow for proxy chains? Example: > > class Obj { public function __construct(public string $name) {} } > $obj1 =3D new Obj('obj1'); > $r->resetAsLazyProxy($obj1, function () use (&$obj2) { > $obj2 =3D new Obj('obj2'); > return $obj2; > }); > $r->resetAsLazyProxy($obj2, function () { > return new Obj('obj3'); > }); > var_dump($obj1->name); // what will this print? This example doesn't work because $obj2 doesn't exist when trying to make it lazy but you probably mean this instead? class Obj { public function __construct(public string $name) {} } > $obj1 =3D new Obj('obj1'); > $obj2 =3D new Obj('obj2'); > $r->resetAsLazyProxy($obj1, function () use ($obj2) { > return $obj2; > }); > $r->resetAsLazyProxy($obj2, function () { > return new Obj('obj3'); > }); > var_dump($obj1->name); // what will this print? This will print "obj3": each object is separate from the other from a behavioral perspective, but with such a chain, accessing $obj1 will trigger its initializer and will then access $obj2->name, which will trigger the second initializer then access $obj3->name, which contains "obj3". (I just confirmed with the implementation I have, which is from a previous API flavor, but the underlying mechanisms are the same). I just noticed in the RFC that I don't see any mention of what happens when > running `get_class`, `get_debug_type`, etc., on the proxies, but it does > mention var_dump. > Yes, because there is nothing to say on the topic: turning an instance lazy doesn't change anything regarding the type-system so that these will return the same result - the class of the object. The RFC is in sync with this message, please have a look for clarifications= . Please let me know if any topics remain unanswered. Nicolas --0000000000007f6a5d061c44d0f0 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Hi Valentin, Marco, Benjamin, Tim, Rob,

Thanks for the detailed feedback again, it's very helpf= ul!
Let me try to answer many emails at once, in chronologica= l order:

The RFC says that=C2=A0Virtual state-proxies are necessary because = of=20 circular references. It's=C2=A0difficult to accept=C2=A0this reasoning,= because=20 using circular=C2=A0references is a bad=C2=A0practice and the given example= is=20 something=C2=A0I try to avoid by all means in my code.

While discussing this argume= nt about circular references with Arnaud, we realized that with this reason= ing, we wouldn't have a garbage collector in the engine. Yet and fortun= ately, there is one because circular references are an important thing that= exists in practice. We have to account for circular references, that's= not an option.

don't touch `readonly` because of lazy objects: thi= s feature is too=20 niche to cripple a major-major feature like `readonly`. I would suggest=20 deferring until after the first bits of this RFC landed.
=

Following Marco's advice, we've decided to remo= ve all the flags related to the various ways to handle readonly. This also = removes the secondary vote. The behavior related to readonly properties is = now that they are skipped if already initialized when calling resetAsLazy* = methods, throw in the initializer as usual, and are resettable only if the = class is not final, as already allowed in userland (and as explained in the= RFC).

I finally got around to giving the RFC another read. Please apologize if this email asks questions that have already been answered elsewhere, as the current mailing list volume makes it hard for me to keep up.

On 6/14/24 14:13, Arnaud Le Blanc wrote:
>> Is there any reason to call the makeLazyX() methods on an object t= hat
>> was not just freshly created with ->newInstanceWithoutConstruct= or()
>> then?
>
> There are not many reasons to do that. The only indented use-case that=
> doesn't involve an object freshly created with
> ->newInstanceWithoutConstructor() is to let an object manage its ow= n
> laziness by making itself lazy in its constructor:
>

Okay. But the RFC (and your email) does not explain why I would want do that. It appears that much of the RFC's complexity (e.g. around readonl= y
properties and destructors) stems from the wish to support turning an
existing object into a lazy object. If there is no strong reason to
support that, I would suggest dropping that. It could always be added in a future PHP version.

This capability i= s needed for two reasons: 1. completeness and 2. feature parity with what c= an be currently done using magic methods (so that it's already used to = solve real-world problems).

This relates to Benjam= in's question about using a static factory instead of a constructor. Th= is is a valid alternative, but it can be used only when you are in control = of the instantiation logic. That's not always the case. E.g. Doctrine = uses the "new $class" pattern in its configuration system. Whethe= r this is a good idea or not is not the topic. But this pattern means that = as a user of Doctrine, you sometimes have to provide a class name and can&#= 39;t use any other constructor. Doctrine is just an example of course. Anot= her example is when you have a library that wants to make one of its classe= s lazy: let's say __ construct() is the way for the users of this lib t= o use it (pretty common), then moving to a static factory is not possible w= ithout a BC break.

So yes, turning an existing ins= tance lazy is definitely needed.
About readonly, see the simplifi= cation above.

=C2=A0
>>>> - The return value of the initializer has to be an instanc= e of a parent
>>>> or a child class of the lazy-object and it must have the s= ame properties.
>>>>
>>>> Would returning a parent class not violate the LSP? Consid= er the
>>>> following example:
>>>>
>>>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 class A { public string $s; } >>>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 class B extends A { public func= tion foo() { } }
>>>>
>>>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 $o =3D new B();
>>>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 ReflectionLazyObject::makeLazyP= roxy($o, function (B $o) {
>>>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return new A();
>>>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 });
>>>>
>>>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 $o->foo(); // works
>>>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 $o->s =3D 'init'; >>>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 $o->foo(); // breaks
>>>
>>> $o->foo() calls B::foo() in both cases here, as $o is alway= s the proxy
>>> object. We need to double check, but we believe that this rule= doesn't
>>> break LSP.
>>
>> I don't understand what happens with the 'A' object th= en, but perhaps
>> this will become clearer once you add the requested examples.
>
> The 'A' object is what is called the "actual instance&quo= t; in the RFC. $o
> acts as a proxy to the actual instance: Any property access on $o is > forwarded to the actual instance A.

I've read the updated RFC and it's still not clear to me that retur= ning
an arbitrary =E2=80=9Cactual instance=E2=80=9D object is sound. Especially = when private
properties - which for all intents and purposes are not visible outside of the class - are involved. Consider the following:

=C2=A0 =C2=A0 =C2=A0class A {
=C2=A0 =C2=A0 =C2=A0 =C2=A0public function __construct(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0public string $property,
=C2=A0 =C2=A0 =C2=A0 =C2=A0) {}
=C2=A0 =C2=A0 =C2=A0}

=C2=A0 =C2=A0 =C2=A0class B extends A {
=C2=A0 =C2=A0 =C2=A0 =C2=A0public function __construct(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0string $property,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0private string $foo,
=C2=A0 =C2=A0 =C2=A0 =C2=A0) { parent::__construct($property); }

=C2=A0 =C2=A0 =C2=A0 =C2=A0public function getFoo() {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return $this->foo;
=C2=A0 =C2=A0 =C2=A0 =C2=A0}
=C2=A0 =C2=A0 }

=C2=A0 =C2=A0 $r =3D new ReflectionClass(B::class);
=C2=A0 =C2=A0 $obj =3D $r->newLazyProxy(function ($obj) {
=C2=A0 =C2=A0 =C2=A0 return new A('value');
=C2=A0 =C2=A0 });
=C2=A0 =C2=A0 var_dump($obj->property); // 'value'
=C2=A0 =C2=A0 var_dump($obj->getFoo()); // Implicitly accesses A::${'= ;\0B\0foo'}
(i.e. the mangled B::$foo property)?

Now you might say that B does not have the same properties as A and
creating the proxy is not legal, but then the addition of a new private property would immediately break the use of the lazy proxy, which
specifically is something that private properties should not be able to do.=

True, thanks for raising this point. A= fter brainstorming with Arnaud, we improved this behavior by:
1. = allowing only parent classes, not child classes
2. requiring that= all properties from a real instance have a corresponding one on the proxy = OR that the extra properties on the proxy are skipped/set before initializa= tion.

This means that it's now possible fo= r a child class to add a property, private or not. There's one requirem= ent: the property must be skipped or set before initialization.

For the record, with magic methods, we currently have no = choice but to create an inheritance proxy. This means the situation of havi= ng Proxy extend Real like in your example is the norm. While doing so, it&= #39;s pretty common to attach some interface so that we can augment Real wi= th extra capabilities (let's say Proxy implements LazyObjectInterface).= Being able to use class Real as a backing store for Proxy gives us a very = smooth upgrade path (the implementation of the laziness can remain an inter= nal detail), and it's also sometimes the only way to leverage a factory= that returns Real, not Proxy.


The cloning behavior appears to be unsound to me. Consider the following:

=C2=A0 =C2=A0 =C2=A0class A {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 public function __construct(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 public string $property,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 ) {}
=C2=A0 =C2=A0 =C2=A0}
=C2=A0 =C2=A0 =C2=A0class B extends A {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 public function foo() { }
=C2=A0 =C2=A0 =C2=A0}

=C2=A0 =C2=A0 =C2=A0function only_b(B $b) { $b->foo(); }

=C2=A0 =C2=A0 =C2=A0$r =3D new ReflectionClass(B::class);
=C2=A0 =C2=A0 =C2=A0$b =3D $r->newLazyProxy(function ($obj) {
=C2=A0 =C2=A0 =C2=A0 =C2=A0return new A('value');
=C2=A0 =C2=A0 =C2=A0});

=C2=A0 =C2=A0 =C2=A0$b->property =3D 'init_please';

=C2=A0 =C2=A0 =C2=A0$notActuallyB =3D clone $b;
=C2=A0 =C2=A0 =C2=A0only_b($b); // legal
=C2=A0 =C2=A0 =C2=A0only_b($notActuallyB); // illegal

I'm cloning what I believe to be an instance of B, but get back an A.

That is very true. I had a look at the userl= and implementation and indeed, we keep the wrapper while cloning the backin= g instance (it's not that we have the choice, the engine doesn't gi= ve us any other options).
RFC updated.

W= e also updated the behavior when an uninitialized proxy is cloned: we now p= ostpone calling $real->__clone to the moment where the proxy clone is in= itialized.


On 6/27/24 16:27, Arnaud Le Blanc wrote:
>>=C2=A0 =C2=A0* flags should be a `list<SomeEnumAroundProxies>= ` instead. A bitmask for
>> a new API feels unsafe and anachronistic, given the tiny performan= ce hit.
>>
>
> Unfortunately this leads to a 30% slowdown in newLazyGhost() when swit= ching
> 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
I'm curious, how did the implementation look like?
I'll let Arnaud answer this one.

<= br>
=C2=A0 Any access to a non-existant (i.e. dynamic) property will trigger
initialization and this is not preventable using
'skipLazyInitialization()' and 'setRawValueWithoutLazyInitializ= ation()'
because these only work with known properties?

While dynamic properties are deprecated, this should be clearly spelled out in the RFC for voters to make an informed decision.
Absolutely. From a behavioral PoV, dynamic vs non-dynamic prop= erties doesn't matter: both kinds are uninitialized at this stage and t= he engine will trigger object handlers in the same way (it will just not tr= igger the same object handlers).


=C2=A0 > If the object is alr= eady lazy, a ReflectionException is thrown with
the message =E2=80=9CObject is already lazy=E2=80=9D.

What happens when calling the method on a *initialized* proxy object?
i.e. the following:

=C2=A0 =C2=A0 =C2=A0class Obj { public function __construct(public string $= name) {} }
=C2=A0 =C2=A0 =C2=A0$obj1 =3D new Obj('obj1');
=C2=A0 =C2=A0 =C2=A0$r->resetAsLazyProxy($obj, ...);
=C2=A0 =C2=A0 =C2=A0$r->initialize($obj);
=C2=A0 =C2=A0 =C2=A0$r->resetAsLazyProxy($obj, ...);

What happens when calling it for the actual object of an initialized
proxy object?

Once initialized, a lazy obje= ct should be indistinguishable from a non-lazy one.
This means th= at the second call to resetAsLazyProxy will just do that: reset the object = like it does for any regular object.

=C2=A0
<= blockquote class=3D"gmail_quote" style=3D"margin:0px 0px 0px 0.8ex;border-l= eft:1px solid rgb(204,204,204);padding-left:1ex">It's probably not poss= ible to prevent this, but will this
allow for proxy chains? Example:

=C2=A0 =C2=A0 =C2=A0class Obj { public function __construct(public string $= name) {} }
=C2=A0 =C2=A0 =C2=A0$obj1 =3D new Obj('obj1');
=C2=A0 =C2=A0 =C2=A0$r->resetAsLazyProxy($obj1, function () use (&$o= bj2) {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0$obj2 =3D new Obj('obj2');
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return $obj2;
=C2=A0 =C2=A0 =C2=A0});
=C2=A0 =C2=A0 =C2=A0$r->resetAsLazyProxy($obj2, function () {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return new Obj('obj3');
=C2=A0 =C2=A0 =C2=A0});
=C2=A0 =C2=A0 =C2=A0var_dump($obj1->name); // what will this print?

This example doesn't work because $obj2 doe= sn't exist when trying to make it lazy but you probably mean this inste= ad?


=C2=A0 =C2=A0 =C2=A0class Obj { public function __constr= uct(public string $name) {} }
=C2=A0 =C2=A0=C2=A0 $obj1 =3D new Obj('= ;obj1');
=C2=A0 =C2=A0 =C2=A0$obj2 =3D new Obj('obj2');
=C2=A0 =C2=A0=C2=A0 $r->resetAsLazyProxy($obj1, function () use ($obj2) = {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return $obj2;
=C2=A0 =C2=A0 =C2=A0});
=C2=A0 =C2=A0 =C2=A0$r->resetAsLazyProxy($obj2, function () {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return new Obj('obj3');
=C2=A0 =C2=A0 =C2=A0});
=C2=A0 =C2=A0 =C2=A0var_dump($obj1->name); // what will this print?

This will print "obj3": each object i= s separate from the other from a behavioral perspective, but with such a ch= ain, accessing $obj1 will trigger its initializer and will then access $obj= 2->name, which will trigger the second initializer then access $obj3->= ;name, which contains "obj3".
(I just confirmed with th= e implementation I have, which is from a previous API flavor, but the under= lying mechanisms are the same).


I just noticed in the R= FC that I don't see any mention of what happens when running `get_class`, `get_debug_type`, etc., on the=20 proxies, but it does mention var_dump.

Yes, because there is nothing to say on the topic: turning an insta= nce lazy doesn't change anything regarding the type-system so that thes= e will return the same result - the class of the object.

The RFC is in sync with this message, please have a look for clarifi= cations.

Please let me know if any topics remain u= nanswered.

Nicolas
--0000000000007f6a5d061c44d0f0--