Hi Tim,

We have updated the RFC to address your feedback. Please find
additional answers below.

On Wed, Jun 5, 2024 at 8:25 PM Tim Düsterhus <t...@bastelstu.be> 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:

```
class C {
     public function __construct() {
        new ReflectionLazyObjectFactory(C::class)->makeInstanceLazyGhost($this,
$this->init(...));
    }
}
```

When drafting this API we figured that makeLazy addressed all
use-cases, and this allowed us to keep the API minimal. However,
following different feedback we have now introduced two separate
newLazyGhostInstance() and newLazyProxyInstance() methods.

> Anything I do with the object before the call to makeLazyX() will
> effectively be reverted, no?

Yes, after calling makeLazy() the object is in the same state as if it
was made lazy immediately after instantiation.

> An example showcasing the intended usage, e.g. a simplified ORM example,
> would really be helpful here.

Agreed

> > in userland by iterating on all properties via the Reflection API, and
> > using unset() in the right scope with a Closure.
> >
> > spl_object_id(), spl_object_hash(), SplObjectStorage, WeakMap,
> > WeakReference, strict equality, etc are not affected by makeLazy*().
>
> That is true for *both* makeLazyGhost(), and makeLazyProxy()?
>
> What would the following example output?
>
>      $object = new MyObject();
>      var_dump(spl_object_id($object));
>      $r = ReflectionLazyObject::makeLazyGhost($object, function
> (MyObject $object) {
>          $object2 = new MyObject();
>          var_dump(spl_object_id($object2));
>          return $object2;
>      });
>      var_dump(spl_object_id($object));
>      $r->initialize();
>      var_dump(spl_object_id($object));
>
> What would happen if I would expose the inner $object2 to the outer
> world by means of the super globals or by means of `use (&$out)` + `$out
> = $object2`?

We have clarified the RFC on these points

> > The intended use of makeLazyGhost() and makeLazyProxy() is to call
> > them either on an object created with
> > ReflectionClass::newInstanceWithoutConstructor(), or on $this in a
> > constructor. The latter is the reason why these APIs take an existing
> > object.
>
> Okay, that answers the question above. Technically being capable of
> calling it on an object that was not just freshly created sounds like a
> footgun, though. What is the interaction with readonly objects? My
> understanding is that it would allow an readonly object with initialized
> properties to change after-the-fact?

Good point about readonly, this is something we had overlooked. We
have updated the RFC to address this.

> >> Consider this example:
> >>
> >>       class Foo {
> >>         public function __destruct() { echo __METHOD__; }
> >>       }
> >>
> >>       class Bar {
> >>         public string $s;
> >>         public ?Foo $foo;
> >>
> >>        public function __destruct() { echo __METHOD__; }
> >>       }
> >>
> >>       $bar = new Bar();
> >>       $bar->foo = new Foo();
> >>
> >>       ReflectionLazyObject::makeLazyProxy($bar, function (Bar $bar) {
> >>         $result = new Bar();
> >>         $result->foo = null;
> >>         $result->s = 'init';
> >>         return $result;
> >>       });
> >>
> >>       var_dump($bar->s);
> >>
> >> My understanding is that this will dump `string(4) "init"`. Will the
> >> destructor of Foo be called? Will the destructor of Bar be called?
> >
> > This will print:
> >
> >      Foo::__destruct (during makeLazyProxy())
> >      string(4) "init" (during var_dump())
> >
> > and eventually
> >
> >      Bar::__destruct (when $bar is released)
>
> Okay, so only one Bar::__destruct(), despite two Bar objects being
> created. I assume it's the destructor of the second Bar, i.e. if I would
> dump `$this->foo` within the destructor, it would dump `null`?

Following your feedback we have updated the RFC to include calling
destructors in makeLazy by default. In the updated version,
Bar::__destruct is called twice: Once on the first instance during the
call to makeLazyProxy(), and another time on the second instance (the
one created in the closure) when it's released. It is not called when
the first instance is released, because it's now a proxy.

> >> Will the "revert to its pre-initialization"
> >> work properly when you have nested lazy objects? An example would
> >> probably help.
> >
> > Only the effects on the object itself are reverted. External side
> > effects are not reverted.
>
> Yes, it's obvious that external side effects are not reverted. I was
> thinking about a situation like:
>
>      $a = new A();
>      $b = new B();
>      ReflectionLazyObject::makeLazyGhost($b, function ($b) {
>          throw new \Exception('xxx');
>      });
>      ReflectionLazyObject::makeLazyGhost($a, function ($a) use ($b) {
>          $a->b = $b->somevalue;
>      });
>      $a->init = 'please';
>
> The initialization of $a will implicitly attempt to initialize $b, which
> will fail. Am I correct in my understanding that both $a and $b will be
> reverted back to a lazy object afterwards? If so, adding that example to
> the RFC would help to make possible edge cases clear.

We have added an example showing that.

> >> - 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 = new B();
> >>       ReflectionLazyObject::makeLazyProxy($o, function (B $o) {
> >>         return new A();
> >>       });
> >>
> >>       $o->foo(); // works
> >>       $o->s = 'init';
> >>       $o->foo(); // breaks
> >
> > $o->foo() calls B::foo() in both cases here, as $o is always 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 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.

Best Regards,
Arnaud

Reply via email to