Le jeu. 11 juil. 2024 à 20:31, Tim Düsterhus <t...@bastelstu.be> a écrit :

> Hi
>
> On 7/11/24 10:32, Nicolas Grekas wrote:
> >> Many things are already possible in userland. That does not always mean
> >> that the cost-benefit ratio is appropriate for inclusion in core. I get
> >> behind the two examples in the “About Lazy-Loading Strategies” section,
> >> but I'm afraid I still can't wrap my head why I would want an object
> >> that makes itself lazy in its own constructor: I have not yet seen a
> >> real-world example.
> >>
> >
> > Keeping this capability for userland is not an option for me as it would
> > mostly defeat my goal, which is to get rid of any userland code on this
> > topic (and is achieved by the RFC).
> >
> > Here is a real-world example:
> >
> https://github.com/doctrine/DoctrineBundle/blob/2.12.x/src/Repository/LazyServiceEntityRepository.php
> >
> > This class currently uses a poor-man's implementation of lazy objects and
> > would greatly benefit from resetAsLazyGhost().
> >
>
> Sorry, I was probably a little unclear with my question. I was not
> specifically asking if anyone did that, because I am fairly sure that
> everything possible has been done before.
>
> I was interested in learning why I would want to promote a
> "LazyServiceEntityRepository" instead of the user of my library just
> making the "ServiceEntityRepository" lazy themselves.
>
> I understand that historically making the "ServiceEntityRepository" lazy
> yourself would have been very complicated, but the new RFC makes this
> super easy.
>
> So based on my understanding the "LazyServiceEntityRepository"
> (c|sh)ould be deprecated with the reason that PHP 8.4 provides all the
> necessary tools to do it yourself, no? That would also match your goal
> of getting rid of userland code on this topic.
>

That class is not a candidate for deprecation even after this RFC. But
thanks to this RFC, PHP 8.4 would allow removing all the code related to
the magic methods in the current implementation.



> To me this is what the language evolution should do: Enable users to do
> things that previously needed to be provided by userland libraries,
> because they were complicated and fragile, not enabling userland
> libraries to simplify things that they should not need to provide in the
> first place because the language already provides it.
>

That's exactly it: instead of using a third party lib (or in this case
implementing a poor man's subset of a correct lazy object implementation),
the engine would enable using a native feature to achieve a fully correct
behavior in a very simple way. In this case, LazyServiceEntityRepository
would directly use ReflectionClass::resetAsLazyGhost to make itself lazy,
so that its consumers get a packaged behavior and don't need to care about
the topic while consuming the class.



> >> I have one question regarding the updated initialization sequence. The
> >> RFC writes:
> >>
> >>> Properties that are declared on the real instance are uninitialized on
> >> the proxy instance (including overlapping properties used with
> >> ReflectionProperty::skipLazyInitialization() or
> >> setRawValueWithoutLazyInitialization()) to synchronize the state shared
> by
> >> both instances.
> >>
> >> I do not understand this. Specifically I do not understand the "to
> >> synchronize the state" bit.
> >
> >
> > We reworded this sentence a bit. Clearer?
>
> Yes, I think it is clearer. Let me try to rephrase this differently to
> see if my understanding is correct:
>
> ---
>
> For every property on that exists on the real instance, the property on
> the proxy instance effectively [1] is replaced by a property hook like
> the following:
>
>      public PropertyType $propertyName {
>          get {
>              return $this->realInstance->propertyName;
>          }
>          set(PropertyType $value) {
>              $this->realInstance->propertyName = $value;
>          }
>      }
>
> And value that is stored in the property will be freed (including
> calling the destructor if it was the last reference), as if `unset()`
> was called on the property.
>
> [1] No actual property hook will be created and the `realInstance`
> property does not actually exist, but the semantics behave as if such a
> hook would be applied.
>

Conceptually, you've got it right yes!



> >> My understanding is that the proxy will
> >> always forward the property access, so there effectively is no state on
> >> the proxy?!
> >
> >
> > It follows that more properties can exist on the proxy itself (declared
> by
> > child classes of the real object that the proxy implements).
> >
>
> Right, that's mentioned in (2), so all clear.
>
> >
> >>> 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.
> >>
> >> Do I understand it correctly that the initializer of the cloned proxy is
> >> effectively replaced by the following:
> >>
> >>       function (object $clonedProxy) use ($originalProxy) {
> >>           return clone $originalProxy->getRealObject();
> >>       }
> >>
> >
> > Nope, that's not what we describe in the RFC so I hope you can read it
> > again and get where you were confused and tell us if we're not clear
> enough
> > (to me we are :) )
>
> The "cloning of the real instance" bit is what lead me to this
> understanding.
>
> > The $originalProxy is *not* shared with $clonedProxy. Instead, it's
> > *initializers* that are shared between clones.
> > And then, when we call that shared initializer in the $clonedProxy, we
> > clone the returned instance, so that even if the initializer returns a
> > shared instance, we don't share anything with the $originalProxy.
> >
>
> Ah, so you mean if the initializer would look like this instead of
> creating a fresh object within the initializer?
>
>       $predefinedObject = new SomeObj();
>       $myProxy = $r->newLazyProxy(function () use ($predefinedObject) {
>           return $predefinedObject;
>       });
>       $clonedProxy = clone $myProxy;
>       $r->initialize($myProxy);
>       $r->initialize($clonedProxy);
>
> It didn't even occur to me that one would be able to return a
> pre-existing object: I assume that simply reusing the initializer would
> create a separate object and that would be sufficient to ensure that the
> cloned instance would be independent.
>

