# [RFC] Lazy Objects

- Thread: https://externals.io/message/123503
- Original archive: https://news-web.php.net/php.internals/123503
- Started by Nicolas Grekas on 2024-06-04T12:28:53+00:00
- Messages: 95
- RFC: Lazy objects (Implemented (PHP 8.4)) — https://externals.io/rfc/lazy-objects.md
- Page 1 of 4: messages 1 to 25 (25 per page, add `?page=N` to the URL)
- Next page: https://externals.io/message/123503.md?page=2
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #123503 — Nicolas Grekas — 2024-06-04T12:28:53+00:00

URL: https://externals.io/message/123503#123503

Dear all,

Arnaud and I are pleased to share with you the RFC we've been shaping for
over a year to add native support for lazy objects to PHP.

Please find all the details here:
https://wiki.php.net/rfc/lazy-objects

We look forward to your thoughts and feedback.

Cheers,
Nicolas and Arnaud

---

## #123505 — Valentin Udaltsov — 2024-06-04T14:55:25+00:00

URL: https://externals.io/message/123503#123505 · In reply to: #123503

> Dear all,
>
> Arnaud and I are pleased to share with you the RFC we've been shaping for
> over a year to add native support for lazy objects to PHP.
>
> Please find all the details here:
> https://wiki.php.net/rfc/lazy-objects
>
> We look forward to your thoughts and feedback.
>
> Cheers,
> Nicolas and Arnaud
>
Hi, Nicolas and Arnaud!

I like the idea, thank you for the RFC!

Here are some initial thoughts and questions:

1. It doesn't seem right that calling `ReflectionLazyObject::makeLazyGhost`
has an implicit side effect on `$instance` and returns reflection. It does
2 things and thus breaks the SRP. Having smth like `$lazyGhost = new
ReflectionClass(MyClass)->newLazyGhost($initializer)` and/or
`ReflectionLazyObject::makeLazy($object, $initializer): void` seems better.
2. If `ReflectionLazyObject extends ReflectionObject`, then how `new
ReflectionLazyObject($object)` will work for non-lazy objects? Will it
throw?
3. Is extending `ReflectionObject` really necessary? What about creating
`ReflectionLazyObject` as a standalone class without abusing inheritance?
Or simply adding methods to `ReflectionObject` / `ReflectionClass`?
4. The RFC says that Virtual state-proxies are necessary because of
circular references. It's difficult to accept this reasoning, because using
circular references is a bad practice and the given example is something I
try to avoid by all means in my code.

-- 
Best regards,
Valentin

---

## #123507 — Nicolas Grekas — 2024-06-04T16:56:43+00:00

URL: https://externals.io/message/123503#123507 · In reply to: #123505

Hello Valentin,

Thanks for having a look.

> Arnaud and I are pleased to share with you the RFC we've been shaping for
> > […]
> Hi, Nicolas and Arnaud!
>
> I like the idea, thank you for the RFC!
>
> Here are some initial thoughts and questions:
>
> 1. It doesn't seem right that calling
> `ReflectionLazyObject::makeLazyGhost` has an implicit side effect on
> `$instance` and returns reflection. It does 2 things and thus breaks the
> SRP. Having smth like `$lazyGhost = new
> ReflectionClass(MyClass)->newLazyGhost($initializer)` and/or
> `ReflectionLazyObject::makeLazy($object, $initializer): void` seems better.
>

About "new ReflectionClass(MyClass)->newLazyGhost", we could add this but
it would provide a duplicate way to achieve what can be done with
"makeLazy($object)" variants. As you might have read in the RFC, being able
to make a pre-existing instance lazy is needed to cover all use cases.

Then about "ReflectionLazyObject::makeLazy($object, $initializer): void",
is the difference only to return void and not a ReflectionLazyObject
instance? Then this might provide an underperforming API: creating a lazy
object and immediately after setting some of its properties is an use case
that can happen on the hot path (of e.g. Doctrine entities creation steps).
Returning "void" would force an extra call to
ReflectionLazyObject::fromInstance() that the proposed API prevents.


> 2. If `ReflectionLazyObject extends ReflectionObject`, then how `new
> ReflectionLazyObject($object)` will work for non-lazy objects? Will it
> throw?
>

The constructor is private so this is not allowed (I should add this to the
RFC).


> 3. Is extending `ReflectionObject` really necessary? What about creating
> `ReflectionLazyObject` as a standalone class without abusing inheritance?
> Or simply adding methods to `ReflectionObject` / `ReflectionClass`?
>

I don't think extending ReflectionObject is necessary. I don't know if
doing so is "abusing" inheritance. It might make sense either way. For the
use cases I identified, it wouldn't harm to not extend anything. Does
anyone else see a reason to go in one or the other direction? To me it just
makes sense to have ReflectionLazyObject extend ReflectionObject.

About adding methods to `ReflectionObject` / `ReflectionClass`, you mention
SRP in your message; ReflectionClass/ReflectionObject is already crowded,
and this lazy object topic is better separated to me.

4. The RFC says that Virtual state-proxies are necessary because of
> circular references. It's difficult to accept this reasoning, because using
> circular references is a bad practice and the given example is something I
> try to avoid by all means in my code.
>

Yet circular references happen all the time in any non-trivial app, so this
has to be supported.

Nicolas

---

## #123511 — tim@bastelstu.be — 2024-06-04T19:16:30+00:00

URL: https://externals.io/message/123503#123511 · In reply to: #123503

Hi


> Please find all the details here:
> https://wiki.php.net/rfc/lazy-objects
> 
> We look forward to your thoughts and feedback.

I've gave the RFC three or four passes and I'm not quite sure if I 
follow everything, here's a list of some questions / remarks that came 
to mind, roughly ordered by the order of things appearing in the RFC.

- "been tested successfully on the Doctrine and on the Symfony projects"

