Skip to content

[VarExporter] Add trait to help implement lazy loading ghost objects #46751

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 12, 2022

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Jun 23, 2022

Q A
Branch? 6.2
Bug fix? no
New feature? yes
Deprecations? no
Tickets -
License MIT
Doc PR -

EDIT: the trait used to be named LazyGhostObjectTrait but it's been renamed to LazyGhostTrait in #47236, where a new LazyProxyTrait has also been added.

This PR packages an implementation of lazy loading ghost objects in a single LazyGhostObjectTrait (as a reminder, a lazy ghost object is an object that is created empty and that is able to initialize itself when being accessed for the first time.)

By using this trait, ppl can easily turn any existing classes into such ghost object implementations.

I target two use cases with this feature (but ppl are free to be more creative):

  1. lazy proxy generation for service containers;
  2. lazy proxy generation for entities.

In all cases, the generation itself is trivial using inheritance (sorry final classes.) For example, in order to turn a Foo class into a lazy ghost object, one just needs to do:

class FooGhost extends Foo implements LazyGhostObjectInterface
{
    use LazyGhostObjectTrait;
}

And then, one can instantiate ghost objects like this:

$fooGhost = FooGhost::createLazyGhostObject($initializer);

$initializer should be a closure that takes the ghost object instance as argument and initializes it. An initializer would typically call the constructor on the instance after resolving its dependencies:

$initializer = function ($instance) use ($etc) {
    // [...] use $etc to compute the $deps
    $instance->__construct(...$deps);
};

Interface LazyGhostObjectInterface is optional to get the behavior of a ghost object but gives a contract that allows managing them when needed:

    public function initializeLazyGhostObject(): void;
    public function resetLazyGhostObject(): bool;

Because initializers are not freed when initializing, it's possible to reset a ghost object to its uninitialized state. This comes with one limitation: resetting readonly properties is not allowed by the engine so these cannot be reset. The main target use case of this capability is Doctrine's EntityManager of course.

To work around the limitation with readonly properties, but also to allow creating partially initialized objects, $initializer can also accept two more arguments $propertyName and $propertyScope. When doing so, $initializer is going to be called on a property-by-property basis and is expected to return the computed value of the corresponding property.

Because lazy-initialization is not triggered when (un)setting a property, it's also possible to do partial initialization by calling setters on a just-created ghost object.


You might wonder why this PR is in the VarExporter component? The answer is that it reuses a lot of its existing code infrastructure. Exporting/hydrating/instantiating require using reflection a lot, and ghost objects too. We could consider renaming the component, but honestly, 1. I don't have a good name in mind; 2. changing the name of a component is costly for the community and 3. more importantly this doesn't really matter because this is all low-level stuff usually.

You might also wonder why this trait while we already have https://github.com/FriendsOfPHP/proxy-manager-lts and https://github.com/Ocramius/ProxyManager?

The reason is that the code infrastructure in ProxyManager is heavy. It comes with a dependency on https://github.com/laminas/laminas-code and it's complex to maintain. While I made the necessary changes to support PHP 8.1 in FriendsOfPHP/proxy-manager-lts (and submitted those changes upstream), getting support for new PHP versions is slow and complex. Don't take me wrong, I don't blame maintainers, ProxyManager is complex for a reason.

But ghost objects are way simpler than other kind of proxies that ProxyManager can produce: a trait does the job. While the trait itself is no trivial logic, it's at least plain PHP code, compared to convoluted (but needed) code generation logic in ProxyManager.

If you need any other kind of proxies that ProxyManager supports, just use ProxyManager.

For Symfony, having a simple lazy ghost object implementation will allow services declared as lazy to be actually lazy out of the box (today, you need to install proxy-manager-bridge as an optional dependency.) \o/

@Tobion
Copy link
Contributor

Tobion commented Jun 24, 2022

For Symfony, having a simple lazy ghost object implementation will allow services declared as lazy to be actually lazy out of the box (today, you need to install proxy-manager-bridge as an optional dependency.) \o/

How do you plan to do this integration? I assume devs need to mark a service as lazy and also use LazyGhostObjectTrait in their class. Or do you want to do code generation for lazy marked services that implements automatically?

class FooGhost extends Foo implements LazyGhostObjectInterface
{
    use LazyGhostObjectTrait;
}

@nicolas-grekas
Copy link
Member Author

@Tobion this is already implemented, you might want to check #46752

@nicolas-grekas nicolas-grekas force-pushed the ve-ghost-objects branch 3 times, most recently from 17e0838 to 798ba60 Compare June 30, 2022 13:18
@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Jun 30, 2022

