From f826d0fc3842b355bed6a0f10f632d66d3f17891 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Fri, 27 Dec 2024 00:42:46 +0300 Subject: [PATCH] Added new Eloquent methods: `whereDoesntHaveRelation`, `orWhereDoesntHaveRelation`, `whereMorphDoesntHaveRelation` and `orWhereMorphDoesntHaveRelation` (#53996) --- .../Concerns/QueriesRelationships.php | 74 +++++++++++++++++ .../Database/EloquentWhereHasTest.php | 82 +++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 894e7525846e..56d116454621 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -424,6 +424,46 @@ public function orWhereRelation($relation, $column, $operator = null, $value = n }); } + /** + * Add a basic count / exists condition to a relationship query. + * + * @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation + * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @return $this + */ + public function whereDoesntHaveRelation($relation, $column, $operator = null, $value = null) + { + return $this->whereDoesntHave($relation, function ($query) use ($column, $operator, $value) { + if ($column instanceof Closure) { + $column($query); + } else { + $query->where($column, $operator, $value); + } + }); + } + + /** + * Add an "or where" clause to a relationship query. + * + * @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation + * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @return $this + */ + public function orWhereDoesntHaveRelation($relation, $column, $operator = null, $value = null) + { + return $this->orWhereDoesntHave($relation, function ($query) use ($column, $operator, $value) { + if ($column instanceof Closure) { + $column($query); + } else { + $query->where($column, $operator, $value); + } + }); + } + /** * Add a polymorphic relationship condition to the query with a where clause. * @@ -458,6 +498,40 @@ public function orWhereMorphRelation($relation, $types, $column, $operator = nul }); } + /** + * Add a polymorphic relationship condition to the query with a doesn't have clause. + * + * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation + * @param string|array $types + * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @return $this + */ + public function whereMorphDoesntHaveRelation($relation, $types, $column, $operator = null, $value = null) + { + return $this->whereDoesntHaveMorph($relation, $types, function ($query) use ($column, $operator, $value) { + $query->where($column, $operator, $value); + }); + } + + /** + * Add a polymorphic relationship condition to the query with an "or doesn't have" clause. + * + * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation + * @param string|array $types + * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @return $this + */ + public function orWhereMorphDoesntHaveRelation($relation, $types, $column, $operator = null, $value = null) + { + return $this->orWhereDoesntHaveMorph($relation, $types, function ($query) use ($column, $operator, $value) { + $query->where($column, $operator, $value); + }); + } + /** * Add a morph-to relationship condition to the query. * diff --git a/tests/Integration/Database/EloquentWhereHasTest.php b/tests/Integration/Database/EloquentWhereHasTest.php index 2e52257b1a18..cf78e9d11ad0 100644 --- a/tests/Integration/Database/EloquentWhereHasTest.php +++ b/tests/Integration/Database/EloquentWhereHasTest.php @@ -86,6 +86,44 @@ public function testOrWhereRelationCallback($callbackEloquent, $callbackQuery) $this->assertEquals($userOrWhereHas->first()->id, $query->first()->id); } + /** + * Check that the 'whereDoesntHaveRelation' callback function works. + */ + #[DataProvider('dataProviderWhereRelationCallback')] + public function testWhereDoesntRelationCallback($callbackEloquent, $callbackQuery) + { + $userWhereDoesntRelation = User::whereDoesntHaveRelation('posts', $callbackEloquent); + $userWhereHas = User::whereDoesntHave('posts', $callbackEloquent); + $query = DB::table('users')->whereNotExists($callbackQuery); + + $this->assertEquals($userWhereDoesntRelation->getQuery()->toSql(), $query->toSql()); + $this->assertEquals($userWhereDoesntRelation->getQuery()->toSql(), $userWhereHas->toSql()); + $this->assertEquals($userWhereHas->getQuery()->toSql(), $query->toSql()); + + $this->assertEquals($userWhereDoesntRelation->first()->id, $query->first()->id); + $this->assertEquals($userWhereDoesntRelation->first()->id, $userWhereHas->first()->id); + $this->assertEquals($userWhereHas->first()->id, $query->first()->id); + } + + /** + * Check that the 'orWhereDoesntRelation' callback function works. + */ + #[DataProvider('dataProviderWhereRelationCallback')] + public function testOrWhereDoesntRelationCallback($callbackEloquent, $callbackQuery) + { + $userOrWhereDoesntRelation = User::orWhereDoesntHaveRelation('posts', $callbackEloquent); + $userOrWhereHas = User::orWhereDoesntHave('posts', $callbackEloquent); + $query = DB::table('users')->orWhereNotExists($callbackQuery); + + $this->assertEquals($userOrWhereDoesntRelation->getQuery()->toSql(), $query->toSql()); + $this->assertEquals($userOrWhereDoesntRelation->getQuery()->toSql(), $userOrWhereHas->toSql()); + $this->assertEquals($userOrWhereHas->getQuery()->toSql(), $query->toSql()); + + $this->assertEquals($userOrWhereDoesntRelation->first()->id, $query->first()->id); + $this->assertEquals($userOrWhereDoesntRelation->first()->id, $userOrWhereHas->first()->id); + $this->assertEquals($userOrWhereHas->first()->id, $query->first()->id); + } + public static function dataProviderWhereRelationCallback() { $callbackArray = function ($value) { @@ -158,6 +196,50 @@ public function testOrWhereMorphRelation() $this->assertEquals([1, 2], $comments->pluck('id')->all()); } + public function testWhereDoesntHaveRelation() + { + $users = User::whereDoesntHaveRelation('posts', 'public', true)->get(); + + $this->assertEquals([2], $users->pluck('id')->all()); + } + + public function testOrWhereDoesntHaveRelation() + { + $users = User::whereDoesntHaveRelation('posts', 'public', true)->orWhereDoesntHaveRelation('posts', 'public', false)->get(); + + $this->assertEquals([1, 2], $users->pluck('id')->all()); + } + + public function testNestedWhereDoesntHaveRelation() + { + $texts = User::whereDoesntHaveRelation('posts.texts', 'content', 'test')->get(); + + $this->assertEquals([2], $texts->pluck('id')->all()); + } + + public function testNestedOrWhereDoesntHaveRelation() + { + $texts = User::whereDoesntHaveRelation('posts.texts', 'content', 'test')->orWhereDoesntHaveRelation('posts.texts', 'content', 'test2')->get(); + + $this->assertEquals([1, 2], $texts->pluck('id')->all()); + } + + public function testWhereMorphDoesntHaveRelation() + { + $comments = Comment::whereMorphDoesntHaveRelation('commentable', '*', 'public', true)->get(); + + $this->assertEquals([2], $comments->pluck('id')->all()); + } + + public function testOrWhereMorphDoesntHaveRelation() + { + $comments = Comment::whereMorphDoesntHaveRelation('commentable', '*', 'public', true) + ->orWhereMorphDoesntHaveRelation('commentable', '*', 'public', false) + ->get(); + + $this->assertEquals([1, 2], $comments->pluck('id')->all()); + } + public function testWithCount() { $users = User::whereHas('posts', function ($query) {