Is there a PoC patch showcasing how the code would change / be 
simplified for those pre-existing codebases?

- int $options = 0

Not a fan of flag parameters that take a bitset, those provide for a 
terrible DX due to magic numbers. Perhaps make this a regular (named) 
parameter, or an list of enum LazyObjectOptions { case 
SkipInitOnSerialize; }?

- skipProperty()

Not a fan of the method name, because it doesn't really say what it 
does, without consulting the docs. Perhaps `skipInitializationFor()` or 
similar?

- setProperty()

Not a fan of the method name, because it is not a direct counterpart to 
`getProperty()`. Unfortunately I don't have a better suggestion.

- The examples should be expanded and clarified, especially the one for 
makeLazyProxy():

My understanding is that the $object that is passed to the first 
parameter of makeLazyProxy() is completely replaced. Is this 
understanding correct? What does that mean for `spl_object_hash()`, 
 `spl_object_id()`? What does this mean for WeakMap and WeakReference? What 
does this mean for objects that are only referenced from within $object?

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?

- What happens if I make an object lazy that already has all properties 
initialized? Will that be a noop? Will that throw? Will that create a 
lazy object that will never automatically be initialized?

- Cloning, unless __clone() is implemented and accesses a property.

The semantics of cloning a lazy object should be explicitly spelled out 
in the RFC, ideally with an example of the various edge cases (should 
any exist).

- Before calling the initializer, properties that were not initialized 
with ReflectionLazyObject::skipProperty(), 
ReflectionLazyObject::setProperty(), 
ReflectionLazyObject::setRawProperty() are initialized to their default 
value.

Should skipProperty() also skip the initialization to the default value? 
My understanding is that it allows skipping the initialization on 
access, but when initialization actually happens it should probably be 
set to a well-defined value, no?

Am I also correct in my understanding that this should read "initialized 
to their default value (if any)", meaning that properties without a 
default value are left uninitialized?

- If an exception is thrown while calling the initializer, the object is 
reverted to its pre-initialization state and is still considered lazy.

Does this mean that the initializer will be called once again when 
accessing another property? Will the "revert to its pre-initialization" 
work properly when you have nested lazy objects? An example would 
probably help.

- The initializer is called with the object as first parameter.

What is the behavior of accessing the object properties, while the 
initializer is active? Based on the examples, I assume it will not be 
recursively called, similarly to how the hooks work?

- The object is marked as non-lazy and the initializer is released.

What does it mean for the initializer to be released? Consider the 
following example:

     ReflectionLazyObject::makeLazyGhost($o, $init = function ($o) use 
(&$init) {
       $o->init = $init;
     });

- 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

- The destructor of lazy non-initialized objects is not called.

That sounds unsafe. Consider the following example:

     class Mutex {
       public string $s;
       public function __construct() {
         // take lock
       }

       public function __destruct() {
         // release lock
       }
     }

     $m = new Mutex();
     ReflectionLazyObject::makeLazyGhost($m, function ($m) {
     });
     unset($m); // will not release the lock.

- Using the Manager::createManager() factory is not compatible with 
ghost objects because their initializer requires initializing the ghost 
object in place,

I don't understand that example, because it doesn't actually use lazy 
objects and thus I don't understand if Manager, Dispatcher, or both are 
intended to be lazily initialized. It would help to rewrite the example 
to use `makeLazyGhost()` and indicate with a comment in which cases the 
problem would arise.

- Backward Incompatible Changes

There a technicality: The `ReflectionLazyObject` class name will no 
longer be available to userland.

Best regards
Tim Düsterhus

---

## #123515 — Robert Landers — 2024-06-05T12:33:49+00:00

URL: https://externals.io/message/123503#123515 · In reply to: #123511

>
> Hi
>
> 
> > […]
>
> I've gave the RFC three or four passes and I'm not quite sure if I
> follow everything, here's a list of some questions / remarks that came
> to mind, roughly ordered by the order of things appearing in the RFC.
>
> - "been tested successfully on the Doctrine and on the Symfony projects"
>
> Is there a PoC patch showcasing how the code would change / be
> simplified for those pre-existing codebases?
>
> - int $options = 0
>
> Not a fan of flag parameters that take a bitset, those provide for a
> terrible DX due to magic numbers. Perhaps make this a regular (named)
> parameter, or an list of enum LazyObjectOptions { case
> SkipInitOnSerialize; }?
>
> - skipProperty()
>
> Not a fan of the method name, because it doesn't really say what it
> does, without consulting the docs. Perhaps `skipInitializationFor()` or
> similar?
>
> - setProperty()
>
> Not a fan of the method name, because it is not a direct counterpart to
> `getProperty()`. Unfortunately I don't have a better suggestion.
>
> - The examples should be expanded and clarified, especially the one for
> makeLazyProxy():
>
> My understanding is that the $object that is passed to the first
> parameter of makeLazyProxy() is completely replaced. Is this
> understanding correct? What does that mean for `spl_object_hash()`,
> `spl_object_id()`? What does this mean for WeakMap and WeakReference? What
> does this mean for objects that are only referenced from within $object?
>
> 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?
>
> - What happens if I make an object lazy that already has all properties
> initialized? Will that be a noop? Will that throw? Will that create a
> lazy object that will never automatically be initialized?
>
> - Cloning, unless __clone() is implemented and accesses a property.
>
> The semantics of cloning a lazy object should be explicitly spelled out
> in the RFC, ideally with an example of the various edge cases (should
> any exist).
>
> - Before calling the initializer, properties that were not initialized
> with ReflectionLazyObject::skipProperty(),
> ReflectionLazyObject::setProperty(),
> ReflectionLazyObject::setRawProperty() are initialized to their default
> value.
>
> Should skipProperty() also skip the initialization to the default value?
> My understanding is that it allows skipping the initialization on
> access, but when initialization actually happens it should probably be
> set to a well-defined value, no?
>
> Am I also correct in my understanding that this should read "initialized
> to their default value (if any)", meaning that properties without a
> default value are left uninitialized?
>
> - If an exception is thrown while calling the initializer, the object is
> reverted to its pre-initialization state and is still considered lazy.
>
> Does this mean that the initializer will be called once again when
> accessing another property? Will the "revert to its pre-initialization"
> work properly when you have nested lazy objects? An example would
> probably help.
>
> - The initializer is called with the object as first parameter.
>
> What is the behavior of accessing the object properties, while the
> initializer is active? Based on the examples, I assume it will not be
> recursively called, similarly to how the hooks work?
>
> - The object is marked as non-lazy and the initializer is released.
>
> What does it mean for the initializer to be released? Consider the
> following example:
>
>      ReflectionLazyObject::makeLazyGhost($o, $init = function ($o) use
> (&$init) {
>        $o->init = $init;
>      });
>
> - 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
>
> - The destructor of lazy non-initialized objects is not called.
>
> That sounds unsafe. Consider the following example:
>
>      class Mutex {
>        public string $s;
>        public function __construct() {
>          // take lock
>        }
>
>        public function __destruct() {
>          // release lock
>        }
>      }
>
>      $m = new Mutex();
>      ReflectionLazyObject::makeLazyGhost($m, function ($m) {
>      });
>      unset($m); // will not release the lock.
>
> - Using the Manager::createManager() factory is not compatible with
> ghost objects because their initializer requires initializing the ghost
> object in place,
>
> I don't understand that example, because it doesn't actually use lazy
> objects and thus I don't understand if Manager, Dispatcher, or both are
> intended to be lazily initialized. It would help to rewrite the example
> to use `makeLazyGhost()` and indicate with a comment in which cases the
> problem would arise.
>
> - Backward Incompatible Changes
>
> There a technicality: The `ReflectionLazyObject` class name will no
> longer be available to userland.
>
> Best regards
> Tim Düsterhus

