From 0bf4f3cfde8210e0a5ab0ff1b64cfbaf6f453fe6 Mon Sep 17 00:00:00 2001 From: AmirHossein Fallah Date: Wed, 2 Apr 2025 02:57:08 +0330 Subject: [PATCH 1/2] Fix pivot model observer not working properly when using withPivotValue --- .../Eloquent/Relations/BelongsToMany.php | 9 +++++- .../Concerns/InteractsWithPivotTable.php | 28 +++++++++++++++++-- .../Database/EloquentPivotEventsTest.php | 25 +++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index e125a760410b..767fb040ba6e 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -483,7 +483,14 @@ public function withPivotValue($column, $value = null) $this->pivotValues[] = compact('column', 'value'); - return $this->wherePivot($column, '=', $value); + // Add where clause for filtering but track it separately so it doesn't + // prevent the pivot model observers from working + $this->wherePivot($column, '=', $value); + + // Mark this where clause as coming from withPivotValue + $this->pivotWheres[count($this->pivotWheres) - 1]['from_pivot_value'] = true; + + return $this; } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index 015554ed767a..1be8995c048e 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -208,7 +208,7 @@ protected function attachNew(array $records, array $current, $touch = true) public function updateExistingPivot($id, array $attributes, $touch = true) { if ($this->using && - empty($this->pivotWheres) && + empty($this->getNonPivotValueWhereClauses()) && empty($this->pivotWhereIns) && empty($this->pivotWhereNulls)) { return $this->updateExistingPivotUsingCustomClass($id, $attributes, $touch); @@ -437,7 +437,7 @@ public function detach($ids = null, $touch = true) { if ($this->using && ! empty($ids) && - empty($this->pivotWheres) && + empty($this->getNonPivotValueWhereClauses()) && empty($this->pivotWhereIns) && empty($this->pivotWhereNulls)) { $results = $this->detachUsingCustomClass($ids); @@ -570,7 +570,15 @@ public function newPivotQuery() $query = $this->newPivotStatement(); foreach ($this->pivotWheres as $arguments) { - $query->where(...$arguments); + // Clone the arguments to avoid modifying the original array + $argumentsToApply = $arguments; + + // Remove the from_pivot_value flag before applying to the query + if (isset($argumentsToApply['from_pivot_value'])) { + unset($argumentsToApply['from_pivot_value']); + } + + $query->where(...$argumentsToApply); } foreach ($this->pivotWhereIns as $arguments) { @@ -691,4 +699,18 @@ protected function getTypeSwapValue($type, $value) default => $value, }; } + + /** + * Get the pivot where clauses that are not from withPivotValue. + * + * @return array + */ + protected function getNonPivotValueWhereClauses() + { + return collect($this->pivotWheres) + ->filter(function ($where) { + return empty($where['from_pivot_value'] ?? false); + }) + ->all(); + } } diff --git a/tests/Integration/Database/EloquentPivotEventsTest.php b/tests/Integration/Database/EloquentPivotEventsTest.php index 7a9962982595..aa6256bbe48f 100644 --- a/tests/Integration/Database/EloquentPivotEventsTest.php +++ b/tests/Integration/Database/EloquentPivotEventsTest.php @@ -152,6 +152,31 @@ public function testCustomMorphPivotClassDetachAttributes() $project->equipments()->save($equipment); $equipment->projects()->sync([]); } + + public function testPivotWithPivotValueTriggerEvents() + { + $user = PivotEventsTestUser::forceCreate(['email' => 'taylor@laravel.com']); + $project = PivotEventsTestProject::forceCreate(['name' => 'Test Project']); + + // Attach with a specific role + $project->collaborators()->attach($user, ['role' => 'admin']); + PivotEventsTestCollaborator::$eventsCalled = []; + + // Get the relation with withPivotValue + $adminRelation = $project->belongsToMany( + PivotEventsTestUser::class, 'project_users', 'project_id', 'user_id' + )->using(PivotEventsTestCollaborator::class) + ->withPivotValue('role', 'admin'); + + // Update through the relation with withPivotValue + $adminRelation->updateExistingPivot($user->id, ['permissions' => ['manage']]); + $this->assertEquals(['saving', 'updating', 'updated', 'saved'], PivotEventsTestCollaborator::$eventsCalled, 'Observer events should fire on updateExistingPivot with withPivotValue'); + + // Detach through the relation with withPivotValue + PivotEventsTestCollaborator::$eventsCalled = []; + $adminRelation->detach($user->id); + $this->assertEquals(['deleting', 'deleted'], PivotEventsTestCollaborator::$eventsCalled, 'Observer events should fire on detach with withPivotValue'); + } } class PivotEventsTestUser extends Model From c7fcdac3b991c9ef28a8c0ea67c57006dec63d95 Mon Sep 17 00:00:00 2001 From: AmirHossein Fallah Date: Wed, 2 Apr 2025 03:03:10 +0330 Subject: [PATCH 2/2] code style --- src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php | 4 ++-- .../Eloquent/Relations/Concerns/InteractsWithPivotTable.php | 4 ++-- tests/Integration/Database/EloquentPivotEventsTest.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 767fb040ba6e..fdf796f86ba0 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -483,10 +483,10 @@ public function withPivotValue($column, $value = null) $this->pivotValues[] = compact('column', 'value'); - // Add where clause for filtering but track it separately so it doesn't + // Add where clause for filtering but track it separately so it doesn't // prevent the pivot model observers from working $this->wherePivot($column, '=', $value); - + // Mark this where clause as coming from withPivotValue $this->pivotWheres[count($this->pivotWheres) - 1]['from_pivot_value'] = true; diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index 1be8995c048e..e880c6b551a4 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -572,12 +572,12 @@ public function newPivotQuery() foreach ($this->pivotWheres as $arguments) { // Clone the arguments to avoid modifying the original array $argumentsToApply = $arguments; - + // Remove the from_pivot_value flag before applying to the query if (isset($argumentsToApply['from_pivot_value'])) { unset($argumentsToApply['from_pivot_value']); } - + $query->where(...$argumentsToApply); } diff --git a/tests/Integration/Database/EloquentPivotEventsTest.php b/tests/Integration/Database/EloquentPivotEventsTest.php index aa6256bbe48f..ba0fdbd2d3b3 100644 --- a/tests/Integration/Database/EloquentPivotEventsTest.php +++ b/tests/Integration/Database/EloquentPivotEventsTest.php @@ -166,7 +166,7 @@ public function testPivotWithPivotValueTriggerEvents() $adminRelation = $project->belongsToMany( PivotEventsTestUser::class, 'project_users', 'project_id', 'user_id' )->using(PivotEventsTestCollaborator::class) - ->withPivotValue('role', 'admin'); + ->withPivotValue('role', 'admin'); // Update through the relation with withPivotValue $adminRelation->updateExistingPivot($user->id, ['permissions' => ['manage']]);