diff --git a/composer.json b/composer.json index 6b81a2834..005084c33 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ ], "require": { "php": "^8.0", - "laravel/framework": "^8.81|^9.0", + "laravel/framework": "^8.83.25|^9.33", "laminas/laminas-diactoros": "^2.5", "laravel/serializable-closure": "^1.0", "nesbot/carbon": "^2.60", diff --git a/src/ApplicationGateway.php b/src/ApplicationGateway.php index 6679c4a82..01d19bc87 100644 --- a/src/ApplicationGateway.php +++ b/src/ApplicationGateway.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Http\Kernel; use Illuminate\Foundation\Application; use Illuminate\Http\Request; +use Illuminate\Routing\Route; use Laravel\Octane\Events\RequestHandled; use Laravel\Octane\Events\RequestReceived; use Laravel\Octane\Events\RequestTerminated; @@ -50,5 +51,11 @@ public function terminate(Request $request, Response $response): void $this->sandbox->make(Kernel::class)->terminate($request, $response); $this->dispatchEvent($this->sandbox, new RequestTerminated($this->app, $this->sandbox, $request, $response)); + + $route = $request->route(); + + if ($route instanceof Route && method_exists($route, 'flushController')) { + $route->flushController(); + } } } diff --git a/tests/RequestStateTest.php b/tests/RequestStateTest.php index 73d207a4d..563528258 100644 --- a/tests/RequestStateTest.php +++ b/tests/RequestStateTest.php @@ -49,6 +49,67 @@ public function test_form_requests_contain_the_correct_state_across_subsequent_r $this->assertEquals('Abigail', $client->responses[1]->original['name']); $this->assertNotEquals($client->responses[0]->original['container'], $client->responses[1]->original['container']); } + + public function test_request_routes_flush_controller_state() + { + [$app, $worker, $client] = $this->createOctaneContext([ + Request::create('/users', 'GET'), + Request::create('/users', 'GET'), + ]); + + $app['router']->get('/users', UserControllerStub::class); + + $worker->run(); + + $this->assertEquals(1, $client->responses[0]->original); + $this->assertEquals(1, $client->responses[1]->original); + + $worker->run(); + + $this->assertEquals(1, $client->responses[0]->original); + $this->assertEquals(1, $client->responses[1]->original); + } + + public function test_request_routes_controller_does_not_leak() + { + UserControllerStub::$destroyedCount = 0; + + [$app, $worker, $client] = $this->createOctaneContext([ + Request::create('/users', 'GET'), + Request::create('/users', 'GET'), + ]); + + $app['router']->get('/users', UserControllerStub::class); + + $worker->run(); + + gc_collect_cycles(); + $this->assertEquals(2, UserControllerStub::$destroyedCount); + + $worker->run(); + + gc_collect_cycles(); + $this->assertEquals(4, UserControllerStub::$destroyedCount); + } +} + +class UserControllerStub +{ + protected $invokedCount = 0; + + public static $destroyedCount = 0; + + public function __invoke() + { + $this->invokedCount++; + + return $this->invokedCount; + } + + public function __destruct() + { + static::$destroyedCount++; + } } class RequestStateTestFormRequest extends FormRequest