Search by

codewiser / belongs-to-many

Cellard

Extended BelongsToMany for Laravel

Package info

github.com/C0deWiser/belongs-to-many

pkg:composer/codewiser/belongs-to-many

Statistics

Installs: 44

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.2.0 2026-09-18 07:24 UTC

This package is auto-updated.

Last update: 2026-09-18 07:26:51 UTC


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 HasPivot trait only affects BelongsToMany relations. Other relations, such as HasMany or BelongsTo, 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.