diff --git a/src/Model/Concerns/QueriesRelationships.php b/src/Model/Concerns/QueriesRelationships.php index 9cd99d6..9e83840 100644 --- a/src/Model/Concerns/QueriesRelationships.php +++ b/src/Model/Concerns/QueriesRelationships.php @@ -376,6 +376,34 @@ public function orWhereDoesntHaveMorph(string $relation, $types, ?Closure $callb return $this->doesntHaveMorph($relation, $types, 'or', $callback); } + /** + * Add a basic where clause to a relationship query. + */ + public function whereRelation(string $relation, array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null): Builder|static + { + return $this->whereHas($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. + */ + public function orWhereRelation(string $relation, array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null): Builder|static + { + return $this->orWhereHas($relation, function ($query) use ($column, $operator, $value) { + if ($column instanceof Closure) { + $column($query); + } else { + $query->where($column, $operator, $value); + } + }); + } + /** * Add a polymorphic relationship count / exists condition to the query. * diff --git a/tests/WhereHasTest.php b/tests/WhereHasTest.php new file mode 100644 index 0000000..90d79b3 --- /dev/null +++ b/tests/WhereHasTest.php @@ -0,0 +1,250 @@ +shouldReceive('get')->with(Db::class)->andReturn($db); + $connectionResolverInterface = $container->get(ConnectionResolverInterface::class); + Register::setConnectionResolver($connectionResolverInterface); + $this->createSchema(); + } + + protected function tearDown(): void + { + Schema::dropIfExists('comments'); + Schema::dropIfExists('texts'); + Schema::dropIfExists('posts'); + Schema::dropIfExists('users'); + Mockery::close(); + $reflection = new ReflectionClass(ApplicationContext::class); + $reflection->setStaticPropertyValue('container', null); + } + + public function createSchema() + { + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('user_id'); + $table->boolean('public'); + }); + + Schema::create('texts', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('post_id'); + $table->text('content'); + }); + + Schema::create('comments', function (Blueprint $table) { + $table->increments('id'); + $table->string('commentable_type'); + $table->integer('commentable_id'); + }); + + $user = User::create(); + $post = tap((new Post(['public' => true]))->user()->associate($user))->save(); + (new Comment())->commentable()->associate($post)->save(); + (new Text(['content' => 'test']))->post()->associate($post)->save(); + + $user = User::create(); + $post = tap((new Post(['public' => false]))->user()->associate($user))->save(); + (new Comment())->commentable()->associate($post)->save(); + (new Text(['content' => 'test2']))->post()->associate($post)->save(); + } + + public static function dataProviderWhereRelationCallback() + { + $callbackArray = function ($value) { + $callbackEloquent = function (ModelBuilder $builder) use ($value) { + $builder->selectRaw('id')->where('public', $value); + }; + + $callbackQuery = function (QueryBuilder $builder) use ($value) { + $hasMany = (new User())->posts(); + + $builder->from('posts')->addSelect(['*'])->whereColumn( + $hasMany->getQualifiedParentKeyName(), + '=', + $hasMany->getQualifiedForeignKeyName() + ); + + $builder->selectRaw('id')->where('public', $value); + }; + + return [$callbackEloquent, $callbackQuery]; + }; + + return [ + 'Find user with post.public = true' => $callbackArray(true), + 'Find user with post.public = false' => $callbackArray(false), + ]; + } + + public function testWhereRelation() + { + $users = User::whereRelation('posts', 'public', true)->get(); + + $this->assertEquals([1], $users->pluck('id')->all()); + } + + public function testOrWhereRelation() + { + $users = User::whereRelation('posts', 'public', true)->orWhereRelation('posts', 'public', false)->get(); + + $this->assertEquals([1, 2], $users->pluck('id')->all()); + } + + public function testNestedWhereRelation() + { + $texts = User::whereRelation('posts.texts', 'content', 'test')->get(); + + $this->assertEquals([1], $texts->pluck('id')->all()); + } + + public function testNestedOrWhereRelation() + { + $texts = User::whereRelation('posts.texts', 'content', 'test')->orWhereRelation('posts.texts', 'content', 'test2')->get(); + + $this->assertEquals([1, 2], $texts->pluck('id')->all()); + } + + /** + * Check that the 'whereRelation' callback function works. + * + * @dataProvider dataProviderWhereRelationCallback + * @param mixed $callbackEloquent + * @param mixed $callbackQuery + */ + public function testWhereRelationCallback($callbackEloquent, $callbackQuery) + { + $userWhereRelation = User::whereRelation('posts', $callbackEloquent); + $userWhereHas = User::whereHas('posts', $callbackEloquent); + $query = Db::table('users')->whereExists($callbackQuery); + + $this->assertEquals($userWhereRelation->getQuery()->toSql(), $query->toSql()); + $this->assertEquals($userWhereRelation->getQuery()->toSql(), $userWhereHas->toSql()); + $this->assertEquals($userWhereHas->getQuery()->toSql(), $query->toSql()); + + $this->assertEquals($userWhereRelation->first()->id, $query->first()->id); + $this->assertEquals($userWhereRelation->first()->id, $userWhereHas->first()->id); + $this->assertEquals($userWhereHas->first()->id, $query->first()->id); + } + + /** + * Check that the 'orWhereRelation' callback function works. + * + * @dataProvider dataProviderWhereRelationCallback + * @param mixed $callbackEloquent + * @param mixed $callbackQuery + */ + public function testOrWhereRelationCallback($callbackEloquent, $callbackQuery): void + { + $userOrWhereRelation = User::orWhereRelation('posts', $callbackEloquent); + $userOrWhereHas = User::orWhereHas('posts', $callbackEloquent); + $query = Db::table('users')->orWhereExists($callbackQuery); + + $this->assertEquals($userOrWhereRelation->getQuery()->toSql(), $query->toSql()); + $this->assertEquals($userOrWhereRelation->getQuery()->toSql(), $userOrWhereHas->toSql()); + $this->assertEquals($userOrWhereHas->getQuery()->toSql(), $query->toSql()); + + $this->assertEquals($userOrWhereRelation->first()->id, $query->first()->id); + $this->assertEquals($userOrWhereRelation->first()->id, $userOrWhereHas->first()->id); + $this->assertEquals($userOrWhereHas->first()->id, $query->first()->id); + } +} + +class Comment extends Model +{ + public bool $timestamps = false; + + public function commentable() + { + return $this->morphTo(); + } +} + +class Post extends Model +{ + public bool $timestamps = false; + + protected array $guarded = []; + + protected array $withCount = ['comments']; + + public function comments() + { + return $this->morphMany(Comment::class, 'commentable'); + } + + public function texts() + { + return $this->hasMany(Text::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } +} + +class Text extends Model +{ + public bool $timestamps = false; + + protected array $guarded = []; + + public function post() + { + return $this->belongsTo(Post::class); + } +} + +class User extends Model +{ + public bool $timestamps = false; + + protected ?string $table = 'users'; + + public function posts() + { + return $this->hasMany(Post::class); + } +}