As someone who has had to maintain these proxies/ghosts before, this
looks quite useful and powerful. I feel it has rather wonky syntax,
but it is clearly better than the alternative of implementing it
yourself. I'm also a huge fan that the syntax allows for
usage/creation far and away from the definition/class itself.

Good luck, and I hope it passes.

Robert Landers
Software Engineer
Utrecht NL

---

## #123517 — Arnaud Le Blanc — 2024-06-05T14:50:21+00:00

URL: https://externals.io/message/123503#123517 · In reply to: #123511

Hi Tim,

That's a lot of interesting feedback. I will try to answer some of
your points, and Nicolas will follow with other points.


> - int $options = 0
>
> Not a fan of flag parameters that take a bitset, those provide for a
> terrible DX due to magic numbers. Perhaps make this a regular (named)
> parameter, or an list of enum LazyObjectOptions { case
> SkipInitOnSerialize; }?

The primary reason for choosing to represent $options as a bitset is
that it's consistent with the rest of the Reflection API (e.g.
ReflectionClass::getProperties() uses a bitset for the $filter
parameter).

I don't get your point about magic numbers since we are using
constants to abstract them.

> - skipProperty()
>
> Not a fan of the method name, because it doesn't really say what it
> does, without consulting the docs. Perhaps `skipInitializationFor()` or
> similar?

We have opted for skipInitializerForProperty()

> - setProperty()
>
> Not a fan of the method name, because it is not a direct counterpart to
> `getProperty()`. Unfortunately I don't have a better suggestion.

We have renamed setRawProperty() to setRawPropertyValue() in the RFC.
We are open to other suggestions.

We have also removed setProperty(), as we believe that there is no
use-case for it.

> - The examples should be expanded and clarified, especially the one for
> makeLazyProxy():

Agreed. We will add examples and clarify some behaviors.

> My understanding is that the $object that is passed to the first
> parameter of makeLazyProxy() is completely replaced. Is this
> understanding correct? What does that mean for `spl_object_hash()`,
> `spl_object_id()`? What does this mean for WeakMap and WeakReference? What
> does this mean for objects that are only referenced from within $object?

The object is updated in-place, and retains its identity. It is not
replaced. What makeLazyGhost() and makeLazyProxy() do is equivalent to
calling `unset()` on all properties, and setting a flag on the object
internally. Apart from setting the internal flag, this is achievable
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*().

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.

The proposed patch integrates into the object handlers fallback code
path used to manage accesses to undefined properties. We implement
lazy initialization by hooking into undefined property accesses,
without impacting the fast path.

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

> - What happens if I make an object lazy that already has all properties
> initialized? Will that be a noop? Will that throw? Will that create a
> lazy object that will never automatically be initialized?

All properties are unset as described earlier, and the object is
flagged as lazy. The object will automatically initialize when trying
to observe its properties.

However, making a fully initialized object lazy is not the intended use-case.

> - Cloning, unless __clone() is implemented and accesses a property.
>
> The semantics of cloning a lazy object should be explicitly spelled out
> in the RFC, ideally with an example of the various edge cases (should
> any exist).

Agreed. We are working on expanding the RFC about this

> - Before calling the initializer, properties that were not initialized
> with ReflectionLazyObject::skipProperty(),
> ReflectionLazyObject::setProperty(),
> ReflectionLazyObject::setRawProperty() are initialized to their default
> value.
>
> Should skipProperty() also skip the initialization to the default value?
> My understanding is that it allows skipping the initialization on
> access, but when initialization actually happens it should probably be
> set to a well-defined value, no?
>
> Am I also correct in my understanding that this should read "initialized
> to their default value (if any)", meaning that properties without a
> default value are left uninitialized?

The primary effect of skipProperty() is to mark a property as
non-lazy, so that accessing it does not trigger the initialization of
the entire object. It also sets the property to its default value if
any, otherwise it is left as undef.

