Skip to content

Commit

Permalink
[11.x] Prevent recursion when touching chaperoned models
Browse files Browse the repository at this point in the history
  • Loading branch information
samlev committed Sep 20, 2024
1 parent bbdca76 commit 35ae45c
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 19 deletions.
27 changes: 16 additions & 11 deletions src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -792,17 +792,22 @@ public function touches($relation)
*/
public function touchOwners()
{
foreach ($this->getTouchedRelations() as $relation) {
$this->$relation()->touch();

if ($this->$relation instanceof self) {
$this->$relation->fireModelEvent('saved', false);

$this->$relation->touchOwners();
} elseif ($this->$relation instanceof Collection) {
$this->$relation->each->touchOwners();
}
}
without_recursion(
function () {
foreach ($this->getTouchedRelations() as $relation) {
$this->$relation()->touch();

if ($this->$relation instanceof self) {
$this->$relation->fireModelEvent('saved', false);

$this->$relation->touchOwners();
} elseif ($this->$relation instanceof Collection) {
$this->$relation->each->touchOwners();
}
}
},
null,
);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/Illuminate/Support/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -528,9 +528,9 @@ function without_recursion(callable $callback, mixed $onRecursion, ?string $as =
{
return Recurser::instance()
->withoutRecursion(
$as
? Recursable::fromSignature($as, $callback, $onRecursion, $for)
: Recursable::fromTrace(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2), $callback, $onRecursion, $for)
$as
? Recursable::fromSignature($as, $callback, $onRecursion, $for)
: Recursable::fromTrace(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2), $callback, $onRecursion, $for)
);
}
}
83 changes: 83 additions & 0 deletions tests/Database/DatabaseEloquentIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ protected function createSchema()
$table->morphs('taggable');
$table->string('taxonomy')->nullable();
});

$this->schema($connection)->create('categories', function ($table) {
$table->increments('id');
$table->string('name');
$table->integer('parent_id')->nullable();
$table->timestamps();
});
}

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

public function testTouchingChaperonedChildModelUpdatesParentTimestamps()
{
$before = Carbon::now();

$one = EloquentTouchingCategory::create(['id' => 1, 'name' => 'One']);
$two = $one->children()->create(['id' => 2, 'name' => 'Two']);

$this->assertTrue($before->isSameDay($one->updated_at));
$this->assertTrue($before->isSameDay($two->updated_at));

Carbon::setTestNow($future = $before->copy()->addDays(3));

$two->touch();

$this->assertTrue($future->isSameDay($two->fresh()->updated_at), 'It is not touching model own timestamps.');
$this->assertTrue($future->isSameDay($one->fresh()->updated_at), 'It is not touching chaperoned models related timestamps.');
}

public function testTouchingBiDirectionalChaperonedModelUpdatesAllRelatedTimestamps()
{
$before = Carbon::now();

EloquentTouchingCategory::insert([
['id' => 1, 'name' => 'One', 'parent_id' => null, 'created_at' => $before, 'updated_at' => $before],
['id' => 2, 'name' => 'Two', 'parent_id' => 1, 'created_at' => $before, 'updated_at' => $before],
['id' => 3, 'name' => 'Three', 'parent_id' => 1, 'created_at' => $before, 'updated_at' => $before],
['id' => 4, 'name' => 'Four', 'parent_id' => 2, 'created_at' => $before, 'updated_at' => $before],
]);

$one = EloquentTouchingCategory::find(1);
[$two, $three] = $one->children;
[$four] = $two->children;

$this->assertTrue($before->isSameDay($one->updated_at));
$this->assertTrue($before->isSameDay($two->updated_at));
$this->assertTrue($before->isSameDay($three->updated_at));
$this->assertTrue($before->isSameDay($four->updated_at));

Carbon::setTestNow($future = $before->copy()->addDays(3));

// Touch a random model and check that all of the others have been updated
$models = tap([$one, $two, $three, $four], shuffle(...));
$target = array_shift($models);
$target->touch();

$this->assertTrue($future->isSameDay($target->fresh()->updated_at), 'It is not touching model own timestamps.');

while ($next = array_shift($models)) {
$this->assertTrue(
$future->isSameDay($next->fresh()->updated_at),
'It is not touching related models timestamps.'
);
}
}

/**
* Helpers...
*/
Expand Down Expand Up @@ -2486,3 +2548,24 @@ public function post()
return $this->belongsTo(EloquentTouchingPost::class, 'post_id');
}
}

class EloquentTouchingCategory extends Eloquent
{
protected $table = 'categories';
protected $guarded = [];

protected $touches = [
'parent',
'children',
];

public function parent()
{
return $this->belongsTo(EloquentTouchingCategory::class, 'parent_id');
}

public function children()
{
return $this->hasMany(EloquentTouchingCategory::class, 'parent_id')->chaperone();
}
}
3 changes: 2 additions & 1 deletion tests/Integration/Support/WithoutRecursionHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ public function testCallbacksAreCalledOnceOnRecursiveInstances()
$this->assertSame(['upline' => 1, 'upline_callback' => 1], $tail->pullCallCount());
}

public function testRecursionCallbacksAreCalledOnRecursiveInstances() {
public function testRecursionCallbacksAreCalledOnRecursiveInstances()
{
$head = DoublyLinkedRecursiveList::make(children: 2);
$body = $head->getNext();
$tail = $body->getNext();
Expand Down
9 changes: 5 additions & 4 deletions tests/Support/SupportRecursableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class SupportRecursableTest extends TestCase
{
public function testForSetsObjectIfBlank()
{
$one = (object)[];
$two = (object)[];
$one = (object) [];
$two = (object) [];

$recursableOne = new Recursable(fn () => 'foo', 'bar', null);
$recursableTwo = new Recursable(fn () => 'foo', 'bar', $one);
Expand Down Expand Up @@ -133,7 +133,8 @@ public function testHashFromSignature(string $signature)
}

#[DataProvider('backtraceProvider')]
public function testObjectFromTrace(array $trace, array $target, string $signature) {
public function testObjectFromTrace(array $trace, array $target, string $signature)
{
$this->assertSame($target['object'], RecursableStub::expose_objectFromTrace($trace));
}

Expand Down Expand Up @@ -214,7 +215,7 @@ public function testFromSignatureCreatesRecursable(string $signature)
public static function backtraceProvider(): array
{
$empty = ['file' => '', 'class' => '', 'function' => '', 'line' => 0, 'object' => null];
$object = (object)[];
$object = (object) [];

return [
'no frames' => [[], $empty, ':0'],
Expand Down

0 comments on commit 35ae45c

Please sign in to comment.