diff --git a/src/Illuminate/Routing/Controller.php b/src/Illuminate/Routing/Controller.php index 18032ca6..d3bfa117 100644 --- a/src/Illuminate/Routing/Controller.php +++ b/src/Illuminate/Routing/Controller.php @@ -33,6 +33,23 @@ abstract class Controller { */ protected $layout; + /** + * Register middleware on the controller (L13 idiom). + * + * BC bridge: middleware registered here runs via the legacy controller-filter + * path (ControllerDispatcher) until the routing engine is swapped to stock L13. + * The signature matches L13's Controller::middleware($middleware, array $options), + * so the '['only'=>..]'/'['except'=>..]' options carry over unchanged after the swap. + * + * @param \Closure|string $middleware + * @param array $options + * @return void + */ + public function middleware($middleware, array $options = array()) + { + $this->beforeFilter($middleware, $options); + } + /** * Register a "before" filter on the controller. * diff --git a/src/Illuminate/Routing/MiddlewareNameResolver.php b/src/Illuminate/Routing/MiddlewareNameResolver.php new file mode 100644 index 00000000..0167b61f --- /dev/null +++ b/src/Illuminate/Routing/MiddlewareNameResolver.php @@ -0,0 +1,93 @@ +action['uses'], array_values($parameters)); } + /** + * Get or set the middlewares attached to the route. + * + * @param array|string|null $middleware + * @return $this|array + */ + public function middleware($middleware = null) + { + if (is_null($middleware)) + { + return (array) ($this->action['middleware'] ?? []); + } + + if ( ! is_array($middleware)) + { + $middleware = func_get_args(); + } + + foreach ($middleware as $index => $value) + { + $middleware[$index] = $value instanceof \Closure ? $value : (string) $value; + } + + $this->action['middleware'] = array_merge( + (array) ($this->action['middleware'] ?? []), $middleware + ); + + return $this; + } + + /** + * Get all middleware, including the ones from the controller. + * + * @return array + */ + public function gatherMiddleware() + { + return Router::uniqueMiddleware($this->middleware()); + } + /** * Determine if the route matches given request. * diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index bb6581c4..517a5abf 100755 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -5,6 +5,8 @@ use Illuminate\Http\Response; use Illuminate\Events\Dispatcher; use Illuminate\Container\Container; +use Illuminate\Pipeline\Pipeline; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; @@ -75,6 +77,27 @@ class Router implements HttpKernelInterface, RouteFiltererInterface { */ protected array $regexFilters = array(); + /** + * All of the short-hand keys for middlewares (L13 route-middleware engine). + * + * @var array + */ + protected array $middleware = array(); + + /** + * All of the middleware groups. + * + * @var array + */ + protected array $middlewareGroups = array(); + + /** + * The priority-sorted list of middleware. + * + * @var array + */ + protected array $middlewarePriority = array(); + /** * The registered route value binders. * @@ -1066,7 +1089,7 @@ public function dispatchToRoute(Request $request) if (is_null($response)) { - $response = $route->run($request); + $response = $this->runRouteWithinStack($route, $request); } $response = $this->prepareResponse($request, $response); @@ -1079,6 +1102,143 @@ public function dispatchToRoute(Request $request) return $response; } + /** + * Run the matched route through its middleware stack, then execute it. + * + * L13 route-middleware runs here, INSIDE the legacy before/after filter + * bracket. Routes without middleware short-circuit to a plain run() so + * existing (filter-only) routes are byte-for-byte unaffected. + * + * @param Route $route + * @param Request $request + * @return mixed + */ + protected function runRouteWithinStack(Route $route, Request $request) + { + $middleware = $this->gatherRouteMiddleware($route); + + if (empty($middleware)) + { + return $route->run(); + } + + return (new Pipeline($this->container)) + ->send($request) + ->through($middleware) + ->then(function() use ($route) + { + return $route->run(); + }); + } + + /** + * Gather the middleware for the given route with resolved class names. + * + * @param Route $route + * @return array + */ + public function gatherRouteMiddleware(Route $route) + { + return $this->resolveMiddleware($route->gatherMiddleware()); + } + + /** + * Resolve a flat array of middleware classes from names/aliases/groups. + * + * @param array $middleware + * @return array + */ + public function resolveMiddleware(array $middleware) + { + $resolved = (new Collection($middleware)) + ->map(function($name) + { + return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups); + }) + ->flatten() + ->values() + ->all(); + + return $this->sortMiddleware($resolved); + } + + /** + * Sort the given middleware by priority. + * + * @param array $middleware + * @return array + */ + protected function sortMiddleware(array $middleware) + { + return (new SortedMiddleware($this->middlewarePriority, $middleware))->all(); + } + + /** + * Register a short-hand name for a middleware. + * + * @param string $name + * @param string $class + * @return $this + */ + public function aliasMiddleware($name, $class) + { + $this->middleware[$name] = $class; + + return $this; + } + + /** + * Register a group of middleware. + * + * @param string $name + * @param array $middleware + * @return $this + */ + public function middlewareGroup($name, array $middleware) + { + $this->middlewareGroups[$name] = $middleware; + + return $this; + } + + /** + * Set the priority-sorted list of middleware. + * + * @param array $middleware + * @return $this + */ + public function middlewarePriority(array $middleware) + { + $this->middlewarePriority = $middleware; + + return $this; + } + + /** + * Remove any duplicate middleware from the given array. + * + * @param array $middleware + * @return array + */ + public static function uniqueMiddleware(array $middleware) + { + $seen = array(); + $result = array(); + + foreach ($middleware as $value) + { + $key = is_object($value) ? spl_object_id($value) : $value; + + if ( ! isset($seen[$key])) + { + $seen[$key] = true; + $result[] = $value; + } + } + + return $result; + } + /** * Find the route matching a given request. * diff --git a/src/Illuminate/Routing/SortedMiddleware.php b/src/Illuminate/Routing/SortedMiddleware.php new file mode 100644 index 00000000..2fa2e3c5 --- /dev/null +++ b/src/Illuminate/Routing/SortedMiddleware.php @@ -0,0 +1,129 @@ +all(); + } + + $this->items = $this->sortMiddleware($priorityMap, $middlewares); + } + + /** + * Sort the middlewares by the given priority map. + * + * Each call to this method makes one discrete middleware movement if necessary. + * + * @param array $priorityMap + * @param array $middlewares + * @return array + */ + protected function sortMiddleware($priorityMap, $middlewares) + { + $lastIndex = 0; + + foreach ($middlewares as $index => $middleware) { + if (! is_string($middleware)) { + continue; + } + + $priorityIndex = $this->priorityMapIndex($priorityMap, $middleware); + + if (! is_null($priorityIndex)) { + // This middleware is in the priority map. If we have encountered another middleware + // that was also in the priority map and was at a lower priority than the current + // middleware, we will move this middleware to be above the previous encounter. + if (isset($lastPriorityIndex) && $priorityIndex < $lastPriorityIndex) { + return $this->sortMiddleware( + $priorityMap, array_values($this->moveMiddleware($middlewares, $index, $lastIndex)) + ); + } + + // This middleware is in the priority map; but, this is the first middleware we have + // encountered from the map thus far. We'll save its current index plus its index + // from the priority map so we can compare against them on the next iterations. + $lastIndex = $index; + + $lastPriorityIndex = $priorityIndex; + } + } + + return Router::uniqueMiddleware($middlewares); + } + + /** + * Calculate the priority map index of the middleware. + * + * @param array $priorityMap + * @param string $middleware + * @return int|null + */ + protected function priorityMapIndex($priorityMap, $middleware) + { + foreach ($this->middlewareNames($middleware) as $name) { + $priorityIndex = array_search($name, $priorityMap); + + if ($priorityIndex !== false) { + return $priorityIndex; + } + } + } + + /** + * Resolve the middleware names to look for in the priority array. + * + * @param string $middleware + * @return \Generator + */ + protected function middlewareNames($middleware) + { + $stripped = head(explode(':', $middleware)); + + yield $stripped; + + $interfaces = @class_implements($stripped); + + if ($interfaces !== false) { + foreach ($interfaces as $interface) { + yield $interface; + } + } + + $parents = @class_parents($stripped); + + if ($parents !== false) { + foreach ($parents as $parent) { + yield $parent; + } + } + } + + /** + * Splice a middleware into a new position and remove the old entry. + * + * @param array $middlewares + * @param int $from + * @param int $to + * @return array + */ + protected function moveMiddleware($middlewares, $from, $to) + { + array_splice($middlewares, $to, 0, $middlewares[$from]); + + unset($middlewares[$from + 1]); + + return $middlewares; + } +} diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index 66f11bbe..64dec601 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -913,6 +913,57 @@ public function testDispatchingCallableActionClasses() } + public function testRouteMiddlewareRunsAndWraps(): void + { + // Closure middleware wraps the route response. + $router = $this->getRouter(); + $router->get('foo/bar', fn() => 'hello')->middleware(fn($request, $next) => $next($request).'!'); + $this->assertEquals('hello!', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent()); + + // Middleware that never calls $next short-circuits the route. + $router = $this->getRouter(); + $router->get('foo/bar', fn() => 'hello')->middleware(fn($request, $next) => 'blocked'); + $this->assertEquals('blocked', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent()); + + // Alias resolves to a class middleware. + $router = $this->getRouter(); + $router->aliasMiddleware('stub', 'RouteMiddlewareStub'); + $router->get('foo/bar', fn() => 'hello')->middleware('stub'); + $this->assertEquals('hello-stubbed', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent()); + + // Parameter after ':' is passed to the middleware. + $router = $this->getRouter(); + $router->aliasMiddleware('append', 'RouteAppendMiddlewareStub'); + $router->get('foo/bar', fn() => 'hello')->middleware('append:X'); + $this->assertEquals('helloX', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent()); + + // A group expands to its members, nesting outer->inner. + $router = $this->getRouter(); + $router->aliasMiddleware('append', 'RouteAppendMiddlewareStub'); + $router->middlewareGroup('grp', ['append:-a', 'append:-b']); + $router->get('foo/bar', fn() => 'x')->middleware('grp'); + $this->assertEquals('x-b-a', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent()); + } + + + public function testRouteMiddlewareCoexistsWithFilters(): void + { + // A before filter that short-circuits still bypasses middleware (filters wrap middleware). + $router = $this->getRouter(); + $router->filter('block', fn() => 'blocked-by-filter'); + $router->get('foo/bar', ['before' => 'block', fn() => 'hello']) + ->middleware(fn($request, $next) => $next($request).'+mw'); + $this->assertEquals('blocked-by-filter', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent()); + + // A pass-through before filter lets the route run; middleware still wraps it. + $router = $this->getRouter(); + $router->filter('pass', fn() => null); + $router->get('foo/bar', ['before' => 'pass', fn() => 'hello']) + ->middleware(fn($request, $next) => $next($request).'+mw'); + $this->assertEquals('hello+mw', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent()); + } + + protected function getRouter(): Router { return new Router(new Illuminate\Events\Dispatcher); @@ -1017,3 +1068,17 @@ public function __invoke(): string return 'hello'; } } + +class RouteMiddlewareStub { + public function handle($request, $next) + { + return $next($request).'-stubbed'; + } +} + +class RouteAppendMiddlewareStub { + public function handle($request, $next, $suffix) + { + return $next($request).$suffix; + } +}