Accessing this property afterwards has exactly the same effect as
doing so on an object created with
ReflectionClass::newInstanceWithoutConstructor() (including triggering
errors when reading an uninitialized property).

> - If an exception is thrown while calling the initializer, the object is
> reverted to its pre-initialization state and is still considered lazy.
>
> Does this mean that the initializer will be called once again when
> accessing another property?

Yes. The goal is to prevent transition from lazy to initialized when
an unexpected error occurred in the initializer.

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

> - The initializer is called with the object as first parameter.
>
> What is the behavior of accessing the object properties, while the
> initializer is active? Based on the examples, I assume it will not be
> recursively called, similarly to how the hooks work?

For ghost objects, the initializer is supposed to initialize the
object itself like a constructor would do. During initializer
execution, the object has exactly the same state and behavior as it
would have in its constructor during `new`:

- The object is not lazy.
- Properties have their default value (if any) and are accessed
without triggering a nested initialization.
- If setRawPropertyValue() was used, some properties may have a value
different from their default.

For virtual proxies, the initializer is supposed to return another
object. Accessing the proxy object during initialization does not
trigger recursive initializations. Some properties will have a value
if setRawPropertyValue() or skipInitializationForProperty().

> - The object is marked as non-lazy and the initializer is released.
>
> What does it mean for the initializer to be released? Consider the
> following example:
>
>      ReflectionLazyObject::makeLazyGhost($o, $init = function ($o) use
> (&$init) {
>        $o->init = $init;
>      });

"released" here means that the initializer is not referenced anymore
by this object, and may be freed if it is not referenced anywhere
else.

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

> - The destructor of lazy non-initialized objects is not called.
>
> That sounds unsafe. Consider the following example:
>
>      class Mutex {
>        public string $s;
>        public function __construct() {
>          // take lock
>        }
>
>        public function __destruct() {
>          // release lock
>        }
>      }
>
>      $m = new Mutex();
>      ReflectionLazyObject::makeLazyGhost($m, function ($m) {
>      });
>      unset($m); // will not release the lock.

Good point. The Mutex constructor is called during "new Mutex()", but
the object is made lazy after that, and the destructor is never
called.

We have made the following changes to the RFC:

- makeLazyGhost / makeLazyProxy will call the object destructor
- A new option flag is added, `ReflectionLazyObject::SKIP_DESTRUCTOR`,
that disables this behavior

This is not ideal since the intended use of these methods is to call
them on objects created with newInstanceWithoutConstructor(), or
directly in a constructor, and both of these will need this flag, but
at least it's safe by default.

Thanks again for the feedback.

Best Regards,
Arnaud

---

## #123518 — Nicolas Grekas — 2024-06-05T15:25:21+00:00

URL: https://externals.io/message/123503#123518 · In reply to: #123511

Hi Tim,

Thanks for the detailed feedback. Arnaud already answered most of your
questions, here is the remaining one:


> > […]
>
> I've gave the RFC three or four passes and I'm not quite sure if I
> follow everything, here's a list of some questions / remarks that came
> to mind, roughly ordered by the order of things appearing in the RFC.
>
> - "been tested successfully on the Doctrine and on the Symfony projects"
>
> Is there a PoC patch showcasing how the code would change / be
> simplified for those pre-existing codebases?
>

Yes!
See https://github.com/nicolas-grekas/symfony/pull/44 for Symfony. All the
complex code is gone \o/
And if anyone is wondering: No, we're not moving this complexity into the
engine. As Arnaud wrote somewhere: Implementation in core is simple
compared to userland as we are at the right level of abstraction. No code
gen, no edge cases with relative type hints, visibility, readonly, hooks,
etc. We get more consistent and transparent behavior as well compared to
userland impl.

For Doctrine, the URL is
https://github.com/nicolas-grekas/doctrine-orm/pull/6 for now, with the
most important line being the removal of the symfony/var-exporter
dependency.

After yours and Valentin's feedback, we're considering an updated API that
would provide the same capabilities but that might be more consensual.

