Hi Larry,

Following your feedback we propose to amend the API as follows:

```
class ReflectionClass
{
    public function newLazyProxy(callable $factory, int $options): object {}

    public function newLazyGhost(callable $initializer, int $options): object {}

    public function resetAsLazyProxy(object $object, callable
$factory, int $options): void {}

    public function resetAsLazyGhost(object $object, callable
$initializer, int $options): void {}

    public function initialize(object $object): object {}

    public function isInitialized(object $object): bool {}

    // existing methods
}

class ReflectionProperty
{
    public function setRawValueWithoutInitialization(object $object,
mixed $value): void {}

    public function skipInitialization(object $object): void {}

    // existing methods
}
```

Comments / rationale:
- Adding methods on ReflectionClass instead of ReflectionObject is
better from a performance point of view, as mentioned earlier
- Keeping the word "Lazy" in method names is clearer, especially for
"newLazyProxy" as a the "Proxy" pattern has many uses-cases that are
not related to laziness. However we removed the word "Instance" to
make the names shorter.
- We have renamed "make" methods to "reset", following your feedback
about the word "make". It should better convey the behavior of these
methods, and clarify that it's modifying the object in-place as well
as resetting its state
- setRawValueWithoutInitialization() has the same behavior as
setRawValue() (from the hooks RFC), except it doesn't trigger
initialization
- Renamed $initializer to $factory for proxy methods

WDYT?

Best Regards,
Arnaud

On Sun, Jun 16, 2024 at 3:46 PM Arnaud Le Blanc <arnaud...@gmail.com> wrote:
>
> On Sat, Jun 15, 2024 at 7:13 PM Larry Garfield <la...@garfieldtech.com> wrote:
> > > In practice I expect there will be two kinds of ghost initializers:
> > > - Those that just call one public method of the object, such as the 
> > > constructor
> > > - Those that initialize everything with ReflectionProperty::setValue()
> > > as in the Doctrine example in the "About Lazy-Loading strategies"
> > > section
> > I'm still missing an example with ::bind().  Actually, I tried to write a 
> > version of what I think the intent is and couldn't figure out how. :-)
> >
> > $init = function() use ($c) {
> >   $this->a = $c->get(ServiceA::class);
> >   $this->b = $c->get(ServiceB::class);
> > }
> >
> > $service = new ReflectionLazyObjectFactory(Service::class, $init);
> >
> > // We need to bind $init to $service now, but we can't because $init is 
> > already registered as the initializer for $service, and binding creates a 
> > new closure object, not modifying the existing one.  So, how does this even 
> > work?
>
> Oh I see. Yes you will not be able to bind $this in a simple way here,
> but you could bind the scope. This modified example will work:
>
> ```
> $init = function($object) use ($c) {
>   $object->a = $c->get(ServiceA::class);
>   $object->b = $c->get(ServiceB::class);
> }
> $service = new ReflectionLazyObjectFactory(Service::class,
> $init->bindTo(null, Service::class));
> ```
>
> If you really want to bind $this you could achieve it in a more convoluted 
> way:
>
> ```
> $init = function($object) use ($c) {
>   (function () use ($c) {
>     $this->a = $c->get(ServiceA::class);
>     $this->b = $c->get(ServiceB::class);
>   })->bindTo($object)();
> }
> $service = new ReflectionLazyObjectFactory(Service::class, $init);
> ```
>
> This is inconvenient, but the need or use-case is not clear to me.
> Could you describe some use-cases where you would hand-write
> initializers like this? Do you feel that the proposal should provide
> an easier way to change $this and/or the scope?
>
> > > In practice we expect that makeInstanceLazy*() methods will not be
> > > used on fully initialized objects, and that the flag will be set most
> > > of the time, but as it is the API is safe by default.
> >
> > In the case an object does not have a destructor, it won't make a 
> > difference either way, correct?
>
> Yes
>
> > >> I find it interesting that your examples list DICs as a use case for 
> > >> proxies, when I would have expected that to fit ghosts better.  The 
> > >> common pattern, I would think, would be:
> > > The RFC didn't make it clear enough that the example was about the
> > > factory case specifically.
> >
> > Ah, got it.  That makes more sense.
> >
> > Which makes me ask if the $initializer of a proxy should actually be called 
> > $factory?  Since that's basically what it's doing,
>
> Good point, $factory would be a good name for this parameter.
>
> > and I'm unclear what it would do with the proxy object itself that's passed 
> > in.
>
> Passing the factory itself as argument could be used to make decisions
> based on the value of some initialized field, or on the class of the
> object, or on its identity. I think Nicolas had a real use-case where
> he detects clones based on the identity of the object:
>
> ```
> $init = function ($object) use (&$originalObject) {
>     if ($object !== $originalObject) {
>         // we are initializing a clone
>     }
> };
> $originalObject = $reflector->newProxyInstance($init);
> ```
>
> This was on ghosts, but I think it's also a valid use-case example for 
> proxies.
>
> > >> ReflectionLazyObjectFactory is a terrible name.  Sorry, it is. :-)  
> > >> Especially if it's subclassing ReflectionClass.  If it were its own 
> > >> thing, maybe, but it's still too verbose.  I know you don't want to put 
> > >> more on the "dumping ground" fo ReflectionClass, but honestly, that 
> > >> feels more ergonomic to me.  That way the following are all siblings:
> > >>
> > >> newInstance(...$args)
> > >> newInstanceWithoutConstructor(...$args)
> > >> newGhostInstance($init)
> > >> newProxyInstance($init)
> > >>
> > >> That feels a lot more sensible and ergonomic to me.  isInitialized(), 
> > >> initialized(), etc. also feel like they make more sense as methods on 
> > >> ReflectionObject, not as static methods on a random new class.
> > >
> > > Thank you for the suggestion. We will check if this fits the
> > > use-cases. Moving some methods on ReflectionObject may have negative
> > > performance implications as it requires creating a dedicated instance
> > > for each object. Some use-cases rely on caching the reflectors for
> > > performance.
> > >
> > > Best Regards,
> > > Arnaud
> >
> > I'm not clear why there's a performance difference, but I haven't looked at 
> > the reflection implementation in, well, ever. :-)
>
> What I meant is that creating an instance (not necessarily of
> ReflectionObject, but of any class) is more expensive than just doing
> nothing. The first two loops below would be fine, but the last one
> would be slower. This can make an important difference in a hot path.
>
> ```
> foreach ($objects as $object) {
>     ReflectionLazyObject::isInitialized($object);
> }
>
> $reflector = new ReflectionClass(SomeClass::class);
> foreach ($objects as $object) {
>     $reflector->isInitialized($object);
> }
>
> foreach ($objects as $object) {
>     $reflector = new ReflectionObject($object);
>     $reflector->isInitialized($object);
> }
> ```
>
> > If it has to be a separate object, please don't make it extend 
> > ReflectionClass but still give it useful dynamic methods rather than static 
> > methods.  Or perhaps even do something like
> >
> > $ghost = new ReflectionGhostInstance(SomeClass::class, $init);
> > $proxy  = new ReflectionProxyINstance(SOmeClass::class, $init);
> >
> > And be done with it.  (I'm just spitballing here.  As I said, I like the 
> > feature, I just want to ensure the ergonomics are as good as possible.)
>
> Thank you for your help. We will think about a better API.

Reply via email to