codewiser / belongs-to-many
Extended BelongsToMany for Laravel
Requires
- php: ^8.2
- laravel/framework: >=11.0
Requires (Dev)
- orchestra/testbench: ^10.6
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A BelongsToMany relation connects two models through a pivot table. In
simple cases, the pivot table holds just two columns — the foreign keys. In
more complex cases, it holds additional columns that you want to use to
constrain the relation.
The problem: when you use the whereHas method on a BelongsToMany
relation, the callback receives a Builder instance instead of the
Relation instance, so you can't use the wherePivot* methods.
Take a look at the difference. Here we get a Relation instance:
$user->organizations()->wherePivot('role', 'accountant');
But in whereHas, we get a Builder instance:
User::query() ->where('users.role', 'superuser') ->whereHas('organizations', fn(Builder $builder) => $builder ->where('organization_user.role', 'accountant') );
Here we're forced to use fully qualified column names to avoid ambiguity.
The solution is to hand the Relation instance to the callback, which is
what the HasPivot trait does for custom builders.
With the trait, all *has* builder methods — whereHas, whereDoesntHave,
etc. — pass a BelongsToMany object to the callback instead of a Builder
instance, so you can use any wherePivot* method to constrain the
intermediate query.
The
HasPivottrait only affectsBelongsToManyrelations. Other relations, such asHasManyorBelongsTo, keep their default behaviour.
Implementation
Give both models a custom builder and apply the HasPivot trait to it:
use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder; use Illuminate\Database\Eloquent\Model; #[UseEloquentBuilder(UserBuilder::class)] class User extends Model { // }
use Codewiser\Database\Eloquent\Traits\HasPivot; use Illuminate\Database\Eloquent\Builder; /** * @extends Builder<User> */ class UserBuilder extends Builder { use HasPivot; }
What the callback receives
Now the whereHas callback for a BelongsToMany relation may receive a
BelongsToMany object — depending on the
type you require for
its first parameter.
Passing a BelongsToMany is safe, because it forwards calls to the
underlying Builder. Pass a plain Builder and you get the default (and
backward compatible) behaviour:
use Illuminate\Contracts\Database\Eloquent\Builder as BuilderContract; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsToMany; Organization::query()->whereHas('users', // Requires Builder explicitly (default, backward compatible); // gets the related model's Builder. fn(Builder $builder) => $builder->where('organization_user.role', 'accountant') ); Organization::query()->whereHas('users', // Requires BelongsToMany explicitly; gets the relation. fn(BelongsToMany $builder) => $builder->wherePivot('role', 'accountant') ); Organization::query()->whereHas('users', // No type; gets the relation. fn($builder) => $builder->wherePivot('role', 'accountant') ); Organization::query()->whereHas('users', // A Builder contract; gets the relation. fn(BuilderContract $builder) => $builder->wherePivot('role', 'accountant') ); Organization::query()->whereHas('users', // BelongsToMany is part of the union; gets the relation. fn(Builder|BelongsToMany $builder) => $builder->wherePivot('role', 'accountant') );
Custom Intermediate Table Builder
If you define a
custom intermediate table model,
it has a query builder of its own. The extended BelongsToMany object
provides a way to access it via the pivot method — a macro registered by
this package's PivotServiceProvider:
$user->organizations()->pivot( fn(PivotBuilder $builder) => $builder->whereRole('accountant') ); Organization::query()->whereHas('users', fn(BelongsToMany $builder) => $builder->pivot( fn(MyPivotBuilder $builder) => $builder->whereRole('accountant') ) );
Since Laravel v13.26.0
added
closure support to wherePivot,
the pivot method is deprecated:
$user->organizations()->wherePivot( fn(MyPivotBuilder $builder) => $builder->whereRole('accountant') ); Organization::query()->whereHas('users', fn(BelongsToMany $builder) => $builder->wherePivot( fn(MyPivotBuilder $builder) => $builder->whereRole('accountant') ) );
Notes
Everything above also applies to MorphToMany relations.