From fcd90b914f30169dce1166a60bd97ddf72eae0e1 Mon Sep 17 00:00:00 2001 From: sadique-cws Date: Mon, 23 Mar 2026 07:46:40 +0530 Subject: [PATCH 1/4] [12.x] Apply eager load constraints to oneOfMany inner subquery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When eager loading a latestOfMany/ofMany relationship with constraints (e.g. with(['lastPayment' => fn($q) => $q->where('store_id', 1)])), the constraints were only applied to the outer query, not the inner subquery that determines which record is the 'latest'. This caused the inner subquery to pick the wrong aggregate row (e.g. the latest payment across ALL stores), and then the outer filter would discard it — silently returning null. The fix snapshots the outer query's wheres before and after the user's constraint closure runs, then copies only the new wheres into the oneOfMany subquery. This ensures the inner subquery determines 'latest' within the user's filtered dataset. Fixes #59318 --- src/Illuminate/Database/Eloquent/Builder.php | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index f13cf1a44e17..5e5423767d05 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -945,8 +945,37 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints) $relation->addEagerConstraints($models); + // For one-of-many relationships, we need to also apply the user's eager + // load constraints to the inner subquery that determines which record + // is the "latest" / "oldest". We snapshot the outer query's wheres + // before and after applying constraints, then copy only the new ones. + $subQuery = method_exists($relation, 'getOneOfManySubQuery') + ? $relation->getOneOfManySubQuery() + : null; + + if ($subQuery) { + $whereCountBefore = count($relation->getQuery()->getQuery()->wheres); + $bindingCountBefore = count($relation->getQuery()->getQuery()->bindings['where']); + } + $constraints($relation); + if ($subQuery) { + $outerWheres = $relation->getQuery()->getQuery()->wheres; + $outerBindings = $relation->getQuery()->getQuery()->bindings['where']; + + $newWheres = array_slice($outerWheres, $whereCountBefore); + $newBindings = array_slice($outerBindings, $bindingCountBefore); + + foreach ($newWheres as $where) { + $subQuery->getQuery()->wheres[] = $where; + } + + foreach ($newBindings as $binding) { + $subQuery->getQuery()->addBinding($binding, 'where'); + } + } + // Once we have the results, we just match those back up to their parent models // using the relationship instance. Then we just return the finished arrays // of models which have been eagerly hydrated and are readied for return. From a3d63c83f1d9d8a30f5d9c310a4872c051b4d507 Mon Sep 17 00:00:00 2001 From: sadique-cws Date: Mon, 23 Mar 2026 21:58:19 +0530 Subject: [PATCH 2/4] Add integration test for eager load oneOfMany constraints --- .../Database/DatabaseEloquentHasOneOfManyTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index c1925e7ec8d2..0b39053917d6 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -437,6 +437,21 @@ public function testEagerLoadingWithMultipleAggregates() $this->assertSame($user2Price->id, $users[1]->price->id); } + public function testEagerLoadingWithConstraintsAppliesToSubQuery() + { + $user = HasOneOfManyTestUser::create(); + $user->logins()->create(); // ID 1 + $login2 = $user->logins()->create(); // ID 2 + $user->logins()->create(); // ID 3 + + $users = HasOneOfManyTestUser::with(['latest_login' => function ($q) { + $q->where('logins.id', '<', 3); + }])->get(); + + $this->assertNotNull($users[0]->latest_login); + $this->assertSame($login2->id, $users[0]->latest_login->id); + } + public function testWithExists() { $user = HasOneOfManyTestUser::create(); From 68f4676ca78b1e1d0acbcdfb52ed4287531ccf1b Mon Sep 17 00:00:00 2001 From: sadique-cws Date: Wed, 25 Mar 2026 10:29:37 +0530 Subject: [PATCH 3/4] [13.x] Fix eager loaded oneOfMany relationship constraints in closures --- src/Illuminate/Database/Eloquent/Builder.php | 31 +------------------ .../Relations/Concerns/CanBeOneOfMany.php | 31 +++++++++++++++++++ .../Database/Eloquent/Relations/Relation.php | 11 +++++++ .../Database/DatabaseEloquentBuilderTest.php | 3 ++ 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 5e5423767d05..c7feb6000bff 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -945,36 +945,7 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints) $relation->addEagerConstraints($models); - // For one-of-many relationships, we need to also apply the user's eager - // load constraints to the inner subquery that determines which record - // is the "latest" / "oldest". We snapshot the outer query's wheres - // before and after applying constraints, then copy only the new ones. - $subQuery = method_exists($relation, 'getOneOfManySubQuery') - ? $relation->getOneOfManySubQuery() - : null; - - if ($subQuery) { - $whereCountBefore = count($relation->getQuery()->getQuery()->wheres); - $bindingCountBefore = count($relation->getQuery()->getQuery()->bindings['where']); - } - - $constraints($relation); - - if ($subQuery) { - $outerWheres = $relation->getQuery()->getQuery()->wheres; - $outerBindings = $relation->getQuery()->getQuery()->bindings['where']; - - $newWheres = array_slice($outerWheres, $whereCountBefore); - $newBindings = array_slice($outerBindings, $bindingCountBefore); - - foreach ($newWheres as $where) { - $subQuery->getQuery()->wheres[] = $where; - } - - foreach ($newBindings as $binding) { - $subQuery->getQuery()->addBinding($binding, 'where'); - } - } + $relation->applyEagerLoadingConstraints($constraints); // Once we have the results, we just match those back up to their parent models // using the relationship instance. Then we just return the finished arrays diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 800999f86c78..0a50b6a47f3f 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -329,4 +329,35 @@ public function getRelationName() { return $this->relationName; } + + /** + * Apply the given constraints to the relationship. + * + * @param \Closure $constraints + * @return void + */ + public function applyEagerLoadingConstraints(Closure $constraints) + { + $subQuery = $this->getOneOfManySubQuery(); + + if ($subQuery) { + $whereCountBefore = count($this->query->getQuery()->wheres); + $bindingCountBefore = count($this->query->getQuery()->bindings['where']); + } + + $constraints($this); + + if ($subQuery) { + $newWheres = array_slice($this->query->getQuery()->wheres, $whereCountBefore); + $newBindings = array_slice($this->query->getQuery()->bindings['where'], $bindingCountBefore); + + foreach ($newWheres as $where) { + $subQuery->getQuery()->wheres[] = $where; + } + + foreach ($newBindings as $binding) { + $subQuery->getQuery()->addBinding($binding, 'where'); + } + } + } } diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index 5f0ced508179..940f341a317f 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/src/Illuminate/Database/Eloquent/Relations/Relation.php @@ -522,6 +522,17 @@ public static function getMorphAlias(string $className) return array_search($className, static::$morphMap, strict: true) ?: $className; } + /** + * Apply the given constraints to the relationship. + * + * @param \Closure $constraints + * @return void + */ + public function applyEagerLoadingConstraints(Closure $constraints) + { + $constraints($this); + } + /** * Handle dynamic method calls to the relationship. * diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 471a78d9a7f2..7bc5a3e76a51 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -844,6 +844,9 @@ public function testRelationshipEagerLoadProcess() $_SERVER['__eloquent.constrain'] = $query; }]); $relation = m::mock(stdClass::class); + $relation->shouldReceive('applyEagerLoadingConstraints')->once()->andReturnUsing(function ($callback) use ($relation) { + $callback($relation); + }); $relation->shouldReceive('addEagerConstraints')->once()->with(['models']); $relation->shouldReceive('initRelation')->once()->with(['models'], 'orders')->andReturn(['models']); $relation->shouldReceive('getEager')->once()->andReturn(['results']); From d4b49e086bf879136cdcedae4b940fbc1308978d Mon Sep 17 00:00:00 2001 From: sadique-cws Date: Wed, 25 Mar 2026 12:32:27 +0530 Subject: [PATCH 4/4] [13.x] Fix oneOfMany relationship constraints via native routing This PR addresses the issue where constraints applied to eager-loaded or lazy-loaded oneOfMany relationships were erroneously applied as post-filters. By implementing __call in the CanBeOneOfMany trait, we now route where* and orWhere* constraints directly to the aggregation subquery, ensuring they correctly influence record selection. --- src/Illuminate/Database/Eloquent/Builder.php | 2 +- .../Relations/Concerns/CanBeOneOfMany.php | 33 +++++++------------ .../Database/Eloquent/Relations/Relation.php | 11 ------- .../Database/DatabaseEloquentBuilderTest.php | 3 -- .../DatabaseEloquentHasOneOfManyTest.php | 8 +++-- 5 files changed, 17 insertions(+), 40 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index c7feb6000bff..f13cf1a44e17 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -945,7 +945,7 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints) $relation->addEagerConstraints($models); - $relation->applyEagerLoadingConstraints($constraints); + $constraints($relation); // Once we have the results, we just match those back up to their parent models // using the relationship instance. Then we just return the finished arrays diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 0a50b6a47f3f..513af5608563 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -331,33 +331,22 @@ public function getRelationName() } /** - * Apply the given constraints to the relationship. + * Handle dynamic method calls to the relationship. * - * @param \Closure $constraints - * @return void + * @param string $method + * @param array $parameters + * @return mixed */ - public function applyEagerLoadingConstraints(Closure $constraints) + public function __call($method, $parameters) { - $subQuery = $this->getOneOfManySubQuery(); - - if ($subQuery) { - $whereCountBefore = count($this->query->getQuery()->wheres); - $bindingCountBefore = count($this->query->getQuery()->bindings['where']); + if (static::hasMacro($method)) { + return $this->macroCall($method, $parameters); } - $constraints($this); - - if ($subQuery) { - $newWheres = array_slice($this->query->getQuery()->wheres, $whereCountBefore); - $newBindings = array_slice($this->query->getQuery()->bindings['where'], $bindingCountBefore); - - foreach ($newWheres as $where) { - $subQuery->getQuery()->wheres[] = $where; - } + $query = $this->isOneOfMany() && (str_starts_with($method, 'where') || str_starts_with($method, 'orWhere')) + ? $this->getOneOfManySubQuery() + : $this->query; - foreach ($newBindings as $binding) { - $subQuery->getQuery()->addBinding($binding, 'where'); - } - } + return $this->forwardDecoratedCallTo($query, $method, $parameters); } } diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index 940f341a317f..5f0ced508179 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/src/Illuminate/Database/Eloquent/Relations/Relation.php @@ -522,17 +522,6 @@ public static function getMorphAlias(string $className) return array_search($className, static::$morphMap, strict: true) ?: $className; } - /** - * Apply the given constraints to the relationship. - * - * @param \Closure $constraints - * @return void - */ - public function applyEagerLoadingConstraints(Closure $constraints) - { - $constraints($this); - } - /** * Handle dynamic method calls to the relationship. * diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 7bc5a3e76a51..471a78d9a7f2 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -844,9 +844,6 @@ public function testRelationshipEagerLoadProcess() $_SERVER['__eloquent.constrain'] = $query; }]); $relation = m::mock(stdClass::class); - $relation->shouldReceive('applyEagerLoadingConstraints')->once()->andReturnUsing(function ($callback) use ($relation) { - $callback($relation); - }); $relation->shouldReceive('addEagerConstraints')->once()->with(['models']); $relation->shouldReceive('initRelation')->once()->with(['models'], 'orders')->andReturn(['models']); $relation->shouldReceive('getEager')->once()->andReturn(['results']); diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index 0b39053917d6..e6f8e7c4ff34 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -229,7 +229,8 @@ public function testItGetsWithConstraintsCorrectResults() $user->logins()->create(); $result = $user->latest_login()->whereKey($previousLogin->getKey())->getResults(); - $this->assertNull($result); + $this->assertNotNull($result); + $this->assertSame($previousLogin->id, $result->id); } public function testItEagerLoadsCorrectModels() @@ -314,7 +315,7 @@ public function testExists() $previousLogin = $user->logins()->create(); $latestLogin = $user->logins()->create(); - $this->assertFalse($user->latest_login()->whereKey($previousLogin->getKey())->exists()); + $this->assertTrue($user->latest_login()->whereKey($previousLogin->getKey())->exists()); $this->assertTrue($user->latest_login()->whereKey($latestLogin->getKey())->exists()); } @@ -349,7 +350,8 @@ public function testGet() $this->assertSame($latestLogin->id, $latestLogins->first()->id); $latestLogins = $user->latest_login()->whereKey($previousLogin->getKey())->get(); - $this->assertCount(0, $latestLogins); + $this->assertCount(1, $latestLogins); + $this->assertSame($previousLogin->id, $latestLogins->first()->id); } public function testCount()