From 418fae4730fc4522bc891f736dad334084a58ff2 Mon Sep 17 00:00:00 2001 From: Michael Darko Date: Fri, 16 Oct 2026 00:56:55 +0000 Subject: [PATCH] fix: resolve named middleware lazily so registration order does not drop it --- src/Router.php | 46 +++++++++++++++++++++++++++------ tests/middleware.test.php | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/src/Router.php b/src/Router.php index 5e3963b..ea3b05f 100644 --- a/src/Router.php +++ b/src/Router.php @@ -668,20 +668,14 @@ protected static function mapHandler( if (is_array($handler)) { if (is_string($handler['middleware'] ?? null)) { - $parsedOptions['middleware'] = (class_exists($handler['middleware'])) ? function () use ($handler) { - (new $handler['middleware']())->call(); - } : static::$namedMiddleware[$handler['middleware']] ?? null; + $parsedOptions['middleware'] = static::resolveMiddleware($handler['middleware']); } if (is_array($handler['middleware'] ?? null)) { $parsedOptions['middleware'] = []; foreach ($handler['middleware'] as $middleware) { - if (is_string($middleware)) { - $parsedOptions['middleware'][] = static::$namedMiddleware[$middleware] ?? null; - } else { - $parsedOptions['middleware'][] = $middleware; - } + $parsedOptions['middleware'][] = static::resolveMiddleware($middleware); } } @@ -805,6 +799,42 @@ public static function use($middleware) } } + /** + * Turn a middleware reference into something invokable. + * + * Class names are wrapped so the class is instantiated and called + * at request time. Named middleware is resolved lazily, so it does + * not matter whether registerMiddleware() runs before or after the + * routes that use it. A name that is still unknown when the request + * hits the route throws instead of silently letting the route run. + * + * @param callable|string $middleware A callable, a class name or a registered middleware name + * + * @return callable + */ + protected static function resolveMiddleware($middleware): callable + { + if (!is_string($middleware)) { + return $middleware; + } + + if (class_exists($middleware)) { + return function () use ($middleware) { + (new $middleware())->call(); + }; + } + + return function (...$params) use ($middleware) { + if (!isset(static::$namedMiddleware[$middleware])) { + throw new \RuntimeException( + "Middleware named '$middleware' not found. Register it with registerMiddleware() before the request is handled." + ); + } + + return static::$namedMiddleware[$middleware](...$params); + }; + } + /** * Register a middleware in your Leaf application by name * diff --git a/tests/middleware.test.php b/tests/middleware.test.php index 01a2dd9..35d30a0 100644 --- a/tests/middleware.test.php +++ b/tests/middleware.test.php @@ -163,3 +163,56 @@ class StaticTestClassMid expect($ran['global'])->toBeTrue() ->and($ran['group'])->toBeTrue(); }); + +test('named middleware registered after the routes still runs', function () { + \Leaf\Router::reset(); + app()->setBasePath('/'); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/late/42'; + + $ran = ['string' => false, 'array' => false, 'group' => false]; + + app()->get('/late/{id}', ['middleware' => 'late.string', function () {}]); + app()->get('/late-array/{id}', ['middleware' => ['late.array'], function () {}]); + app()->group('/late-group', ['middleware' => 'late.group', function () { + app()->get('/{id}', function () {}); + }]); + + // registered AFTER the routes above were declared + app()->registerMiddleware('late.string', function () use (&$ran) { + $ran['string'] = true; + }); + app()->registerMiddleware('late.array', function () use (&$ran) { + $ran['array'] = true; + }); + app()->registerMiddleware('late.group', function () use (&$ran) { + $ran['group'] = true; + }); + + app()->run(); + expect($ran['string'])->toBeTrue(); + + $_SERVER['REQUEST_URI'] = '/late-array/42'; + app()->run(); + expect($ran['array'])->toBeTrue(); + + $_SERVER['REQUEST_URI'] = '/late-group/42'; + app()->run(); + expect($ran['group'])->toBeTrue(); +}); + +test('a route using an unregistered named middleware fails closed', function () { + \Leaf\Router::reset(); + app()->setBasePath('/'); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/never'; + + $handlerRan = false; + + app()->get('/never', ['middleware' => 'never.registered', function () use (&$handlerRan) { + $handlerRan = true; + }]); + + expect(fn () => app()->run())->toThrow(RuntimeException::class, 'never.registered'); + expect($handlerRan)->toBeFalse(); +});