From 9dc34d5c1049092fce81688d0fea81d13a54de26 Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Fri, 25 Sep 2026 12:56:37 +0200 Subject: [PATCH] fix(hot-reload): keep the event dispatcher in the message child The dev hot reload forgot the `events` instance, so the container rebuilt an empty dispatcher and every listener the service providers registered was gone for that message. Events fired from WS controllers reached nobody (a shop's "order paid" mails and invoice silently never happened in dev). Model observers kept working, which hid it: Eloquent holds its own reference to the old dispatcher. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Websocket/Handler.php | 12 +++---- tests/HotReloadKeepsListenersTest.php | 47 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 tests/HotReloadKeepsListenersTest.php diff --git a/src/Websocket/Handler.php b/src/Websocket/Handler.php index abd4ea7..aea1775 100644 --- a/src/Websocket/Handler.php +++ b/src/Websocket/Handler.php @@ -521,14 +521,10 @@ class Handler implements MessageComponentInterface // Validator refresh failed, continue anyway } - // 9. Clear event dispatcher cache (for fresh event/listener bindings) - try { - if ($container->bound('events')) { - $container->forgetInstance('events'); - } - } catch (\Throwable $e) { - // Events refresh failed, continue anyway - } + // 9. The event dispatcher is deliberately KEPT: listeners are registered + // once by the service providers (Event::listen in boot()), which never + // run again in this child. A forgotten instance would be rebuilt empty + // and every event fired from the controller would reach no listener. // 10. Clear WebSocket ControllerResolver cache for fresh controller loading ControllerResolver::clearCache(); diff --git a/tests/HotReloadKeepsListenersTest.php b/tests/HotReloadKeepsListenersTest.php new file mode 100644 index 0000000..f731bf5 --- /dev/null +++ b/tests/HotReloadKeepsListenersTest.php @@ -0,0 +1,47 @@ +setHotReload(null); + + parent::tearDown(); + } + + private function setHotReload(?bool $on): void + { + $flag = new \ReflectionProperty(Handler::class, 'hotReload'); + $flag->setAccessible(true); + $flag->setValue(null, $on); + } + + public function test_listeners_registered_at_boot_survive_the_hot_reload() + { + $heard = 0; + Event::listen('shop.order-paid', function () use (&$heard) { + $heard++; + }); + + $this->setHotReload(true); + $reload = new \ReflectionMethod($this->wsHandler, 'hotReloadChild'); + $reload->setAccessible(true); + $reload->invoke($this->wsHandler); + + event('shop.order-paid'); + + $this->assertSame(1, $heard); + } +}