Skip to content

Commit 6a08dbd

Browse files
[12.x] Add whereAttachedTo() Eloquent builder method (#55245)
* Add `whereAttachedTo` and `orWhereAttachedTo` * Add tests
1 parent 83d07e9 commit 6a08dbd

File tree

3 files changed

+144
-1
lines changed

3 files changed

+144
-1
lines changed

Diff for: src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php

+58
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
99
use Illuminate\Database\Eloquent\RelationNotFoundException;
1010
use Illuminate\Database\Eloquent\Relations\BelongsTo;
11+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
1112
use Illuminate\Database\Eloquent\Relations\MorphTo;
1213
use Illuminate\Database\Eloquent\Relations\Relation;
1314
use Illuminate\Database\Query\Builder as QueryBuilder;
@@ -765,6 +766,63 @@ public function orWhereBelongsTo($related, $relationshipName = null)
765766
return $this->whereBelongsTo($related, $relationshipName, 'or');
766767
}
767768

769+
/**
770+
* Add a "belongs to many" relationship where clause to the query.
771+
*
772+
* @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model> $related
773+
* @param string|null $relationshipName
774+
* @param string $boolean
775+
* @return $this
776+
*
777+
* @throws \Illuminate\Database\Eloquent\RelationNotFoundException
778+
*/
779+
public function whereAttachedTo($related, $relationshipName = null, $boolean = 'and')
780+
{
781+
$relatedCollection = $related instanceof EloquentCollection ? $related : $related->newCollection([$related]);
782+
783+
$related = $relatedCollection->first();
784+
785+
if ($relatedCollection->isEmpty()) {
786+
throw new InvalidArgumentException('Collection given to whereAttachedTo method may not be empty.');
787+
}
788+
789+
if ($relationshipName === null) {
790+
$relationshipName = Str::plural(Str::camel(class_basename($related)));
791+
}
792+
793+
try {
794+
$relationship = $this->model->{$relationshipName}();
795+
} catch (BadMethodCallException) {
796+
throw RelationNotFoundException::make($this->model, $relationshipName);
797+
}
798+
799+
if (! $relationship instanceof BelongsToMany) {
800+
throw RelationNotFoundException::make($this->model, $relationshipName, BelongsToMany::class);
801+
}
802+
803+
$this->has(
804+
$relationshipName,
805+
boolean: $boolean,
806+
callback: fn (Builder $query) => $query->whereKey($relatedCollection),
807+
);
808+
809+
return $this;
810+
}
811+
812+
/**
813+
* Add a "belongs to many" relationship with an "or where" clause to the query.
814+
*
815+
* @param \Illuminate\Database\Eloquent\Model $related
816+
* @param string|null $relationshipName
817+
* @return $this
818+
*
819+
* @throws \RuntimeException
820+
*/
821+
public function orWhereAttachedTo($related, $relationshipName = null)
822+
{
823+
return $this->whereAttachedTo($related, $relationshipName, 'or');
824+
}
825+
768826
/**
769827
* Add subselect queries to include an aggregate value for a relationship.
770828
*

Diff for: tests/Database/DatabaseEloquentBuilderTest.php

+32-1
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,29 @@ public function testWhereBelongsTo()
12781278
$this->assertEquals($result, $builder);
12791279
}
12801280

1281+
public function testWhereAttachedTo()
1282+
{
1283+
$related = new EloquentBuilderTestModelFarRelatedStub;
1284+
$related->id = 49;
1285+
1286+
$builder = EloquentBuilderTestModelParentStub::whereAttachedTo($related, 'roles');
1287+
1288+
$this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where exists (select * from "eloquent_builder_test_model_far_related_stubs" inner join "user_role" on "eloquent_builder_test_model_far_related_stubs"."id" = "user_role"."related_id" where "eloquent_builder_test_model_parent_stubs"."id" = "user_role"."self_id" and "eloquent_builder_test_model_far_related_stubs"."id" in (49))', $builder->toSql());
1289+
}
1290+
1291+
public function testWhereAttachedToCollection()
1292+
{
1293+
$model1 = new EloquentBuilderTestModelParentStub;
1294+
$model1->id = 3;
1295+
1296+
$model2 = new EloquentBuilderTestModelParentStub;
1297+
$model2->id = 4;
1298+
1299+
$builder = EloquentBuilderTestModelFarRelatedStub::whereAttachedTo(new Collection([$model1, $model2]), 'roles');
1300+
1301+
$this->assertSame('select * from "eloquent_builder_test_model_far_related_stubs" where exists (select * from "eloquent_builder_test_model_parent_stubs" inner join "user_role" on "eloquent_builder_test_model_parent_stubs"."id" = "user_role"."self_id" where "eloquent_builder_test_model_far_related_stubs"."id" = "user_role"."related_id" and "eloquent_builder_test_model_parent_stubs"."id" in (3, 4))', $builder->toSql());
1302+
}
1303+
12811304
public function testDeleteOverride()
12821305
{
12831306
$builder = $this->getBuilder();
@@ -2813,7 +2836,15 @@ public function baz()
28132836

28142837
class EloquentBuilderTestModelFarRelatedStub extends Model
28152838
{
2816-
//
2839+
public function roles()
2840+
{
2841+
return $this->belongsToMany(
2842+
EloquentBuilderTestModelParentStub::class,
2843+
'user_role',
2844+
'related_id',
2845+
'self_id',
2846+
);
2847+
}
28172848
}
28182849

28192850
class EloquentBuilderTestModelSelfRelatedStub extends Model

Diff for: tests/Database/DatabaseEloquentIntegrationTest.php

+54
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,15 @@ protected function createSchema()
170170
$table->integer('parent_id')->nullable();
171171
$table->timestamps();
172172
});
173+
174+
$this->schema($connection)->create('achievements', function ($table) {
175+
$table->increments('id');
176+
});
177+
178+
$this->schema($connection)->create('eloquent_test_achievement_eloquent_test_user', function ($table) {
179+
$table->integer('eloquent_test_achievement_id');
180+
$table->integer('eloquent_test_user_id');
181+
});
173182
}
174183

175184
$this->schema($connection)->create('non_incrementing_users', function ($table) {
@@ -1483,6 +1492,34 @@ public function testBelongsToManyRelationshipModelsAreProperlyHydratedOverCursor
14831492
}
14841493
}
14851494

1495+
public function testWhereAttachedTo()
1496+
{
1497+
$user1 = EloquentTestUser::create(['email' => '[email protected]']);
1498+
$user2 = EloquentTestUser::create(['email' => '[email protected]']);
1499+
$user3 = EloquentTestUser::create(['email' => '[email protected]']);
1500+
$achievement1 = EloquentTestAchievement::create();
1501+
$achievement2 = EloquentTestAchievement::create();
1502+
$achievement3 = EloquentTestAchievement::create();
1503+
1504+
$user1->eloquentTestAchievements()->attach([$achievement1]);
1505+
$user2->eloquentTestAchievements()->attach([$achievement1, $achievement3]);
1506+
$user3->eloquentTestAchievements()->attach([$achievement2, $achievement3]);
1507+
1508+
$achievedAchievement1 = EloquentTestUser::whereAttachedTo($achievement1)->get();
1509+
1510+
$this->assertSame(2, $achievedAchievement1->count());
1511+
$this->assertTrue($achievedAchievement1->contains($user1));
1512+
$this->assertTrue($achievedAchievement1->contains($user2));
1513+
1514+
$achievedByUser1or2 = EloquentTestAchievement::whereAttachedTo(
1515+
new Collection([$user1, $user2])
1516+
)->get();
1517+
1518+
$this->assertSame(2, $achievedByUser1or2->count());
1519+
$this->assertTrue($achievedByUser1or2->contains($achievement1));
1520+
$this->assertTrue($achievedByUser1or2->contains($achievement3));
1521+
}
1522+
14861523
public function testBasicHasManyEagerLoading()
14871524
{
14881525
$user = EloquentTestUser::create(['email' => '[email protected]']);
@@ -2686,6 +2723,11 @@ public function postWithPhotos()
26862723
$join->where('photo.imageable_type', 'EloquentTestPost');
26872724
});
26882725
}
2726+
2727+
public function eloquentTestAchievements()
2728+
{
2729+
return $this->belongsToMany(EloquentTestAchievement::class);
2730+
}
26892731
}
26902732

26912733
class EloquentTestUserWithCustomFriendPivot extends EloquentTestUser
@@ -2941,6 +2983,18 @@ public function children()
29412983
}
29422984
}
29432985

2986+
class EloquentTestAchievement extends Eloquent
2987+
{
2988+
public $timestamps = false;
2989+
2990+
protected $table = 'achievements';
2991+
2992+
public function eloquentTestUsers()
2993+
{
2994+
return $this->belongsToMany(EloquentTestUser::class);
2995+
}
2996+
}
2997+
29442998
class ModelWithUniqueStringIds extends Eloquent
29452999
{
29463000
use HasUuids;

0 commit comments

Comments
 (0)