The RFC isn't updated but below is what we have in our drafts. Let me know
what you think already if you want (otherwise, let us work on the updated
implementation/RFC and we'll let you know about them ASAP).

Nicolas
PS: I understand that the concepts in the RFC might be difficult to grasp.
They were certainly challenging for me to summarize. I would happily accept
any help to improve the wording if anyone is willing.


class ReflectionLazyClass extends ReflectionClass
{
public int const SKIP_INITIALIZATION_ON_SERIALIZE = 1;
public int const SKIP_DESTRUCTOR = 2;

public function __construct(object|string $objectOrClass);

public function newLazyGhostInstance(callable $initializer, int $options = 0
): object;
public function newLazyProxyInstance(callable $initializer, int $options = 0
): object;

public function makeInstanceLazyGhost(object $object, callable $initializer,
int $options = 0): void;
public function makeInstanceLazyProxy(object $object, callable $initializer,
int $options = 0): void;
public static function isInitialized(object $instance): bool;
/**
* Initializes a lazy object (no-op if the object is already initialized.)
*
* The backing object is returned, which can be another instance than the
lazy object when the virtual strategy is used.
*/
public function initialize(object $object, bool $skipInitializer = false):
object;
/**
* Marks a property as *not* triggering initialization when being accessed.
*
* This method is useful to bypass initialization when setting a property.
*/
public function skipInitializerForProperty(object $object, string $name,
string $class = null): void;
/**
* Sets a property *without* triggering initialization while skipping hooks
if any.
*/
public function setRawPropertyValue(object $object, string $name, mixed $
value, string $class = null): void;
}

---

## #123519 — Larry Garfield — 2024-06-05T15:31:15+00:00

URL: https://externals.io/message/123503#123519 · In reply to: #123517

> Good point. The Mutex constructor is called during "new Mutex()", but
> the object is made lazy after that, and the destructor is never
> called.
>
> We have made the following changes to the RFC:
>
> - makeLazyGhost / makeLazyProxy will call the object destructor
> - A new option flag is added, `ReflectionLazyObject::SKIP_DESTRUCTOR`,
> that disables this behavior
>
> This is not ideal since the intended use of these methods is to call
> them on objects created with newInstanceWithoutConstructor(), or
> directly in a constructor, and both of these will need this flag, but
> at least it's safe by default.
>
> Thanks again for the feedback.
>
> Best Regards,
> Arnaud

Let me make sure I am following, since I'm still having a hard time with the explanation in the RFC (as I discussed with Nicolas off list).

The two use cases intended here are, essentially, "delay invoking the constructor until first use" (ghost) and "sneak a factory object in the way that is called on first use" (virtual).  Right?

The ghost initializer callback is basically an alternate constructor (which will probably just call the real constructor in the typical case), and the virtual initializer callback is basically the body of the factory object.

So the most common ghost implementation for a service would be something like:

$c = the_container();

$object = new ReflectionClass(Foo::class)->newInstanceWithoutConstructor();

$initializer = static function(Foo $foo) => $foo->__construct($c->dep1, $c->dep2, $c->dep3);

$lazyReflector = ReflectionLazyObject::makeLazyGhost($object, $initializer, ReflectionLazyObject::SKIP_DESTRUCTOR);

Am I following?  Because if so, the proposed API is extremely clunky.  For one thing, using an input/output parameter ($object) is a code smell 99% of the time.  It's changing an un-constructed object into a ghost object, and returning... um, I'm not sure what.  It also means I need to use both reflection classes in different ways to achieve the result.

It seems a much cleaner API would be something like:

$object = new ReflectionClass(Foo::class)->newInstanceWithLazyConstructor($initializer);

In which case it becomes a lot more obvious that we are, essentially, "swapping out" the constructor for a lazy one.  It also suggests that perhaps the function should be using $this, not $foo, as it's running within the context of the object (I presume?  Can it call private methods?  I assume so since it can set private properties.)

That would also suggest this API for the other approach:

$initializer = static function() => new Foo($c->dep1, $c->dep2, $c->dep3);

$object = new ReflectionClass(Foo::class)->newInstanceWithLazyFactory($initializer);

In which case $object is the proxy, and gets "swapped out" for the return value of the $initializer on first use.

Am I understanding all this correctly?  Because if so, I think the above simplified API would make it much more obvious what is going on, much easier to work with, easier to document/explain, and simple enough that it could conceivably be used in cases outside of DI or ORMs, too.

If I'm way off and don't understand what you're doing, then please explain as I'm clearly very confused. :-)

--Larry Garfield

---

## #123522 — Robert Landers — 2024-06-05T17:41:20+00:00

URL: https://externals.io/message/123503#123522 · In reply to: #123518

On Wed, Jun 5, 2024 at 6:58 PM Nicolas Grekas
<nicolas.grekas+php@gmail.com> wrote:
>
> Hi Tim,
>
> Thanks for the detailed feedback. Arnaud already answered most of your questions, here is the remaining one:
>
> > […]
>
>
> Yes!
> See https://github.com/nicolas-grekas/symfony/pull/44 for Symfony. All the complex code is gone \o/
> And if anyone is wondering: No, we're not moving this complexity into the engine. As Arnaud wrote somewhere: Implementation in core is simple compared to userland as we are at the right level of abstraction. No code gen, no edge cases with relative type hints, visibility, readonly, hooks, etc. We get more consistent and transparent behavior as well compared to userland impl.
>
> For Doctrine, the URL is https://github.com/nicolas-grekas/doctrine-orm/pull/6 for now, with the most important line being the removal of the symfony/var-exporter dependency.
>
> After yours and Valentin's feedback, we're considering an updated API that would provide the same capabilities but that might be more consensual.
>
> The RFC isn't updated but below is what we have in our drafts. Let me know what you think already if you want (otherwise, let us work on the updated implementation/RFC and we'll let you know about them ASAP).
>
> Nicolas
> PS: I understand that the concepts in the RFC might be difficult to grasp. They were certainly challenging for me to summarize. I would happily accept any help to improve the wording if anyone is willing.
>
>
> class ReflectionLazyClass extends ReflectionClass
> {
> public int const SKIP_INITIALIZATION_ON_SERIALIZE = 1;
> public int const SKIP_DESTRUCTOR = 2;
>
> public function __construct(object|string $objectOrClass);
>
> public function newLazyGhostInstance(callable $initializer, int $options = 0): object;
> public function newLazyProxyInstance(callable $initializer, int $options = 0): object;
>
> public function makeInstanceLazyGhost(object $object, callable $initializer, int $options = 0): void;
> public function makeInstanceLazyProxy(object $object, callable $initializer, int $options = 0): void;
> public static function isInitialized(object $instance): bool;
> /**
> * Initializes a lazy object (no-op if the object is already initialized.)
> *
> * The backing object is returned, which can be another instance than the lazy object when the virtual strategy is used.
> */
> public function initialize(object $object, bool $skipInitializer = false): object;
> /**
> * Marks a property as *not* triggering initialization when being accessed.
> *
> * This method is useful to bypass initialization when setting a property.
> */
> public function skipInitializerForProperty(object $object, string $name, string $class = null): void;
> /**
> * Sets a property *without* triggering initialization while skipping hooks if any.
> */
> public function setRawPropertyValue(object $object, string $name, mixed $value, string $class = null): void;
> }

For virtual proxies, it would be nice if the instance returned didn't
have to be the same type or child type.

For example, in Durable PHP, I allow accessing remote entities via a
Spying Proxy:

$ctx->signal($id, fn(MyEntity $obj) => $obj->doSomething($x));

In this example, ->signal() generates/loads the proxy which implements
MyEntity with code that captures the value of $x to be forwarded to a
remote instance. It then calls the passed closure and captures the
method + arguments passed.

