Skip to content

Register back-end navigation, widgets and settings in a Laravel Octane worker - #1511

Closed
austinderrick wants to merge 10 commits into
wintercms:wip/1.3from
austinderrick:feat/octane-worker-support
Closed

austinderrick wants to merge 10 commits into
wintercms:wip/1.3from
austinderrick:feat/octane-worker-support

Conversation

@austinderrick

@austinderrick austinderrick commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Working toward Laravel Octane support. This PR is now only the change that has to live in core; the rest moved to the Winter.Octane plugin or to separate PRs.

The Backend, Cms and System service providers register back-end navigation, report widgets, form widgets and settings only when runningInBackend() is true. An Octane worker registers its providers once, against the synthetic request it boots with, so that check is false and none of the back-end registrations ever happen. Each gate now also passes when Octane's client contract (Laravel\Octane\Contracts\Client) is bound. Octane binds it in the worker before the providers register, and nothing else binds it, so PHP-FPM and artisan behave exactly as before. Registering in a worker costs little, because each call stores a callback and the menus and lists are built later, on first use.

The check uses the binding directly, so this PR no longer depends on wintercms/storm#241.

What moved out

These changes moved to the Winter.Octane plugin, which clears the same state from outside core at the start of every Octane operation:

  • the per-request resets for NavigationManager, WidgetManager, SettingsManager, MailManager and ComponentManager
  • the per-user key for the PreferenceMaker cache, which the plugin's reset of that static makes unnecessary
  • the BackendController::$action docblock fix

These are bugs under PHP-FPM too, so each is now its own PR:

Testing

modules/backend/tests/ServiceProviderTest.php checks that back-end navigation is not registered outside the back end, and is registered when Octane's client is bound; that second test fails without the change. Since the Cms and System gates are the same one-line change, it covers only the Backend provider.

The Winter.Octane suite (44 tests) also passes against this branch and wintercms/storm#241. It drives requests through Octane's real Worker and ApplicationGateway.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6a1ad9dd-0a21-427d-93bd-8680efba4c6a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…oundary

Under PHP-FPM every request gets a fresh process, so state left on a long-lived object never hurts
anyone. Under Laravel Octane the same process serves many requests, and anything derived from one
request stays visible to the next. Several of these leak one user's data to another.

Nothing here requires the companion Storm changes to be present. Winter has to load on a Storm that
predates them, and `implements` is resolved when a class is loaded, so naming a contract that a given
Storm may not ship makes a class unloadable rather than merely unresettable. The managers therefore
declare resetWorkerState() without naming ResetsWorkerState, and the boot-time gates reach
runningInApplicationServer() through method_exists(). Where the Storm side is absent, the gates answer
exactly as they do today, which is correct: there is no worker support to admit.

Winter also takes no dependency on laravel/octane, not even for development. The package appears in
suggest only, since a persistent worker is something an application opts into. Every Octane test skips
where the package is absent, and the test bootstrap builds its no-op client as an anonymous class so
that no file naming an Octane contract has to load when there is nothing to name.

Registers Octane's service provider when the package is installed, since Winter replaces Laravel's
provider discovery and the provider was never picked up. Adds System\Classes\Octane\ResetsRequestState,
which clears request-derived state at the start of every operation rather than the end, because an
exception escaping the HTTP kernel skips terminate() and RequestTerminated never fires.

Rebuilds the permission-filtered contents of NavigationManager, WidgetManager, SettingsManager,
MailManager and ComponentManager per request, while leaving their once-per-worker registrations alone.
Clears the per-request caches that PreferenceMaker and the URL maker declare on traits, which needs the
using classes resolved because PHP gives each of them its own copy. Keys the preference cache by user.

Admits an application server in the boot-time gates of the Backend, System and Cms providers. Those
gates test runningInBackend() once per worker, at boot, when no request exists, so back-end registration
never ran at all under a worker.

Loads a rebuilt CMS code cache file with require rather than require_once. The path is deterministic
while the class name inside is random per rebuild, so in a reused process the load did nothing and left
the class undeclared, which also disabled the recovery path in handleCorruptCache().

Closes the Twig include gate in a finally block, so a throw during template loading no longer leaves it
open for every later request.

Invokes the plugin reset on any plugin that implements ResetsWorkerState or simply declares
resetWorkerState(), for the same loading reason as the managers above.

