Propagate #[Singleton] attribute bindings back to root app (fixes laravel/octane#1077)#1109
Draft
JoshSalway wants to merge 4 commits intolaravel:2.xfrom
Draft
Propagate #[Singleton] attribute bindings back to root app (fixes laravel/octane#1077)#1109JoshSalway wants to merge 4 commits intolaravel:2.xfrom
JoshSalway wants to merge 4 commits intolaravel:2.xfrom
Conversation
When classes decorated with the #[Singleton] attribute are resolved inside the request sandbox, the container lazily registers them as shared bindings - but only on the cloned sandbox. Because the root application never receives these bindings, every subsequent clone treats the singleton as a new instance, effectively scoped per request. Add PropagateAttributeSingletons listener that runs on OperationTerminated (before FlushTemporaryContainerInstances) to copy attribute-detected singleton bindings and their resolved instances from the sandbox back to the root application, so future clones inherit them. Fixes laravel#1077 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The listener used reflection to access the checkedForSingletonOrScopedAttributes property which was introduced in Laravel 11. On Laravel 10, this threw a ReflectionException that propagated through the OperationTerminated event chain, preventing subsequent listeners (FlushTemporaryContainerInstances, etc.) from running and causing cascading test failures. Add a property_exists guard so the listener is a no-op on Laravel versions that predate the #[Singleton] attribute, and skip the AttributeSingletonPropagation tests on those versions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Thanks for submitting a PR! Note that draft PRs are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface. Pull requests that are abandoned in draft may be closed due to inactivity. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1077 -- Classes decorated with PHP's
#[Singleton]attribute are re-instantiated on every request in Octane, defeating the purpose of the singleton pattern. The attribute works correctly in standard Laravel but fails in Octane's sandbox architecture.Root Cause
The bug exists because Octane clones the application container for each request (the "sandbox"). When a
#[Singleton]class is first resolved during a request, the container lazily detects the attribute and registers a shared binding -- but only on the sandbox clone. When the request ends, the sandbox is discarded. The root application never receives the singleton binding or instance, so the next request's fresh sandbox clone starts over, resolving and constructing the class again. Effectively,#[Singleton]behaves as a scoped binding in Octane.Why This Fix Works
This fix works because it introduces a
PropagateAttributeSingletonslistener that runs after each request (on theOperationTerminatedevent). The listener inspects the sandbox's internalcheckedForSingletonOrScopedAttributescache to find any classes that were detected as singletons during the request. For each one, it copies three things back to the root application: (1) the singleton binding registration, (2) the resolved instance, and (3) the attribute cache entry. Future sandbox clones inherit these from the root app, so the singleton is resolved once and reused across all subsequent requests -- matching the behavior of singletons registered in service providers.Alternatives Considered
We chose this approach over pre-scanning all classes at boot time because: (1) the attribute detection is intentionally lazy in Laravel -- forcing eager scanning would slow boot time; (2) modifying the container's clone behavior would risk breaking scoped bindings (
#[Scoped]) that intentionally should NOT persist across requests; (3) the listener approach is non-invasive, only affects classes actually resolved during requests, and correctly distinguishes betweensingletonandscopedattribute types via the cache metadata.Files Changed
src/Listeners/PropagateAttributeSingletons.php-- New listener that copies attribute singleton bindings and instances from sandbox to root appconfig/octane.php-- Registers the listener onOperationTerminatedsrc/OctaneServiceProvider.php-- Registers the listener as a singletontests/AttributeSingletonPropagationTest.php-- 2 tests verifying instance persistence and single construction across requests