Skip to content

Fix dispatching events that have listeners registered against an interface - #241

Merged
LukeTowers merged 10 commits into
wintercms:wip/1.3from
austinderrick:feat/octane-worker-support
Oct 16, 2026
Merged

LukeTowers merged 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.

Winter\Storm\Events\Dispatcher inherits Laravel's addInterfaceListeners(), which expects listeners stored as listeners[event][]. Winter's dispatcher wraps listeners as it registers them and stores them by priority, as listeners[event][priority][], so the inherited method passes each priority bucket to makeListener() as if it were a listener. Dispatching any event that has a listener registered against one of its interfaces then throws. Octane registers its post-operation cleanup against the OperationTerminated interface, so every RequestTerminated dispatch fails. The override merges the sorted listeners for each interface instead.

This also affects PHP-FPM: any application or package that listens on an interface hits the same exception today.

What moved out

These changes moved to the Winter.Octane plugin, which clears the same state from outside core:

  • the ResetsWorkerState contract and the auth manager reset
  • the lazy throttle address (the plugin records the client address after the trusted-proxy middleware)
  • ErrorHandler::resetMaskState() and the Halcyon request cache flush

These were dropped:

These are bugs outside Octane too, so each is now its own PR:

Testing

tests/Events/InterfaceListenerTest.php covers interface listeners alone, alongside class listeners in priority order, class-string listeners resolved through the container, and events with no interface listeners. The full Storm suite passes (872 tests).

…t application server

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. This adds the primitives the companion Winter changes call.

Fires listeners registered against the interfaces an event carries, not only against its class name.
Laravel dispatches several events by interface and Octane depends on it: RequestTerminated carries
OperationTerminated, and Octane registers its cleanup against the interface. With the previous
dispatcher, Octane's own state handling sat inactive for the whole request lifecycle and nothing
errored. This applies everywhere rather than only under Octane, and is the widest reaching change here.

Adds Application::runningInApplicationServer(), which reports whether a request-serving worker is
active by testing for the Laravel\Octane\Contracts\Client binding. Octane binds it in workers and
nowhere else, so installing the package does not make an ordinary artisan command answer true. Boot-time
gates need this because runningInConsole() is true under Octane, where the SAPI really is CLI.

Adds reset primitives to the services that cache per-request values, named by the new
Winter\Storm\Contracts\ResetsWorkerState contract so Winter can find them. Auth\Manager held the
resolved user and impersonation stack, Halcyon\MemoryCacheManager and Halcyon\Model held parsed template
records, Exception\ErrorHandler held custom handler masks, and ExtendableTrait held per-request
extension bindings. Implementations must be idempotent, and must leave boot-time registrations alone,
since those are built once per worker and discarding them degrades the worker for good.

Applies the console root URL override in RegisterWinter only when no request is bound. Under Octane the
SAPI is CLI, so it previously fired for every request a worker served and overwrote the URL derived
from the real request.

Documents how flushRequestCache differs from flushDuplicateCache, which are easy to confuse.

Tests: 736 green, each verified failing before its fix. 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.
@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: 137192bb-0284-4a2c-ba5d-1ba003c35738

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.

@austinderrick
austinderrick marked this pull request as draft August 12, 2026 00:53
@austinderrick
austinderrick marked this pull request as ready for review August 12, 2026 01:07
The worker-state reset ran before the trusted-proxy middleware, so an eager
Request::ip() there recorded the load balancer's address — and could still be
reading the previous operation's request. resetWorkerState() now leaves the
address unresolved and resolveIpAddress() derives it on first use, which under
throttling happens mid-request, after the proxy chain is trusted.

Also corrects the runningInApplicationServer() docblock: Octane's worker
entrypoints set APP_RUNNING_IN_CONSOLE=false, so runningInConsole() answers
false inside a worker, not true as previously claimed.
Coalescing a null Request::ip() to '0.0.0.0' changed throttle semantics for
queue jobs and artisan commands: findThrottleByUserId() scopes its lookup to
the given address when one is present, so the placeholder address missed
existing ban rows recorded under real client IPs and minted spurious
'0.0.0.0' rows. A null address means 'match on the user alone', which is how
console contexts found bans before this branch. The resolver is now public
(getIpAddress) so external readers have a resolving access path instead of
observing the property's unresolved null window.
init() still captured Request::ip() at singleton construction, which under
plain PHP-FPM can happen before the trusted-proxy middleware runs — anything
resolving auth early froze the load balancer's address for the whole request,
and getIpAddress() never re-derives once a non-null value is stored. The
address now starts unresolved everywhere and is derived at first actual use.
@austinderrick
austinderrick force-pushed the feat/octane-worker-support branch from 3ad24fb to 18d39fe Compare August 13, 2026 23:50
The worker-state and detection tests carried multi-paragraph docblocks
narrating production rationale that belongs in the production code, plus one
claim the branch itself later corrected (that a worker answers true to
runningInConsole). Cut each comment to what the test name does not already
say, and correct that claim where it was repeated.
@github-actions

Copy link
Copy Markdown

This pull request will be closed and archived in 3 days, as there has been no activity in the last 60 days.
If this is still being worked on, please respond and we will re-open this pull request.
If this pull request is critical to your business, consider joining the Premium Support Program where a Service Level Agreement is offered.

The auth, error handler, Halcyon cache and ResetsWorkerState changes move to the Winter.Octane plugin, which clears the same state from outside. The Auth\Manager::once() and ExtendableTrait fixes move to their own PRs. runningInApplicationServer() is no longer needed: the Winter gates check Octane's client binding directly, and Octane's entry points already make runningInConsole() false, so the Halcyon request cache is on in a worker without a change.
Octane's worker entry points set APP_RUNNING_IN_CONSOLE to false, so runningInConsole() is false in a worker and the console root URL override never runs there. The Winter.Octane suite passes without the guard.
@austinderrick austinderrick changed the title Working toward Octane support: worker-safe primitives Fix dispatching events that have listeners registered against an interface 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.
@austinderrick

Copy link
Copy Markdown
Contributor Author

@LukeTowers I reduced this to the interface-listener fix only. The rest moved to wn-octane-plugin or the split-out PRs (#261, #262).

@LukeTowers
LukeTowers merged commit c7990f3 into wintercms:wip/1.3 Oct 16, 2026
10 checks passed
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.

2 participants