Skip to content

Commit 95fd374

Browse files
authored
[11.x] Prevent infinite recursion when touching chaperoned models (#52883)
1 parent e216708 commit 95fd374

File tree

2 files changed

+93
-8
lines changed

2 files changed

+93
-8
lines changed

src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -792,17 +792,19 @@ public function touches($relation)
792792
*/
793793
public function touchOwners()
794794
{
795-
foreach ($this->getTouchedRelations() as $relation) {
796-
$this->$relation()->touch();
795+
$this->withoutRecursion(function () {
796+
foreach ($this->getTouchedRelations() as $relation) {
797+
$this->$relation()->touch();
797798

798-
if ($this->$relation instanceof self) {
799-
$this->$relation->fireModelEvent('saved', false);
799+
if ($this->$relation instanceof self) {
800+
$this->$relation->fireModelEvent('saved', false);
800801

801-
$this->$relation->touchOwners();
802-
} elseif ($this->$relation instanceof Collection) {
803-
$this->$relation->each->touchOwners();
802+
$this->$relation->touchOwners();
803+
} elseif ($this->$relation instanceof Collection) {
804+
$this->$relation->each->touchOwners();
805+
}
804806
}
805-
}
807+
});
806808
}
807809

808810
/**

tests/Database/DatabaseEloquentIntegrationTest.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ protected function createSchema()
152152
$table->morphs('taggable');
153153
$table->string('taxonomy')->nullable();
154154
});
155+
156+
$this->schema($connection)->create('categories', function ($table) {
157+
$table->increments('id');
158+
$table->string('name');
159+
$table->integer('parent_id')->nullable();
160+
$table->timestamps();
161+
});
155162
}
156163

157164
$this->schema($connection)->create('non_incrementing_users', function ($table) {
@@ -2182,6 +2189,61 @@ public function testMorphPivotsCanBeRefreshed()
21822189
$this->assertSame('primary', $pivot->taxonomy);
21832190
}
21842191

2192+
public function testTouchingChaperonedChildModelUpdatesParentTimestamps()
2193+
{
2194+
$before = Carbon::now();
2195+
2196+
$one = EloquentTouchingCategory::create(['id' => 1, 'name' => 'One']);
2197+
$two = $one->children()->create(['id' => 2, 'name' => 'Two']);
2198+
2199+
$this->assertTrue($before->isSameDay($one->updated_at));
2200+
$this->assertTrue($before->isSameDay($two->updated_at));
2201+
2202+
Carbon::setTestNow($future = $before->copy()->addDays(3));
2203+
2204+
$two->touch();
2205+
2206+
$this->assertTrue($future->isSameDay($two->fresh()->updated_at), 'It is not touching model own timestamps.');
2207+
$this->assertTrue($future->isSameDay($one->fresh()->updated_at), 'It is not touching chaperoned models related timestamps.');
2208+
}
2209+
2210+
public function testTouchingBiDirectionalChaperonedModelUpdatesAllRelatedTimestamps()
2211+
{
2212+
$before = Carbon::now();
2213+
2214+
EloquentTouchingCategory::insert([
2215+
['id' => 1, 'name' => 'One', 'parent_id' => null, 'created_at' => $before, 'updated_at' => $before],
2216+
['id' => 2, 'name' => 'Two', 'parent_id' => 1, 'created_at' => $before, 'updated_at' => $before],
2217+
['id' => 3, 'name' => 'Three', 'parent_id' => 1, 'created_at' => $before, 'updated_at' => $before],
2218+
['id' => 4, 'name' => 'Four', 'parent_id' => 2, 'created_at' => $before, 'updated_at' => $before],
2219+
]);
2220+
2221+
$one = EloquentTouchingCategory::find(1);
2222+
[$two, $three] = $one->children;
2223+
[$four] = $two->children;
2224+
2225+
$this->assertTrue($before->isSameDay($one->updated_at));
2226+
$this->assertTrue($before->isSameDay($two->updated_at));
2227+
$this->assertTrue($before->isSameDay($three->updated_at));
2228+
$this->assertTrue($before->isSameDay($four->updated_at));
2229+
2230+
Carbon::setTestNow($future = $before->copy()->addDays(3));
2231+
2232+
// Touch a random model and check that all of the others have been updated
2233+
$models = tap([$one, $two, $three, $four], shuffle(...));
2234+
$target = array_shift($models);
2235+
$target->touch();
2236+
2237+
$this->assertTrue($future->isSameDay($target->fresh()->updated_at), 'It is not touching model own timestamps.');
2238+
2239+
while ($next = array_shift($models)) {
2240+
$this->assertTrue(
2241+
$future->isSameDay($next->fresh()->updated_at),
2242+
'It is not touching related models timestamps.'
2243+
);
2244+
}
2245+
}
2246+
21852247
/**
21862248
* Helpers...
21872249
*/
@@ -2486,3 +2548,24 @@ public function post()
24862548
return $this->belongsTo(EloquentTouchingPost::class, 'post_id');
24872549
}
24882550
}
2551+
2552+
class EloquentTouchingCategory extends Eloquent
2553+
{
2554+
protected $table = 'categories';
2555+
protected $guarded = [];
2556+
2557+
protected $touches = [
2558+
'parent',
2559+
'children',
2560+
];
2561+
2562+
public function parent()
2563+
{
2564+
return $this->belongsTo(EloquentTouchingCategory::class, 'parent_id');
2565+
}
2566+
2567+
public function children()
2568+
{
2569+
return $this->hasMany(EloquentTouchingCategory::class, 'parent_id')->chaperone();
2570+
}
2571+
}

0 commit comments

Comments
 (0)