E.g. Doctrine's Entity Manager returns always the same instance.



> >> ? Then I believe this is unsound. Consider the following:
> >>
> >>       $myProxy = $r->newLazyProxy(...);
> >>       $clonedProxy = clone $myProxy;
> >>       $r->initialize($myProxy);
> >>       $myProxy->someProp++;
> >>       var_dump($clonedProxy->someProp);
> >>
> >> The clone was created before `someProp` was modified, but it outputs the
> >> value after modification!
> >>
> >> Also: What happens if the cloned proxy is initialized *before* the
> >> original proxy? There is no real object to clone.
> >>
> >> I believe the correct behavior would be: Just clone the proxy and keep
> >> the same initializer. Then both proxies are actually fully independent
> >> after cloning, as I would expect from the clone operation.
> >>
> >
> > That's basically what we do and what we describe in the RFC, just with
> the
> > added lazy-clone operation on the instance returned by the initializer.
> >
>
> This means that if I would return a completely new object within the
> initializer then for a cloned proxy the new object would immediately be
> cloned and the original object be destructed, yes?
>

That's correct. This preserves the lifecycle of cloned objects: __clone()
has to be called when creating clones if the method exists.



> Frankly, thinking about this cloning behavior gives me a headache,
> because it quickly leads to very weird semantics. Consider the following
> example:
>
>       $predefinedObject = new SomeObj();
>       $initializer = function () use ($predefinedObject) {
>           return $predefinedObject;
>       };
>       $myProxy = $r->newLazyProxy($initializer);
>       $otherProxy = $r->newLazyProxy($initializer);
>       $clonedProxy = clone $myProxy;
>       $r->initialize($myProxy);
>       $r->initialize($otherProxy);
>       $r->initialize($clonedProxy);
>
> To my understanding both $myProxy and $otherProxy would share the
> $predefinedObject as the real instance and $clonedProxy would have a
> clone of the $predefinedObject at the time of the initialization as its
> real instance?
>

Correct. The sharing is not specifically related to cloning. But when
cloning happens, the expected behavior is well defined: we should have
separate states.



> To me this sounds like cloning an uninitialized proxy would need to
> trigger an initialization to result in semantics that do not violate the
> principle of least astonishment.
>

Forcing an initialization when cloning would be unexpected. E.g. in
Doctrine, when you clone an uninitialized entity, you don't trigger a
database roundtrip. Instead, you create a new object that still references
the original state internally, but under a different object identity. This
cloning behavior is the one we've had for more than 10 years and I think
it's also the least astonishing one - at least if we consider this example
as a real world trial of this principle.



> I would assume that cloning a proxy is something that rarely happens,
> because my understanding is that proxies are most useful for service
> objects, whereas ghost objects would be used for entities / value
> objects, so this should not be too much of a problem.
>

I share this opinion. Then, since the difference between ghost vs proxy
should be internal from a consumer PoV, defining an observable behavior for
ghost objects also defines the behavior for proxy ones.



> >> 2.
> >>
> >>   > Properties are not initialized to their default value yet (they are
> >> initialized before calling the initializer).
> >>
> >> I see that you removed the bit about this being not observable. What is
> >> the reason that you removed that? One possible reason that comes to my
> >> mind is a default value that refers to a non-existing constant. It would
> >> be observable because the initialization emits an error. Are there any
> >> other reasons?
> >>
> >
> > That's because this is observable using e.g. (array) or var_dump.
> >
>
> I see. Perhaps add a short sentence with the reasoning. Something like:
>
> Properties are not initialized to their default value yet (they are
> initialized before calling the initializer). As an example, this has an
> impact on the behavior of an (array) cast on uninitialized objects and
> also when the default value is based on a constant that is not yet
> defined when creating the lazy object, but will be defined at the point
> of initialization.
>

Thanks for the proposal. I've added it to the RFC.

Cheers,
Nicolas

Reply via email to