Repository: laravel/framework (php)

Issue:
Route model binding behaviour changed in 9.14
- Laravel Version: 9.14.0
- PHP Version: 8.1.6
- Database Driver & Version:

### Description:

I've just discovered a breaking change that appeared between 9.13.0 and 9.14.0. I've got a complex use-case but I've tried to break it down. Here's the route set up that was working in 9.13.0.

```php
Route::get('{university}/{campus:slug}/categories/{category}', [...]);
```

The intention here is that the `campus` record is scoped to the `institution` and that the category is *not scoped to other route models*. However, after upgrading to 9.14.0 it appears Laravel is also trying to scope the category to the campus and blowing up because the association does not exist:

```
Call to undefined method App\Models\Campus::categories()
```

Totally appreciate if perhaps my mixed use of nested routing and non-nested routing is unintended behaviour and that this is actually a fix, but thought it was worth bringing up. I'll roll back to 9.13.0 in the meantime.

### Steps To Reproduce:

I have created a repo here: https://github.com/dwightwatson/routing-bug

If you pull it down and run `php artisan test` it should pass using the SQLite database. If you open `composer.json` and bump to Laravel 9.14.0 and re-run the tests it should fail.

Pull request:
[9.x] Fix regression when only some route parameters are scoped
Fixes #42513

Patch:
diff --git a/src/Illuminate/Routing/ImplicitRouteBinding.php b/src/Illuminate/Routing/ImplicitRouteBinding.php
index 4d75630a0e..8d597559dd 100644
--- a/src/Illuminate/Routing/ImplicitRouteBinding.php
+++ b/src/Illuminate/Routing/ImplicitRouteBinding.php
@@ -46,7 +46,7 @@ public static function resolveForRoute($container, $route)
                         ? 'resolveSoftDeletableRouteBinding'
                         : 'resolveRouteBinding';
 
-            if ($parent instanceof UrlRoutable && ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) {
+            if ($parent instanceof UrlRoutable && ($route->enforcesScopedBindings() || $route->bindingFieldFor($parameterName) !== null)) {
                 $childRouteBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
                             ? 'resolveSoftDeletableChildRouteBinding'
                             : 'resolveChildRouteBinding';


Test patch:
diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php
index 1de71335a4..00c42519f6 100644
--- a/tests/Routing/RoutingRouteTest.php
+++ b/tests/Routing/RoutingRouteTest.php
@@ -1682,6 +1682,30 @@ public function testParentChildImplicitBindings()
         $this->assertSame('1|test-slug', $router->dispatch(Request::create('foo/1/test-slug', 'GET'))->getContent());
     }
 
+    public function testParentChildImplicitBindingsWhereOnlySomeParametersAreScoped()
+    {
+        $router = $this->getRouter();
+        $action = function (RoutingTestTeamModel $team, RoutingTestUserModel $user, RoutingTestPostModel $post) {
+            $this->assertInstanceOf(RoutingTestTeamModel::class, $team);
+            $this->assertInstanceOf(RoutingTestUserModel::class, $user);
+            $this->assertInstanceOf(RoutingTestPostModel::class, $post);
+
+            return $team->value.'|'.$user->value.'|'.$post->value;
+        };
+
+        $router->get('foo/{team}/{user:slug}/{post}', [
+            'middleware' => SubstituteBindings::class,
+            'uses' => $action,
+        ]);
+        $this->assertSame('1|test-slug|2', $router->dispatch(Request::create('foo/1/test-slug/2', 'GET'))->getContent());
+
+        $router->get('foo/{team}/{user}/{post:id}', [
+            'middleware' => SubstituteBindings::class,
+            'uses' => $action,
+        ]);
+        $this->assertSame('2|another-test-slug|3', $router->dispatch(Request::create('foo/2/another-test-slug/3', 'GET'))->getContent());
+    }
+
     public function testParentChildImplicitBindingsProperlyCamelCased()
     {
         $router = $this->getRouter();
@@ -2296,6 +2320,11 @@ public function first()
 
 class RoutingTestTeamModel extends Model
 {
+    public function users()
+    {
+        return new RoutingTestUserModel;
+    }
+
     public function getRouteKeyName()
     {
         return 'id';