Most of the complexity is generating the objects themselves to
implement the methods. It would be much simpler to implement magic
methods that can intercept the method calls and not worry about
generating an entire class every time a dev wants to use this
functionality.

In my case, none of the complexity actually goes away since I still
need to generate concrete types of the correct type. That being said,
I don't exactly have millions of users so if I still need to manually
create this, I will. I just thought I'd toss out the idea of not
needing an object of the correct type, just one that can handle the
expected behavior.

Robert Landers
Software Engineer
Utrecht NL

---

## #123523 — Arnaud Le Blanc — 2024-06-05T18:06:42+00:00

URL: https://externals.io/message/123503#123523 · In reply to: #123519

Hi Larry,

Thank you for the feedback.

I think you got the two strategies right. However, there is a use-case
in which an object manages its own laziness by making itself lazy:

```
class C {
     public function __construct() {
        ReflectionLazyObject::makeLazyGhost($this, $this->init(...));
    }
}
```

This one can not be addressed by a newInstance*() method since the
object to be made lazy already exists.

The makeLazyGhost() / makeLazyProxy() methods are the minimal methods
necessary to address all use-cases, but the methods you are suggesting
are a better API most of the time, so we are adding approximately this
to the proposal [1]. We are keeping them in a separate class to not
pollute ReflectionClass.

> It also suggests that perhaps the function should be using $this, not $foo, as it's running within the context of the object (I presume?  Can it call private methods?  I assume so since it can set private properties.)

The function is not running in the context of the object. It can only
access private members via Reflection or if the closure was bound to
the right scope by the user. This should not be an issue when the
initializer just calls a public constructor.

> In which case $object is the proxy, and gets "swapped out" for the return value of the $initializer on first use.

Just to be sure: $object continues to be the proxy instance after the
initializer is called, but it forwards all property accesses to the
return value of the $initializer.

[1] https://news-web.php.net/php.internals/123518

Best Regards,
Arnaud

---

## #123524 — tim@bastelstu.be — 2024-06-05T18:25:17+00:00

URL: https://externals.io/message/123503#123524 · In reply to: #123517

Hi

Working through your reply in order of the email, without any 
backtracking, because the complexity of this topic makes it hard to keep 
the entire email in mind. This might mean that I am asking follow-up 
questions that you already answered further down. Please apologize if 
that is the case :-)

One general note: Please include the answers to my questions in the RFC 
text as appropriate for reference of other readers and so that all my 
questions are answered when re-reading the RFC without needing to refer 
back to your email.


> > […]
> 
> The primary reason for choosing to represent $options as a bitset is
> that it's consistent with the rest of the Reflection API (e.g.
> ReflectionClass::getProperties() uses a bitset for the $filter
> parameter).
> 
> I don't get your point about magic numbers since we are using
> constants to abstract them.

It's not a magic number in the classic sense, but when trying to observe 
it, e.g. by means of a debugger and the $options have been passed 
through some other functions that wrap the lazy object API, it will 
effectively be an opaque number that one will need to decode manually, 
whereas a list of enums is immediately clear.

> > […]
> 
> The object is updated in-place, and retains its identity. It is not
> replaced. What makeLazyGhost() and makeLazyProxy() do is equivalent to
> calling `unset()` on all properties, and setting a flag on the object
> internally. Apart from setting the internal flag, this is achievable

Oh. It was not clear at all to me that all existing properties will be 
unset. Did I miss it or is that not written down in the RFC?

Is there any reason to call the makeLazyX() methods on an object that 
was not just freshly created with ->newInstanceWithoutConstructor() 
then? Anything I do with the object before the call to makeLazyX() will 
effectively be reverted, no?

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

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

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

