Hi
On 7/15/24 10:23, Nicolas Grekas wrote:
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 guess we have to agree to disagree here.
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!
Sweet. Unless I've missed anything the bit about the value being unset
and the destructor implications is missing in the RFC text. It should be
added. Also the "Properties that are declared on the real instance are
bound to the proxy instance" bit because slightly misleading with the
newest change, because the proxy may no longer define additional properties.
May I suggest something along the lines of the following:
The proxy's properties will be bound to the proxy instance, so that
accessing any of these properties on the proxy forwards the operation to
the corresponding property on the real instance as if the proxy's
property was a virtual property. Any value stored within the proxy's
properties will be unset() and the destructor will be called if the
proxy held the last reference. This includes properties used with
ReflectionProperty::skipLazyInitialization() or
setRawValueWithoutLazyInitialization().
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.
Yes, it's clear that the should have separate states. The issue I'm
having here is that the actual cloning does not happen at the time of
the `clone` operation, but at an arbitrary later point in time and this
can have some odd consequences for the object lifecycles. Perhaps my
example was too simplified.
Let me try to expand the example a little.
class SomeObj { public string $foo = 'A'; public string $dummy; }
$predefinedObject = new SomeObj();
$initializer = function () use ($predefinedObject) {
return $predefinedObject;
};
$myProxy = $r->newLazyProxy($initializer);
$otherProxy = $r->newLazyProxy($initializer);
$r->getProperty('foo')->skipLazyInitialization($myProxy);
$clonedProxy = clone $myProxy;
var_dump($clonedProxy->foo);
$r->initialize($myProxy);
$r->initialize($otherProxy);
$myProxy->foo = 'B';
$r->initialize($clonedProxy);
var_dump($clonedProxy->foo);
I would expect that this dumps 'A' both of the times, because at the
time of cloning the $foo property held the the value 'A'. But my
understanding is that it returns 'A' at the first time and 'B' at the
second time, because `$predefinedObject` is cloned at the time of the
`$r->initialize($clonedProxy);` call.
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.
See above.
Best regards
Tim Düsterhus