diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php index 97ae5fdb..bbb06565 100644 --- a/src/Drivers/LaravelHttpServer.php +++ b/src/Drivers/LaravelHttpServer.php @@ -273,6 +273,10 @@ private function handleRequest(AmpRequest $request): Response $debug = config('app.debug'); + // This server reuses one container across a test's requests, so scoped() bindings must be + // released per request (as FPM and Octane do) — otherwise they leak like singletons. + app()->forgetScopedInstances(); + try { config(['app.debug' => false]); diff --git a/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php b/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php index 7491d6a6..f875d787 100644 --- a/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php +++ b/tests/Unit/Drivers/Laravel/LaravelHttpServerTest.php @@ -36,3 +36,17 @@ visit('/server-variables') ->assertSee('"test-server-key":"test value"'); }); + +it('flushes scoped container bindings between requests', function (): void { + // A scoped() binding is released only by Application::forgetScopedInstances(). Since this + // server reuses one container across a test's requests, it must flush per request — otherwise + // the instance resolved in the first request leaks into the second (and would behave like a + // singleton), unlike FPM or Octane. Each request renders the resolved object's id; they differ. + app()->scoped('scoped-probe', fn (): object => new stdClass); + Route::get('/scoped-probe', fn (): string => spl_object_hash(app('scoped-probe'))); + + $first = visit('/scoped-probe')->content(); + $second = visit('/scoped-probe')->content(); + + expect($first)->not->toBe($second); +});