> > […]
> 
> 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`?

> > […]
> 
> All properties are unset as described earlier, and the object is
> flagged as lazy. The object will automatically initialize when trying
> to observe its properties.
> 
> However, making a fully initialized object lazy is not the intended use-case.

Understood. See above with my follow-up question then.

> > […]
> 
> The primary effect of skipProperty() is to mark a property as
> non-lazy, so that accessing it does not trigger the initialization of
> the entire object. It also sets the property to its default value if
> any, otherwise it is left as undef.
> 
> Accessing this property afterwards has exactly the same effect as
> doing so on an object created with
> ReflectionClass::newInstanceWithoutConstructor() (including triggering
> errors when reading an uninitialized property).

I'm rereading my own question and can't make sense of it any more. I 
probably forgot that skipProperty() is defined to set the default value 
in the PHPDoc when I got down to the bit that I quoted.

Please just insert the 'if any' after 'default value' for clarity.

> > […]
> 
> 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.

> > […]
> 
> $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.

Best regards
Tim Düsterhus

---

## #123525 — tim@bastelstu.be — 2024-06-05T18:35:36+00:00

URL: https://externals.io/message/123503#123525 · In reply to: #123523

Hi

Thank you Larry for your email. Your suggested API is basically what I 
also had in mind after Arnaud's clarification.


> I think you got the two strategies right. However, there is a use-case
> in which an object manages its own laziness by making itself lazy:

What is the corresponding real-world use case for that? When would I 
want to create an object that makes itself lazy?

FWIW: If I understand it right, this use case would be supported by 
Larry's proposed API by writing a named constructor:

     class C {
       private function __construct($foo, $bar) { }
       public static function lazyNew(...$args) {
         $r = new ReflectionClass(self::class);

         return $r->newInstanceWithLazyConstructor(fn ($o) => 
$o->__construct(...$args));
       }
     }

> > […]
> 
> The function is not running in the context of the object. It can only
> access private members via Reflection or if the closure was bound to
> the right scope by the user. This should not be an issue when the
> initializer just calls a public constructor.

Please clarify the interaction with visibility in the RFC.

Best regards
Tim Düsterhus

---

## #123526 — tim@bastelstu.be — 2024-06-05T18:48:55+00:00

URL: https://externals.io/message/123503#123526 · In reply to: #123518

Hi


> Yes!
> See https://github.com/nicolas-grekas/symfony/pull/44 for Symfony. All the
> complex code is gone \o/
> [...]
> For Doctrine, the URL is
> https://github.com/nicolas-grekas/doctrine-orm/pull/6 for now, with the
> most important line being the removal of the symfony/var-exporter
> dependency.

Thank you, I'll have a look at it when I have the time.

> The RFC isn't updated but below is what we have in our drafts. Let me know
> what you think already if you want (otherwise, let us work on the updated
> implementation/RFC and we'll let you know about them ASAP).

I've already mentioned in the sub-thread with Larry that this is also 
what I had in mind after Arnaud's clarification, so that sounds good to me.

I'll give the RFC another full read once you finished incorporating the 
existing feedback. Doesn't really make sense to give feedback on 
something that still is in-flux and that after all might not exist in 
the updated proposal.

> PS: I understand that the concepts in the RFC might be difficult to grasp.
> They were certainly challenging for me to summarize. I would happily accept
> any help to improve the wording if anyone is willing.

I've already said it in my email to Arnaud: Examples showcasing the 
usage of the API in a (simplified) real-world use case would definitely 
help making the RFC easier to understand.

And for me it is important that any interactions with the existing 
functionality [1] are explicitly spelled out. I'm happier spending the 
time reading an RFC that is five times as long, but clearly defines the 
entire behavior, than reading an RFC that leaves many open questions 
that will end up being "implementation-defined" rather than fully 
thought-through.

Best regards
Tim Düsterhus

[1] e.g. my WeakReference question, instanceof behavior, whether or not 
private properties are accessible in the initializer, readonly, etc.

---

## #123529 — michal.brzuchalski@gmail.com — 2024-06-06T06:53:04+00:00

URL: https://externals.io/message/123503#123529 · In reply to: #123523

Hi Arnaud,

śr., 5 cze 2024 o 20:08 Arnaud Le Blanc <arnaud.lb@gmail.com> napisał(a):

> Hi Larry,
>
> Thank you for the feedback.
>
> I think you got the two strategies right. However, there is a use-case
> in which an object manages its own laziness by making itself lazy:
>
> ```
> class C {
>      public function __construct() {
>         ReflectionLazyObject::makeLazyGhost($this, $this->init(...));
>     }
> }
> ```
>
> This one can not be addressed by a newInstance*() method since the
> object to be made lazy already exists.
>

Did you consider implementing it using some attribute?

On constructor like:
```
class C {
    #[LazyInitialization]
     public function __construct(private readonly string $foo) {
        // ... init executes after first use, but all promoted properties
are already initialized
    }
}
```

or on specialized initializer:

```
class C {
     public function __construct(private readonly string $foo) {
        // do something light
    }

