diff --git a/src/Illuminate/Routing/RouteCollection.php b/src/Illuminate/Routing/RouteCollection.php index d3fd6a9d1143..b8eaed5fa72a 100644 --- a/src/Illuminate/Routing/RouteCollection.php +++ b/src/Illuminate/Routing/RouteCollection.php @@ -46,6 +46,9 @@ public function add(Route $route) { $this->addToCollections($route); + // Sort routes by model binding BEFORE adding lookups + $this->sortRoutesByModelBinding(); + $this->addLookups($route); return $route; @@ -266,4 +269,37 @@ public function toCompiledRouteCollection(Router $router, Container $container) ->setRouter($router) ->setContainer($container); } + + /** + * Sort routes by whether they have model binding. + * + * @return void + */ + protected function sortRoutesByModelBinding(): void + { + if (count($this->allRoutes) == 1) return; + + usort($this->allRoutes, function ($routeA, $routeB) { + // Fetch parameter names or bindings for both routes + $bindingsA = $this->extractRouteParameters($routeA); + $bindingsB = $this->extractRouteParameters($routeB); + + + return count($bindingsA) <=> count($bindingsB); + }); + } + + /** + * Helper method to extract route parameters or bindings from the route URI. + * + * @param \Illuminate\Routing\Route $route + * @return array + */ + protected function extractRouteParameters($route): array + { + // Extract parameter placeholders from the URI (e.g., {id}, {slug}) + preg_match_all('/\{[^\}]+\}/', $route->uri(), $matches); + + return $matches[0] ?? []; + } } diff --git a/tests/Routing/RouteCollectionTest.php b/tests/Routing/RouteCollectionTest.php index 1c2517b0f92a..5a30131019a9 100644 --- a/tests/Routing/RouteCollectionTest.php +++ b/tests/Routing/RouteCollectionTest.php @@ -325,4 +325,56 @@ public function testToSymfonyRouteCollection() $this->assertInstanceOf("\Symfony\Component\Routing\RouteCollection", $this->routeCollection->toSymfonyRouteCollection()); } + + public function testRoutesAreSortedByModelBinding() + { + $collection = new RouteCollection(); + + // Create a route with model binding (has parameters) + $routeWithBinding = new Route(['GET'], 'user/{id}', function () {}); + + // Create a route without model binding (no parameters) + $routeWithoutBinding = new Route(['GET'], 'home', function () {}); + + // Add the routes to the collection + $collection->add($routeWithBinding); + $collection->add($routeWithoutBinding); + + // Get the sorted routes from the collection + $sortedRoutes = $collection->getRoutes(); + + // Assert that the route without model binding comes first + $this->assertEquals('home', $sortedRoutes[0]->uri()); + $this->assertEquals('user/{id}', $sortedRoutes[1]->uri()); + } + + /** + * Test that the route order is maintained with multiple routes. + */ + public function testMultipleRoutesSortedCorrectly() + { + $collection = new RouteCollection(); + + // Create multiple routes with and without model binding + $routeWithBinding1 = new Route(['GET'], 'user/{id}', function () {}); + $routeWithBinding2 = new Route(['POST'], 'product/{productId}', function () {}); + $routeWithoutBinding1 = new Route(['GET'], 'about', function () {}); + $routeWithoutBinding2 = new Route(['GET'], 'contact', function () {}); + + // Add the routes to the collection + $collection->add($routeWithBinding1); + $collection->add($routeWithoutBinding1); + $collection->add($routeWithBinding2); + $collection->add($routeWithoutBinding2); + + // Get the sorted routes from the collection + $sortedRoutes = $collection->getRoutes(); + + // Assert that the routes without model bindings come first + $this->assertEquals('about', $sortedRoutes[0]->uri()); + $this->assertEquals('contact', $sortedRoutes[1]->uri()); + $this->assertEquals('user/{id}', $sortedRoutes[2]->uri()); + $this->assertEquals('product/{productId}', $sortedRoutes[3]->uri()); + } + }