Skip to content

Commit d1093f5

Browse files
[8.x] Enforce implicit Route Model scoping (#39440)
* Ability to enforce implicit scoping * Change to Integration Test * allow fluent scoping method Co-authored-by: Taylor Otwell <[email protected]>
1 parent 5e837d5 commit d1093f5

File tree

5 files changed

+134
-14
lines changed

5 files changed

+134
-14
lines changed

src/Illuminate/Routing/ImplicitRouteBinding.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public static function resolveForRoute($container, $route)
4141
? 'resolveSoftDeletableRouteBinding'
4242
: 'resolveRouteBinding';
4343

44-
if ($parent instanceof UrlRoutable && in_array($parameterName, array_keys($route->bindingFields()))) {
44+
if ($parent instanceof UrlRoutable && ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) {
4545
$childRouteBindingMethod = $route->allowsTrashedBindings()
4646
? 'resolveSoftDeletableChildRouteBinding'
4747
: 'resolveChildRouteBinding';

src/Illuminate/Routing/Route.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,28 @@ public function excludedMiddleware()
10951095
return (array) ($this->action['excluded_middleware'] ?? []);
10961096
}
10971097

1098+
/**
1099+
* Indicate that the route should enforce scoping of multiple implicit Eloquent bindings.
1100+
*
1101+
* @return bool
1102+
*/
1103+
public function scopeBindings()
1104+
{
1105+
$this->action['scope_bindings'] = true;
1106+
1107+
return $this;
1108+
}
1109+
1110+
/**
1111+
* Determine if the route should enforce scoping of multiple implicit Eloquent bindings.
1112+
*
1113+
* @return bool
1114+
*/
1115+
public function enforcesScopedBindings()
1116+
{
1117+
return (bool) ($this->action['scope_bindings'] ?? false);
1118+
}
1119+
10981120
/**
10991121
* Specify that the route should not allow concurrent requests from the same session.
11001122
*

src/Illuminate/Routing/RouteRegistrar.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,14 @@ class RouteRegistrar
5555
* @var string[]
5656
*/
5757
protected $allowedAttributes = [
58-
'as', 'domain', 'middleware', 'name', 'namespace', 'prefix', 'where',
58+
'as',
59+
'domain',
60+
'middleware',
61+
'name',
62+
'namespace',
63+
'prefix',
64+
'scopeBindings',
65+
'where',
5966
];
6067

6168
/**
@@ -65,6 +72,7 @@ class RouteRegistrar
6572
*/
6673
protected $aliases = [
6774
'name' => 'as',
75+
'scopeBindings' => 'scope_bindings',
6876
];
6977

7078
/**
@@ -222,7 +230,7 @@ public function __call($method, $parameters)
222230
return $this->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters);
223231
}
224232

225-
return $this->attribute($method, $parameters[0]);
233+
return $this->attribute($method, $parameters[0] ?? true);
226234
}
227235

228236
throw new BadMethodCallException(sprintf(

src/Illuminate/Routing/Router.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1326,6 +1326,6 @@ public function __call($method, $parameters)
13261326
return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters);
13271327
}
13281328

1329-
return (new RouteRegistrar($this))->attribute($method, $parameters[0]);
1329+
return (new RouteRegistrar($this))->attribute($method, $parameters[0] ?? true);
13301330
}
13311331
}

tests/Integration/Routing/ImplicitRouteBindingTest.php

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,15 @@ protected function defineDatabaseMigrations(): void
5050
$table->softDeletes();
5151
});
5252

53+
Schema::create('posts', function (Blueprint $table) {
54+
$table->increments('id');
55+
$table->integer('user_id');
56+
$table->timestamps();
57+
});
58+
5359
$this->beforeApplicationDestroyed(function () {
5460
Schema::dropIfExists('users');
61+
Schema::dropIfExists('posts');
5562
});
5663
}
5764

@@ -60,14 +67,14 @@ public function testWithRouteCachingEnabled()
6067
$this->defineCacheRoutes(<<<PHP
6168
<?php
6269
63-
use Illuminate\Tests\Integration\Routing\ImplicitBindingModel;
70+
use Illuminate\Tests\Integration\Routing\ImplicitBindingUser;
6471
65-
Route::post('/user/{user}', function (ImplicitBindingModel \$user) {
72+
Route::post('/user/{user}', function (ImplicitBindingUser \$user) {
6673
return \$user;
6774
})->middleware('web');
6875
PHP);
6976

70-
$user = ImplicitBindingModel::create(['name' => 'Dries']);
77+
$user = ImplicitBindingUser::create(['name' => 'Dries']);
7178

7279
$response = $this->postJson("/user/{$user->id}");
7380

@@ -79,11 +86,11 @@ public function testWithRouteCachingEnabled()
7986

8087
public function testWithoutRouteCachingEnabled()
8188
{
82-
$user = ImplicitBindingModel::create(['name' => 'Dries']);
89+
$user = ImplicitBindingUser::create(['name' => 'Dries']);
8390

8491
config(['app.key' => str_repeat('a', 32)]);
8592

86-
Route::post('/user/{user}', function (ImplicitBindingModel $user) {
93+
Route::post('/user/{user}', function (ImplicitBindingUser $user) {
8794
return $user;
8895
})->middleware(['web']);
8996

@@ -97,13 +104,13 @@ public function testWithoutRouteCachingEnabled()
97104

98105
public function testSoftDeletedModelsAreNotRetrieved()
99106
{
100-
$user = ImplicitBindingModel::create(['name' => 'Dries']);
107+
$user = ImplicitBindingUser::create(['name' => 'Dries']);
101108

102109
$user->delete();
103110

104111
config(['app.key' => str_repeat('a', 32)]);
105112

106-
Route::post('/user/{user}', function (ImplicitBindingModel $user) {
113+
Route::post('/user/{user}', function (ImplicitBindingUser $user) {
107114
return $user;
108115
})->middleware(['web']);
109116

@@ -114,13 +121,13 @@ public function testSoftDeletedModelsAreNotRetrieved()
114121

115122
public function testSoftDeletedModelsCanBeRetrievedUsingWithTrashedMethod()
116123
{
117-
$user = ImplicitBindingModel::create(['name' => 'Dries']);
124+
$user = ImplicitBindingUser::create(['name' => 'Dries']);
118125

119126
$user->delete();
120127

121128
config(['app.key' => str_repeat('a', 32)]);
122129

123-
Route::post('/user/{user}', function (ImplicitBindingModel $user) {
130+
Route::post('/user/{user}', function (ImplicitBindingUser $user) {
124131
return $user;
125132
})->middleware(['web'])->withTrashed();
126133

@@ -131,13 +138,96 @@ public function testSoftDeletedModelsCanBeRetrievedUsingWithTrashedMethod()
131138
'name' => $user->name,
132139
]);
133140
}
141+
142+
public function testEnforceScopingImplicitRouteBindings()
143+
{
144+
$user = ImplicitBindingUser::create(['name' => 'Dries']);
145+
$post = ImplicitBindingPost::create(['user_id' => 2]);
146+
$this->assertEmpty($user->posts);
147+
148+
config(['app.key' => str_repeat('a', 32)]);
149+
150+
Route::scopeBindings()->group(function () {
151+
Route::get('/user/{user}/post/{post}', function (ImplicitBindingUser $user, ImplicitBindingPost $post) {
152+
return [$user, $post];
153+
})->middleware(['web']);
154+
});
155+
156+
$response = $this->getJson("/user/{$user->id}/post/{$post->id}");
157+
158+
$response->assertNotFound();
159+
}
160+
161+
public function testEnforceScopingImplicitRouteBindingsWithRouteCachingEnabled()
162+
{
163+
$user = ImplicitBindingUser::create(['name' => 'Dries']);
164+
$post = ImplicitBindingPost::create(['user_id' => 2]);
165+
$this->assertEmpty($user->posts);
166+
167+
$this->defineCacheRoutes(<<<PHP
168+
<?php
169+
170+
use Illuminate\Tests\Integration\Routing\ImplicitBindingUser;
171+
use Illuminate\Tests\Integration\Routing\ImplicitBindingPost;
172+
173+
Route::group(['scoping' => true], function () {
174+
Route::get('/user/{user}/post/{post}', function (ImplicitBindingUser \$user, ImplicitBindingPost \$post) {
175+
return [\$user, \$post];
176+
})->middleware(['web']);
177+
});
178+
PHP);
179+
180+
$response = $this->getJson("/user/{$user->id}/post/{$post->id}");
181+
182+
$response->assertNotFound();
183+
}
184+
185+
public function testWithoutEnforceScopingImplicitRouteBindings()
186+
{
187+
$user = ImplicitBindingUser::create(['name' => 'Dries']);
188+
$post = ImplicitBindingPost::create(['user_id' => 2]);
189+
$this->assertEmpty($user->posts);
190+
191+
config(['app.key' => str_repeat('a', 32)]);
192+
193+
Route::group(['scoping' => false], function () {
194+
Route::get('/user/{user}/post/{post}', function (ImplicitBindingUser $user, ImplicitBindingPost $post) {
195+
return [$user, $post];
196+
})->middleware(['web']);
197+
});
198+
199+
$response = $this->getJson("/user/{$user->id}/post/{$post->id}");
200+
$response->assertOk();
201+
$response->assertJson([
202+
[
203+
'id' => $user->id,
204+
'name' => $user->name,
205+
],
206+
[
207+
'id' => 1,
208+
'user_id' => 2,
209+
],
210+
]);
211+
}
134212
}
135213

136-
class ImplicitBindingModel extends Model
214+
class ImplicitBindingUser extends Model
137215
{
138216
use SoftDeletes;
139217

140218
public $table = 'users';
141219

142220
protected $fillable = ['name'];
221+
222+
public function posts()
223+
{
224+
return $this->hasMany(ImplicitBindingPost::class, 'user_id');
225+
}
226+
}
227+
228+
class ImplicitBindingPost extends Model
229+
{
230+
public $table = 'posts';
231+
232+
protected $fillable = ['user_id'];
143233
}

0 commit comments

Comments
 (0)