Tests: 499 green against the companion Storm branch, and 499 green against stock wip/1.3 Storm with
laravel/octane absent, where the Octane tests skip. Each fix was verified failing before its change.
Also exercised end to end against a Winter based application of about thirty plugins running on Octane
and FrankenPHP, whose Cypress suite of 36 groups passes on Octane and PHP-FPM alike.
@austinderrick
austinderrick force-pushed the feat/octane-worker-support branch from 468913a to 1be38b5 Compare August 12, 2026 01:23
@LukeTowers

Copy link
Copy Markdown
Member

@austinderrick can Octane support be provided by a first party plugin (i.e. Winter.Octane)?

@austinderrick

Copy link
Copy Markdown
Contributor Author

@austinderrick can Octane support be provided by a first party plugin (i.e. Winter.Octane)?

Hey @LukeTowers : we could move some of this to a first party plugin, like the registerOctane/resetRequestState orchestrator, etc.

A big chunk of the changes here are more specific to the Winter internals and how they hold onto variables, so we'll probably need to keep some of them. (And the partner wintercms/storm#241 too).

I'll draft something up for it!

The worker-safety primitives stay in core: resettable manager state, the
ResetsWorkerState contract in Storm, and the boot-vs-request gating fixes.
What moves to the plugin is the Octane-specific wiring — registering
Laravel\Octane\OctaneServiceProvider (Winter disables package discovery),
the request-boundary ResetsRequestState listener, and the tests that drive
Octane's ApplicationGateway.

The plugin declares itself elevated so the reset still attaches when
PluginManager::$noInit skips ordinary plugin initialization. The composer
suggestion now points at winter/wn-octane-plugin, which requires
laravel/octane itself.
@austinderrick austinderrick changed the title Working toward Octane support: reset request state at the operation boundary Working toward Octane support: make core request state worker-safe Aug 13, 2026
The plugin's own composer.json requires laravel/octane; core does not need
to advertise it.
@austinderrick

austinderrick commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor Author

@LukeTowers : I made a repo over here for the plugin and invited you, so we can move it to the Winter org: https://github.com/austinderrick/Winter.Octane

Also on this PR and the related Storm one, I rewired them to work with the plugin instead.
(Both need some changes to fix state operations, but it's much more minimal with the plugin. I like this approach a lot more!)

@austinderrick
austinderrick marked this pull request as ready for review August 13, 2026 23:48
@LukeTowers

Copy link
Copy Markdown
Member

@austinderrick if you make me an admin or an owner on the repo then I can transfer it to the Winter organization and you can continue working on it from the official namespace.

@austinderrick

Copy link
Copy Markdown
Contributor Author

@austinderrick if you make me an admin or an owner on the repo then I can transfer it to the Winter organization and you can continue working on it from the official namespace.

@LukeTowers : Yep! Transfer headed your way.

@LukeTowers

Copy link
Copy Markdown
Member

@austinderrick repository has been transferred and added to packagist: https://packagist.org/packages/winter/wn-octane-plugin

The manager resets, the preference cache key and the docblock fix move to the Winter.Octane plugin. The CodeParser and Twig include gate fixes move to their own PRs. The gates now check Octane's client binding directly, so this PR no longer depends on a new Storm method.
@austinderrick austinderrick changed the title Working toward Octane support: make core request state worker-safe Register back-end navigation, widgets and settings in a Laravel Octane worker Oct 15, 2026
austinderrick added a commit to wintercms/wn-octane-plugin that referenced this pull request Oct 15, 2026
…eset methods (#1)

The core PRs were reduced to the changes a plugin cannot make (wintercms/winter#1511 and wintercms/storm#241), so the plugin now clears the manager, auth, error mask and Halcyon state itself:

- MANAGER_STATE lists the request-derived properties on the navigation, widget, settings, mail, component and back-end auth managers, cleared by reflection.
- ErrorHandler's masks join STATIC_CACHES, and the Halcyon in-memory cache is flushed on every resolved store.
- ResolveAuthIpAddress records the client address on the auth manager after the trusted-proxy middleware.
- The ResetsWorkerState contract moves to Winter\Octane\Contracts.
- Registration now checks only that Storm dispatches interface listeners, and boot() logs a warning when a worker has no back-end navigation (Winter without #1511).
- The auth reset also turns sessions back on, covering Storm without the once() fix.

@LukeTowers LukeTowers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not wild about this approach, plugins make use of the same pattern. Is there another way we could achieve the desired end result without requiring an octane specific conditional in the core code?

@austinderrick

austinderrick commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Closing: this moved into the Octane plugin in wintercms/wn-octane-plugin#2, which makes these back-end registrations itself when a worker boots, so core needs no Octane-specific check.

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.

2 participants