diff --git a/src/Illuminate/Routing/ImplicitRouteBinding.php b/src/Illuminate/Routing/ImplicitRouteBinding.php index 990a6466dbc0..d5f9488df3e5 100644 --- a/src/Illuminate/Routing/ImplicitRouteBinding.php +++ b/src/Illuminate/Routing/ImplicitRouteBinding.php @@ -41,7 +41,7 @@ public static function resolveForRoute($container, $route) ? 'resolveSoftDeletableRouteBinding' : 'resolveRouteBinding'; - if ($parent instanceof UrlRoutable && in_array($parameterName, array_keys($route->bindingFields()))) { + if ($parent instanceof UrlRoutable && ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) { $childRouteBindingMethod = $route->allowsTrashedBindings() ? 'resolveSoftDeletableChildRouteBinding' : 'resolveChildRouteBinding'; diff --git a/src/Illuminate/Routing/Route.php b/src/Illuminate/Routing/Route.php index a6871521fcd1..5de2f4004604 100755 --- a/src/Illuminate/Routing/Route.php +++ b/src/Illuminate/Routing/Route.php @@ -1077,6 +1077,28 @@ public function excludedMiddleware() return (array) ($this->action['excluded_middleware'] ?? []); } + /** + * Indicate that the route should enforce scoping of multiple implicit Eloquent bindings. + * + * @return bool + */ + public function scopeBindings() + { + $this->action['scope_bindings'] = true; + + return $this; + } + + /** + * Determine if the route should enforce scoping of multiple implicit Eloquent bindings. + * + * @return bool + */ + public function enforcesScopedBindings() + { + return (bool) ($this->action['scope_bindings'] ?? false); + } + /** * Specify that the route should not allow concurrent requests from the same session. * diff --git a/src/Illuminate/Routing/RouteRegistrar.php b/src/Illuminate/Routing/RouteRegistrar.php index ad7f6c0ccafa..d89737497262 100644 --- a/src/Illuminate/Routing/RouteRegistrar.php +++ b/src/Illuminate/Routing/RouteRegistrar.php @@ -55,7 +55,14 @@ class RouteRegistrar * @var string[] */ protected $allowedAttributes = [ - 'as', 'domain', 'middleware', 'name', 'namespace', 'prefix', 'where', + 'as', + 'domain', + 'middleware', + 'name', + 'namespace', + 'prefix', + 'scopeBindings', + 'where', ]; /** @@ -65,6 +72,7 @@ class RouteRegistrar */ protected $aliases = [ 'name' => 'as', + 'scopeBindings' => 'scope_bindings', ]; /** @@ -216,7 +224,7 @@ public function __call($method, $parameters) return $this->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters); } - return $this->attribute($method, $parameters[0]); + return $this->attribute($method, $parameters[0] ?? true); } throw new BadMethodCallException(sprintf( diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index eaecb4873687..241cfdb63e6d 100644 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -1326,6 +1326,6 @@ public function __call($method, $parameters) return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters); } - return (new RouteRegistrar($this))->attribute($method, $parameters[0]); + return (new RouteRegistrar($this))->attribute($method, $parameters[0] ?? true); } } diff --git a/tests/Integration/Routing/ImplicitRouteBindingTest.php b/tests/Integration/Routing/ImplicitRouteBindingTest.php index 987bb0fbad36..a930e7fc5115 100644 --- a/tests/Integration/Routing/ImplicitRouteBindingTest.php +++ b/tests/Integration/Routing/ImplicitRouteBindingTest.php @@ -50,8 +50,15 @@ protected function defineDatabaseMigrations(): void $table->softDeletes(); }); + Schema::create('posts', function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id'); + $table->timestamps(); + }); + $this->beforeApplicationDestroyed(function () { Schema::dropIfExists('users'); + Schema::dropIfExists('posts'); }); } @@ -60,14 +67,14 @@ public function testWithRouteCachingEnabled() $this->defineCacheRoutes(<<middleware('web'); PHP); - $user = ImplicitBindingModel::create(['name' => 'Dries']); + $user = ImplicitBindingUser::create(['name' => 'Dries']); $response = $this->postJson("/user/{$user->id}"); @@ -79,11 +86,11 @@ public function testWithRouteCachingEnabled() public function testWithoutRouteCachingEnabled() { - $user = ImplicitBindingModel::create(['name' => 'Dries']); + $user = ImplicitBindingUser::create(['name' => 'Dries']); config(['app.key' => str_repeat('a', 32)]); - Route::post('/user/{user}', function (ImplicitBindingModel $user) { + Route::post('/user/{user}', function (ImplicitBindingUser $user) { return $user; })->middleware(['web']); @@ -97,13 +104,13 @@ public function testWithoutRouteCachingEnabled() public function testSoftDeletedModelsAreNotRetrieved() { - $user = ImplicitBindingModel::create(['name' => 'Dries']); + $user = ImplicitBindingUser::create(['name' => 'Dries']); $user->delete(); config(['app.key' => str_repeat('a', 32)]); - Route::post('/user/{user}', function (ImplicitBindingModel $user) { + Route::post('/user/{user}', function (ImplicitBindingUser $user) { return $user; })->middleware(['web']); @@ -114,13 +121,13 @@ public function testSoftDeletedModelsAreNotRetrieved() public function testSoftDeletedModelsCanBeRetrievedUsingWithTrashedMethod() { - $user = ImplicitBindingModel::create(['name' => 'Dries']); + $user = ImplicitBindingUser::create(['name' => 'Dries']); $user->delete(); config(['app.key' => str_repeat('a', 32)]); - Route::post('/user/{user}', function (ImplicitBindingModel $user) { + Route::post('/user/{user}', function (ImplicitBindingUser $user) { return $user; })->middleware(['web'])->withTrashed(); @@ -131,13 +138,96 @@ public function testSoftDeletedModelsCanBeRetrievedUsingWithTrashedMethod() 'name' => $user->name, ]); } + + public function testEnforceScopingImplicitRouteBindings() + { + $user = ImplicitBindingUser::create(['name' => 'Dries']); + $post = ImplicitBindingPost::create(['user_id' => 2]); + $this->assertEmpty($user->posts); + + config(['app.key' => str_repeat('a', 32)]); + + Route::scopeBindings()->group(function () { + Route::get('/user/{user}/post/{post}', function (ImplicitBindingUser $user, ImplicitBindingPost $post) { + return [$user, $post]; + })->middleware(['web']); + }); + + $response = $this->getJson("/user/{$user->id}/post/{$post->id}"); + + $response->assertNotFound(); + } + + public function testEnforceScopingImplicitRouteBindingsWithRouteCachingEnabled() + { + $user = ImplicitBindingUser::create(['name' => 'Dries']); + $post = ImplicitBindingPost::create(['user_id' => 2]); + $this->assertEmpty($user->posts); + + $this->defineCacheRoutes(<< true], function () { + Route::get('/user/{user}/post/{post}', function (ImplicitBindingUser \$user, ImplicitBindingPost \$post) { + return [\$user, \$post]; + })->middleware(['web']); +}); +PHP); + + $response = $this->getJson("/user/{$user->id}/post/{$post->id}"); + + $response->assertNotFound(); + } + + public function testWithoutEnforceScopingImplicitRouteBindings() + { + $user = ImplicitBindingUser::create(['name' => 'Dries']); + $post = ImplicitBindingPost::create(['user_id' => 2]); + $this->assertEmpty($user->posts); + + config(['app.key' => str_repeat('a', 32)]); + + Route::group(['scoping' => false], function () { + Route::get('/user/{user}/post/{post}', function (ImplicitBindingUser $user, ImplicitBindingPost $post) { + return [$user, $post]; + })->middleware(['web']); + }); + + $response = $this->getJson("/user/{$user->id}/post/{$post->id}"); + $response->assertOk(); + $response->assertJson([ + [ + 'id' => $user->id, + 'name' => $user->name, + ], + [ + 'id' => 1, + 'user_id' => 2, + ], + ]); + } } -class ImplicitBindingModel extends Model +class ImplicitBindingUser extends Model { use SoftDeletes; public $table = 'users'; protected $fillable = ['name']; + + public function posts() + { + return $this->hasMany(ImplicitBindingPost::class, 'user_id'); + } +} + +class ImplicitBindingPost extends Model +{ + public $table = 'posts'; + + protected $fillable = ['user_id']; }