Skip to content

Propagate #[Singleton] attribute bindings back to root app (fixes laravel/octane#1077)#1109

Draft
JoshSalway wants to merge 4 commits intolaravel:2.xfrom
JoshSalway:fix/singleton-attribute-sandbox
Draft

Propagate #[Singleton] attribute bindings back to root app (fixes laravel/octane#1077)#1109
JoshSalway wants to merge 4 commits intolaravel:2.xfrom
JoshSalway:fix/singleton-attribute-sandbox

Conversation

@JoshSalway
Copy link
Copy Markdown

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 PropagateAttributeSingletons listener that runs after each request (on the OperationTerminated event). The listener inspects the sandbox's internal checkedForSingletonOrScopedAttributes cache 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 between singleton and scoped attribute types via the cache metadata.

Files Changed

  • src/Listeners/PropagateAttributeSingletons.php -- New listener that copies attribute singleton bindings and instances from sandbox to root app
  • config/octane.php -- Registers the listener on OperationTerminated
  • src/OctaneServiceProvider.php -- Registers the listener as a singleton
  • tests/AttributeSingletonPropagationTest.php -- 2 tests verifying instance persistence and single construction across requests

Josh Salway and others added 3 commits March 19, 2026 04:44
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>
@github-actions
Copy link
Copy Markdown

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Singleton Attribute in DI is not respected (reset at each request in same worker)

1 participant