    #[LazyInitialization]
    private function initi(): void
    {
        // do something heavy
    }
}
```

I don't know if this is a good example of doing the same thing or if it
doesn't limit functionality,
but for me, it is way more clean and easier to understand.

Cheers,
Michał Marcin Brzuchalski

---

## #123562 — Chris Riley — 2024-06-08T23:24:26+00:00

URL: https://externals.io/message/123503#123562 · In reply to: #123503

On Tue, 4 Jun 2024 at 13:29, Nicolas Grekas <nicolas.grekas+php@gmail.com>
wrote:

> Dear all,
>
> Arnaud and I are pleased to share with you the RFC we've been shaping for
> over a year to add native support for lazy objects to PHP.
>
> Please find all the details here:
> https://wiki.php.net/rfc/lazy-objects
>
> We look forward to your thoughts and feedback.
>
> Cheers,
> Nicolas and Arnaud
>

I'm wondering why this has been attached to the existing reflection API
instead of being a new thing in and of itself? It doesn't seem strictly
related to reflection other than currently the solutions for this rely on
reflection to work.

~C

---

## #123601 — Arnaud Le Blanc — 2024-06-14T12:13:28+00:00

URL: https://externals.io/message/123503#123601 · In reply to: #123524

Hi Tim,

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


> 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

> > […]
>
> 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

> > […]
>
> 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.

> > […]
>
> 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.

> > […]
>
> 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.

> > […]
>
> 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

---

## #123602 — Arnaud Le Blanc — 2024-06-14T13:03:30+00:00

URL: https://externals.io/message/123503#123602 · In reply to: #123529

Hi Michał, Chris,

On Thu, Jun 6, 2024 at 8:53 AM Michał Marcin Brzuchalski
<michal.brzuchalski@gmail.com> wrote:
> Did you consider implementing it using some attribute?


> I'm wondering why this has been attached to the existing reflection API instead of being a new thing in and of itself? It doesn't seem strictly related to reflection other than currently the solutions for this rely on reflection to work.

Thank you for your feedback.

Currently the lazy objects feature is designed as a low-level
technical building block that libraries and frameworks can use. FFI
and Fibers are examples of such features that most users do not use
directly, but can benefit greatly from within libraries they use.

We have considered the proposed syntaxes, such as using annotations,
but these do not match the core use case of creating a lazy instance
of a class without requiring cooperation from the class itself.
Furthermore we believe that designing a higher-level API in the
language would be considerably more difficult and could put the RFC at
risk.

However, it is possible to introduce a higher-level way to create lazy
objects in a future RFC.

Best Regards,
Arnaud

---

## #123604 — tim@bastelstu.be — 2024-06-14T15:57:11+00:00

URL: https://externals.io/message/123503#123604 · In reply to: #123601

Hi


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

for some preliminary feedback: I've given the RFC another quick read and 
it already reads *much* better, thank you. The two examples for the two 
strategies were pretty illustrative.

- I noticed a small typo "makeInstanceLazyHost".
- The ORM example uses 'setPropertyValue', I believe it should read 
'setRawPropertyValue'.
- The LazyConnection example uses 'ReflectionLazyObject', should that 
read 'ReflectionLazyObjectFactory'?

I plan to give more detailed feedback at a later point, when i have the 
time to think through your reply and the semantics updated RFC.

Best regards
Tim Düsterhus

PS: Thanks, I hate the example of how readonly properties can already 
change in existing versions :-(

---

## #123607 — Larry Garfield — 2024-06-14T20:15:25+00:00

URL: https://externals.io/message/123503#123607 · In reply to: #123601

> Hi Tim,
>
> We have updated the RFC to address your feedback. Please find
> additional answers below.

The updated RFC looks much better, thank you.  Though I still have some thoughts, in no particular order.

> The actual instance is allowed to escape the proxy and to create direct references to itself.

How?  Is this a "return $this" type of situation?  This could use more fleshing out and examples.

The terms "virtual" and "proxy" seem to be used interchangeably in different places, including in the API.  Please just use one, and purge the other.  It's confusing as is. :-)  (I'd favor "proxy", as it seems more accurate to what is happening.)  For that matter, I'd be very tempted to remove the word "lazy" from the API calls.  `newGhostInstance()` and `newProxyInstance()` are plenty understandable, and shorter/easier to read.  (Similarly, `makeGhostInstance()` and `makeProxyInstance()`.  Although since those are more about modifying a provided object to be lazy, perhaps "make" isn't the right verb to use as that often means "create".)


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.

> After calling newLazyGhostInstance(), the behavior of the object is the same as an object created by newLazyGhostInstance(). 

I think the first is supposed be a make* call?

> When making an existing object lazy, the makeInstanceLazy*() methods call the destructor unless the SKIP_DESTRUCTOR flag is given. 

I don't quite get why this is.  Admittedly destructors are rarely used, but why does it need to call the destructor?

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?  


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.  


As I said, definitely improved, and I like where it's going.  I think it can improve further, though.

--Larry Garfield

---

## #123608 — Larry Garfield — 2024-06-14T20:25:50+00:00

URL: https://externals.io/message/123503#123608 · In reply to: #123607

And of course I got the code sample wrong.  It should 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)->newGhostInstance(init);

Sorry about that.

--Larry Garfield

---

## #123612 — Arnaud Le Blanc — 2024-06-15T13:28:40+00:00

URL: https://externals.io/message/123503#123612 · In reply to: #123607

Hi Larry,


> > […]
>
> How?  Is this a "return $this" type of situation?  This could use more fleshing out and examples.

"return $this" will return the proxy object, but it is possible to
create references to the actual instance during initialization, either
directly in the initializer function, or in methods called by the
initializer. The "About Proxies" section discusses this a bit. I've
added an example.

> The terms "virtual" and "proxy" seem to be used interchangeably in different places, including in the API.  Please just use one, and purge the other.  It's confusing as is. :-)  (I'd favor "proxy", as it seems more accurate to what is happening.)

Agreed

> 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 think the first is supposed be a make* call?

Thank you!

> > […]
>
> I don't quite get why this is.  Admittedly destructors are rarely used, but why does it need to call the destructor?

The rationale is that unless specified otherwise, we must assume that
the constructor has been called on the object. Therefore we must call
the destructor before resetting the object's state entirely. See also
the Mutex example given by Tim. I've added the rationale and an
example.

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.

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

> 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

---

## #123619 — Larry Garfield — 2024-06-15T17:09:50+00:00

URL: https://externals.io/message/123503#123619 · In reply to: #123612

> Hi Larry,
>

> > […]
>
> 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?

> > […]
>
> 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.

> > […]
>
> 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

---

## #123635 — Arnaud Le Blanc — 2024-06-16T13:46:28+00:00

URL: https://externals.io/message/123503#123635 · In reply to: #123619

> > […]
> 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 the case an object does not have a destructor, it won't make a difference either way, correct?

Yes

> > […]
>
> 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.

> > […]
>
> 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.

---

## #123636 — Larry Garfield — 2024-06-16T14:35:40+00:00

URL: https://externals.io/message/123503#123636 · In reply to: #123635

> 
> > […]
>
> 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?

Primarily I was just reacting to this line:

> However, for more complex use-cases where the initializer wishes to access non-public properties, it is required to bind the initializer function to the right scope (with Closure::bind()), or to access properties with ReflectionProperty. 

And asking "OK, um, how?"  Because what I was coming up with didn't make any sense. :-)  

It's debatable if the use case of wanting to assign to private properties in the initializer without using Reflection is common enough to warrant more.  I'm not sure at this point.  If we wanted to, I could see an extra flag that would tell the system to bind the closure to the object before calling it.  I don't know how common a need that will be, though, so I won't insist it be included.  But I would like to see that line clarified with one of the above examples, because as is, I would expect to be able to bind to the object as I was trying to do and it (obviously) didn't work.

> > […]
>
> 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.

Hm, interesting.  Please include this sort of example in the RFC, so we know what the use is (and when it won't matter, which seems like it would be the more common case).

> > […]
>
> 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);
> }
> ```

Ah, now I see what you mean.  Interesting.  Including that reasoning in the RFC would be good.  Though, I don't know how often I'd be calling isInitialized() on a larger set of objects, hot path or no.  

--Larry Garfield

---

## #123664 — Arnaud Le Blanc — 2024-06-18T17:45:00+00:00

URL: https://externals.io/message/123503#123664 · In reply to: #123635

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


>
> 
> > […]
>
> 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?
>
> > […]
>
> Yes
>
> > […]
>
> Good point, $factory would be a good name for this parameter.
>
> > […]
>
> 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.
>
> > […]
>
> 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);
> }
> ```
>
> > […]
>
> Thank you for your help. We will think about a better API.

---

Next page: https://externals.io/message/123503.md?page=2
