diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 1e602ff35cb8..e064689a8f54 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -536,7 +536,7 @@ public function orWhereMorphDoesntHaveRelation($relation, $types, $column, $oper * Add a morph-to relationship condition to the query. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation - * @param \Illuminate\Database\Eloquent\Model|string|null $model + * @param \Illuminate\Database\Eloquent\Model|iterable|string|null $model * @return $this */ public function whereMorphedTo($relation, $model, $boolean = 'and') @@ -559,9 +559,19 @@ public function whereMorphedTo($relation, $model, $boolean = 'and') return $this->where($relation->qualifyColumn($relation->getMorphType()), $model, null, $boolean); } - return $this->where(function ($query) use ($relation, $model) { - $query->where($relation->qualifyColumn($relation->getMorphType()), $model->getMorphClass()) - ->where($relation->qualifyColumn($relation->getForeignKeyName()), $model->getKey()); + $models = BaseCollection::wrap($model); + + if ($models->isEmpty()) { + throw new InvalidArgumentException('Collection given to whereMorphedTo method may not be empty.'); + } + + return $this->where(function ($query) use ($relation, $models) { + $models->groupBy(fn ($model) => $model->getMorphClass())->each(function ($models) use ($query, $relation) { + $query->orWhere(function ($query) use ($relation, $models) { + $query->where($relation->qualifyColumn($relation->getMorphType()), $models->first()->getMorphClass()) + ->whereIn($relation->qualifyColumn($relation->getForeignKeyName()), $models->map->getKey()); + }); + }); }, null, null, $boolean); } @@ -569,7 +579,7 @@ public function whereMorphedTo($relation, $model, $boolean = 'and') * Add a not morph-to relationship condition to the query. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation - * @param \Illuminate\Database\Eloquent\Model|string $model + * @param \Illuminate\Database\Eloquent\Model|iterable|string $model * @return $this */ public function whereNotMorphedTo($relation, $model, $boolean = 'and') @@ -588,9 +598,19 @@ public function whereNotMorphedTo($relation, $model, $boolean = 'and') return $this->whereNot($relation->qualifyColumn($relation->getMorphType()), '<=>', $model, $boolean); } - return $this->whereNot(function ($query) use ($relation, $model) { - $query->where($relation->qualifyColumn($relation->getMorphType()), '<=>', $model->getMorphClass()) - ->where($relation->qualifyColumn($relation->getForeignKeyName()), '<=>', $model->getKey()); + $models = BaseCollection::wrap($model); + + if ($models->isEmpty()) { + throw new InvalidArgumentException('Collection given to whereNotMorphedTo method may not be empty.'); + } + + return $this->whereNot(function ($query) use ($relation, $models) { + $models->groupBy(fn ($model) => $model->getMorphClass())->each(function ($models) use ($query, $relation) { + $query->orWhere(function ($query) use ($relation, $models) { + $query->where($relation->qualifyColumn($relation->getMorphType()), '<=>', $models->first()->getMorphClass()) + ->whereNotIn($relation->qualifyColumn($relation->getForeignKeyName()), $models->map->getKey()); + }); + }); }, null, null, $boolean); } @@ -598,7 +618,7 @@ public function whereNotMorphedTo($relation, $model, $boolean = 'and') * Add a morph-to relationship condition to the query with an "or where" clause. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation - * @param \Illuminate\Database\Eloquent\Model|string|null $model + * @param \Illuminate\Database\Eloquent\Model|iterable|string|null $model * @return $this */ public function orWhereMorphedTo($relation, $model) @@ -610,7 +630,7 @@ public function orWhereMorphedTo($relation, $model) * Add a not morph-to relationship condition to the query with an "or where" clause. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation - * @param \Illuminate\Database\Eloquent\Model|string $model + * @param \Illuminate\Database\Eloquent\Model|iterable|string $model * @return $this */ public function orWhereNotMorphedTo($relation, $model) diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 6ce852dd3f71..a84cd851eb0e 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -1774,10 +1774,47 @@ public function testWhereMorphedTo() $builder = $model->whereMorphedTo('morph', $relatedModel); - $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where ("eloquent_builder_test_model_parent_stubs"."morph_type" = ? and "eloquent_builder_test_model_parent_stubs"."morph_id" = ?)', $builder->toSql()); + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where (("eloquent_builder_test_model_parent_stubs"."morph_type" = ? and "eloquent_builder_test_model_parent_stubs"."morph_id" in (?)))', $builder->toSql()); $this->assertEquals([$relatedModel->getMorphClass(), $relatedModel->getKey()], $builder->getBindings()); } + public function testWhereMorphedToCollection() + { + $model = new EloquentBuilderTestModelParentStub; + $this->mockConnectionForModel($model, ''); + + $firstRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $firstRelatedModel->id = 1; + + $secondRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $secondRelatedModel->id = 2; + + $builder = $model->whereMorphedTo('morph', new Collection([$firstRelatedModel, $secondRelatedModel])); + + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where (("eloquent_builder_test_model_parent_stubs"."morph_type" = ? and "eloquent_builder_test_model_parent_stubs"."morph_id" in (?, ?)))', $builder->toSql()); + $this->assertEquals([$firstRelatedModel->getMorphClass(), $firstRelatedModel->getKey(), $secondRelatedModel->getKey()], $builder->getBindings()); + } + + public function testWhereMorphedToCollectionWithDifferentModels() + { + $model = new EloquentBuilderTestModelParentStub; + $this->mockConnectionForModel($model, ''); + + $firstRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $firstRelatedModel->id = 1; + + $secondRelatedModel = new EloquentBuilderTestModelFarRelatedStub; + $secondRelatedModel->id = 2; + + $thirdRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $thirdRelatedModel->id = 3; + + $builder = $model->whereMorphedTo('morph', [$firstRelatedModel, $secondRelatedModel, $thirdRelatedModel]); + + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where (("eloquent_builder_test_model_parent_stubs"."morph_type" = ? and "eloquent_builder_test_model_parent_stubs"."morph_id" in (?, ?)) or ("eloquent_builder_test_model_parent_stubs"."morph_type" = ? and "eloquent_builder_test_model_parent_stubs"."morph_id" in (?)))', $builder->toSql()); + $this->assertEquals([$firstRelatedModel->getMorphClass(), $firstRelatedModel->getKey(), $thirdRelatedModel->getKey(), $secondRelatedModel->getMorphClass(), $secondRelatedModel->id], $builder->getBindings()); + } + public function testWhereMorphedToNull() { $model = new EloquentBuilderTestModelParentStub; @@ -1797,10 +1834,47 @@ public function testWhereNotMorphedTo() $builder = $model->whereNotMorphedTo('morph', $relatedModel); - $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where not ("eloquent_builder_test_model_parent_stubs"."morph_type" <=> ? and "eloquent_builder_test_model_parent_stubs"."morph_id" <=> ?)', $builder->toSql()); + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where not (("eloquent_builder_test_model_parent_stubs"."morph_type" <=> ? and "eloquent_builder_test_model_parent_stubs"."morph_id" not in (?)))', $builder->toSql()); $this->assertEquals([$relatedModel->getMorphClass(), $relatedModel->getKey()], $builder->getBindings()); } + public function testWhereNotMorphedToCollection() + { + $model = new EloquentBuilderTestModelParentStub; + $this->mockConnectionForModel($model, ''); + + $firstRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $firstRelatedModel->id = 1; + + $secondRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $secondRelatedModel->id = 2; + + $builder = $model->whereNotMorphedTo('morph', new Collection([$firstRelatedModel, $secondRelatedModel])); + + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where not (("eloquent_builder_test_model_parent_stubs"."morph_type" <=> ? and "eloquent_builder_test_model_parent_stubs"."morph_id" not in (?, ?)))', $builder->toSql()); + $this->assertEquals([$firstRelatedModel->getMorphClass(), $firstRelatedModel->getKey(), $secondRelatedModel->getKey()], $builder->getBindings()); + } + + public function testWhereNotMorphedToCollectionWithDifferentModels() + { + $model = new EloquentBuilderTestModelParentStub; + $this->mockConnectionForModel($model, ''); + + $firstRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $firstRelatedModel->id = 1; + + $secondRelatedModel = new EloquentBuilderTestModelFarRelatedStub; + $secondRelatedModel->id = 2; + + $thirdRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $thirdRelatedModel->id = 3; + + $builder = $model->whereNotMorphedTo('morph', [$firstRelatedModel, $secondRelatedModel, $thirdRelatedModel]); + + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where not (("eloquent_builder_test_model_parent_stubs"."morph_type" <=> ? and "eloquent_builder_test_model_parent_stubs"."morph_id" not in (?, ?)) or ("eloquent_builder_test_model_parent_stubs"."morph_type" <=> ? and "eloquent_builder_test_model_parent_stubs"."morph_id" not in (?)))', $builder->toSql()); + $this->assertEquals([$firstRelatedModel->getMorphClass(), $firstRelatedModel->getKey(), $thirdRelatedModel->getKey(), $secondRelatedModel->getMorphClass(), $secondRelatedModel->id], $builder->getBindings()); + } + public function testOrWhereMorphedTo() { $model = new EloquentBuilderTestModelParentStub; @@ -1811,10 +1885,47 @@ public function testOrWhereMorphedTo() $builder = $model->where('bar', 'baz')->orWhereMorphedTo('morph', $relatedModel); - $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where "bar" = ? or ("eloquent_builder_test_model_parent_stubs"."morph_type" = ? and "eloquent_builder_test_model_parent_stubs"."morph_id" = ?)', $builder->toSql()); + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where "bar" = ? or (("eloquent_builder_test_model_parent_stubs"."morph_type" = ? and "eloquent_builder_test_model_parent_stubs"."morph_id" in (?)))', $builder->toSql()); $this->assertEquals(['baz', $relatedModel->getMorphClass(), $relatedModel->getKey()], $builder->getBindings()); } + public function testOrWhereMorphedToCollection() + { + $model = new EloquentBuilderTestModelParentStub; + $this->mockConnectionForModel($model, ''); + + $firstRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $firstRelatedModel->id = 1; + + $secondRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $secondRelatedModel->id = 2; + + $builder = $model->where('bar', 'baz')->orWhereMorphedTo('morph', new Collection([$firstRelatedModel, $secondRelatedModel])); + + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where "bar" = ? or (("eloquent_builder_test_model_parent_stubs"."morph_type" = ? and "eloquent_builder_test_model_parent_stubs"."morph_id" in (?, ?)))', $builder->toSql()); + $this->assertEquals(['baz', $firstRelatedModel->getMorphClass(), $firstRelatedModel->getKey(), $secondRelatedModel->getKey()], $builder->getBindings()); + } + + public function testOrWhereMorphedToCollectionWithDifferentModels() + { + $model = new EloquentBuilderTestModelParentStub; + $this->mockConnectionForModel($model, ''); + + $firstRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $firstRelatedModel->id = 1; + + $secondRelatedModel = new EloquentBuilderTestModelFarRelatedStub; + $secondRelatedModel->id = 2; + + $thirdRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $thirdRelatedModel->id = 3; + + $builder = $model->where('bar', 'baz')->orWhereMorphedTo('morph', [$firstRelatedModel, $secondRelatedModel, $thirdRelatedModel]); + + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where "bar" = ? or (("eloquent_builder_test_model_parent_stubs"."morph_type" = ? and "eloquent_builder_test_model_parent_stubs"."morph_id" in (?, ?)) or ("eloquent_builder_test_model_parent_stubs"."morph_type" = ? and "eloquent_builder_test_model_parent_stubs"."morph_id" in (?)))', $builder->toSql()); + $this->assertEquals(['baz', $firstRelatedModel->getMorphClass(), $firstRelatedModel->getKey(), $thirdRelatedModel->getKey(), $secondRelatedModel->getMorphClass(), $secondRelatedModel->id], $builder->getBindings()); + } + public function testOrWhereMorphedToNull() { $model = new EloquentBuilderTestModelParentStub; @@ -1836,10 +1947,47 @@ public function testOrWhereNotMorphedTo() $builder = $model->where('bar', 'baz')->orWhereNotMorphedTo('morph', $relatedModel); - $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where "bar" = ? or not ("eloquent_builder_test_model_parent_stubs"."morph_type" <=> ? and "eloquent_builder_test_model_parent_stubs"."morph_id" <=> ?)', $builder->toSql()); + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where "bar" = ? or not (("eloquent_builder_test_model_parent_stubs"."morph_type" <=> ? and "eloquent_builder_test_model_parent_stubs"."morph_id" not in (?)))', $builder->toSql()); $this->assertEquals(['baz', $relatedModel->getMorphClass(), $relatedModel->getKey()], $builder->getBindings()); } + public function testOrWhereNotMorphedToCollection() + { + $model = new EloquentBuilderTestModelParentStub; + $this->mockConnectionForModel($model, ''); + + $firstRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $firstRelatedModel->id = 1; + + $secondRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $secondRelatedModel->id = 2; + + $builder = $model->where('bar', 'baz')->orWhereNotMorphedTo('morph', new Collection([$firstRelatedModel, $secondRelatedModel])); + + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where "bar" = ? or not (("eloquent_builder_test_model_parent_stubs"."morph_type" <=> ? and "eloquent_builder_test_model_parent_stubs"."morph_id" not in (?, ?)))', $builder->toSql()); + $this->assertEquals(['baz', $firstRelatedModel->getMorphClass(), $firstRelatedModel->getKey(), $secondRelatedModel->getKey()], $builder->getBindings()); + } + + public function testOrWhereNotMorphedToCollectionWithDifferentModels() + { + $model = new EloquentBuilderTestModelParentStub; + $this->mockConnectionForModel($model, ''); + + $firstRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $firstRelatedModel->id = 1; + + $secondRelatedModel = new EloquentBuilderTestModelFarRelatedStub; + $secondRelatedModel->id = 2; + + $thirdRelatedModel = new EloquentBuilderTestModelCloseRelatedStub; + $thirdRelatedModel->id = 3; + + $builder = $model->where('bar', 'baz')->orWhereNotMorphedTo('morph', [$firstRelatedModel, $secondRelatedModel, $thirdRelatedModel]); + + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where "bar" = ? or not (("eloquent_builder_test_model_parent_stubs"."morph_type" <=> ? and "eloquent_builder_test_model_parent_stubs"."morph_id" not in (?, ?)) or ("eloquent_builder_test_model_parent_stubs"."morph_type" <=> ? and "eloquent_builder_test_model_parent_stubs"."morph_id" not in (?)))', $builder->toSql()); + $this->assertEquals(['baz', $firstRelatedModel->getMorphClass(), $firstRelatedModel->getKey(), $thirdRelatedModel->getKey(), $secondRelatedModel->getMorphClass(), $secondRelatedModel->id], $builder->getBindings()); + } + public function testWhereMorphedToClass() { $model = new EloquentBuilderTestModelParentStub;