On Thu, Jul 11, 2024, at 20:31, Tim Düsterhus wrote:
> Hi
> 
> On 7/11/24 10:32, Nicolas Grekas wrote:

... snip

> > 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.
> 
> > 
> >> ? 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?
> 
> 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?
> 
> 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.

I think it would be up to the developer writing the proxy framework to use or 
abuse this, for example, I've been trying for years to get some decent 
semantics of value objects in PHP (I may or may not create an RFC for it once 
I've finished all my research), but, this seems like a perfectly usable case 
that creates the principle of least astonishment for value objects. For 
example, if you have an immutable Money(10) and clone Money(10) .... is there 
any reason to create a new Money(10)? Currently, clone's default behavior is 
already astonishing for value objects! The instance doesn't matter; it's the 
value that matters. For service objects, it may be the same thing -- at least, 
IMHO, services shouldn't have state, just behavior. For non-value objects, such 
as those in the domain, maybe they should be fetched anew from the DB, created 
newly from a cache, or cloned from an existing instance.

The point is, that this can have framework-level behavior that simply isn't 
possible right now because there is no way to control a clone operation 
properly. I'm actually quite excited to have some more control over cloning 
(even in this limited form) because the current behavior of __clone is so 
cobbled that it is barely usable except for the most basic of programs, and 
currently, the only solution is to disable cloning when it will break 
assumptions.

— Rob

Reply via email to