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 <[email protected]> wrote:
>
> On Sat, Jun 15, 2024 at 7:13 PM Larry Garfield <[email protected]> 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.