Re: [RFC] Lazy Objects

From: Date: Sat, 15 Jun 2024 17:09:50 +0000
Subject: Re: [RFC] Lazy Objects
References: 1 2 3 4 5 6 7  Groups: php.internals 
Request: Send a blank email to [email protected] to get a copy of this message
On Sat, Jun 15, 2024, at 8:28 AM, Arnaud Le Blanc wrote:
> Hi Larry,
>

>> Under Common Behavior, you have an example of calling the constructor directly, using the
>> reflection API, but not of binding the callable, which the text says is also available.  Please
>> include an example of that so we can evaluate how clumsy (or not) it would be.
>
> I've clarified that binding can be achieved with Closure::bind(). 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?

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

>> 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:
>>
>> class Service {
>>     public function __construct(private ServiceA $a, private ServiceB $b) {}
>> }
>>
>> $c = some_container();
>>
>> $init = fn() => $this->__construct($c->get(ServiceA::class),
>> $c->get(ServiceB::class));
>>
>> $service = new ReflectionLazyObjectFactory(Service::class, $init);
>>
>> (Most likely in generated code that can dynamically sort out the container calls to
>> inline.)
>>
>> Am I missing something?
>
> No you are right, but they must fallback to the proxy strategy when
> the user provides a factory.
>
> E.g. this will use the ghost strategy because the DIC
> instantiates/initializes the service itself:
>
> my_service:
>     class: MyClass
>     arguments: [@service_a, @service_b]
>     lazy: true
>
> But this will use the proxy strategy because the DIC doesn't
> instantiate/initialize the service itself:
>
> my_service:
>     class: MyClass
>     arguments: [@service_a, @service_b]
>     factory: [@my_service_factory, createService]
>     lazy: true
>
> 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, and I'm unclear what it would do with the proxy
object itself that's passed in.

>> 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. :-)

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

--Larry Garfield


Thread (95 messages)

« previous php.internals (#123619) next »