I reworked the implementation to allow partial initialization. There are two ways to achieve it:

  • the $initializer can optionally accept two more arguments $propertyName and $propertyScope. When doing so, it is going to be called on a property-by-property basis and is expected to return the computed value of the corresponding property.
  • because lazy-initialization is not triggered when (un)setting a property, it's also possible to do partial initialization by calling setters on a just-created ghost object.

PR description + README updated.

@nicolas-grekas nicolas-grekas force-pushed the ve-ghost-objects branch 7 times, most recently from 6971e57 to f5d45dd Compare July 5, 2022 07:56
@nicolas-grekas
Copy link
Member Author

/cc @symfony/mergers this PR is ready :)

nicolas-grekas added a commit that referenced this pull request Jul 5, 2022
…ce (nicolas-grekas)

This PR was merged into the 6.2 branch.

Discussion
----------

[VarExporter] Fix accessing readonly properties by reference

| Q             | A
| ------------- | ---
| Branch?       | 6.2
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

Extracted from #46751

Should make tests green on PHP 8.2.

Commits
-------

46ca03e [VarExporter] Fix accessing readonly properties by reference
@nicolas-grekas nicolas-grekas merged commit 338daf2 into symfony:6.2 Jul 12, 2022
nicolas-grekas added a commit that referenced this pull request Jul 12, 2022
…oxies out of the box (nicolas-grekas)

This PR was merged into the 6.2 branch.

Discussion
----------

[DependencyInjection] Use lazy-loading ghost object proxies out of the box

| Q             | A
| ------------- | ---
| Branch?       | 6.2
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #35345
| License       | MIT
| Doc PR        | -

This PR builds on #46751. It also replaces #46458.

Instead of using ProxyManager to make lazy services actually lazy, using `LazyGhostObjectTrait` from #46751 allows doing so *out of the box* - aka without the need to install any optional dependencies.

When a virtual proxy is required (typically when using [the `proxy` tag](#27697)), ProxyManager is still required (and the dep remains optional.)

But for most services, using `LazyGhostObjectTrait` just works \o/

Commits
-------

58a1848 [DependencyInjection] Use lazy-loading ghost object proxies out of the box
@nicolas-grekas nicolas-grekas deleted the ve-ghost-objects branch July 12, 2022 16:17
fabpot added a commit that referenced this pull request Sep 3, 2022
…ng virtual proxies for non-ghostable lazy services (nicolas-grekas)

This PR was merged into the 6.2 branch.

Discussion
----------

[DependencyInjection][VarExporter] Generate lazy-loading virtual proxies for non-ghostable lazy services

| Q             | A
| ------------- | ---
| Branch?       | 6.2
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

Since #46752 and #46751, we are able to make services lazy out of the box, except when 1. a service relies on an internal class 2. a service has the `proxy` tag or 3. a service's class is abstract (and the service uses a factory). In these situations, proxy-manager-bridge was required. This was an acceptable trade-off because this would be quite uncommon. But while working on Doctrine, I realized that we cannot use ghost objects when a factory is used. I described this for Doctrine in doctrine/orm#9896 but the situation can happen with any services constructed by a factory. This means we'd need proxy-manager-bridge anytime a factory is used on a lazy service. What was uncommon becomes quite common and the trade-off is not acceptable anymore. Thus this PR.

This PR adds a `LazyProxyTrait` and a `ProxyHelper` to build lazy loading virtual proxies at will. It then wires this new capability into the container. As a result proxy-manager-bridge is not needed anymore. We can deprecate it in another PR.

While the diff is quite big,  `LazyProxyTrait` has many similarities with `LazyGhostTrait` and both traits can be diffed to see where their behavior varies.

Excerpt from the [README](https://github.com/nicolas-grekas/symfony/blob/ve-inheritance-proxy/src/Symfony/Component/VarExporter/README.md) for the record:

>The component provides two lazy loading patterns: ghost objects and virtual proxies (see https://martinfowler.com/eaaCatalog/lazyLoad.html for reference.)
>
>Ghost objects work only on concrete and non-internal classes. In the generic case, they are not compatible with using factories in their initializer.
>
>Virtual proxies work on concrete, abstract or internal classes. They provide an API that looks like the actual objects and forward calls to them. They can cause identity problems because proxies might not be seen as equivalents to the actual objects.
>
>Because of this identity problem, ghost objects should be preferred when possible. Exceptions thrown by the ProxyHelper class can help decide when it can be used or not.
>
>Ghost objects and virtual proxies both provide implementations for the LazyObjectInterface which allows resetting them to their initial state or to forcibly initialize them when needed. Note that resetting a ghost object skips its read-only properties. You should use a virtual proxy to reset read-only properties.

Commits
-------

4862139 [DependencyInjection][VarExporter] Generate lazy proxies for non-ghostable lazy services out of the box
@fabpot fabpot mentioned this pull request Oct 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants