-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Conversation
f0d3a20
to
43dfc9b
Compare
1b739d0
to
135fc98
Compare
How do you plan to do this integration? I assume devs need to mark a service as lazy and also
|
src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhostObject/MagicClass.php
Outdated
Show resolved
Hide resolved
17e0838
to
798ba60
Compare
I reworked the implementation to allow partial initialization. There are two ways to achieve it:
PR description + README updated. |
6971e57
to
f5d45dd
Compare
/cc @symfony/mergers this PR is ready :) |
…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
f5d45dd
to
af1612a
Compare
src/Symfony/Component/VarExporter/Internal/GhostObjectState.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/VarExporter/Internal/GhostObjectRegistry.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/VarExporter/Tests/LazyGhostObjectTraitTest.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/VarExporter/Tests/LazyGhostObjectTraitTest.php
Outdated
Show resolved
Hide resolved
af1612a
to
cf62f1a
Compare
cf62f1a
to
27b4325
Compare
…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
…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
EDIT: the trait used to be named
LazyGhostObjectTrait
but it's been renamed toLazyGhostTrait
in #47236, where a newLazyProxyTrait
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):
In all cases, the generation itself is trivial using inheritance (sorry
final
classes.) For example, in order to turn aFoo
class into a lazy ghost object, one just needs to do:And then, one can instantiate ghost objects like this:
$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:Interface
LazyGhostObjectInterface
is optional to get the behavior of a ghost object but gives a contract that allows managing them when needed: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/