Skip to content

[13.x] feat: add chaperone support for BelongsToMany pivot models - #61152

Open
calebdw wants to merge 1 commit into
laravel:13.xfrom
calebdw:calebdw/push-tqvuqtxvszyk
Open

[13.x] feat: add chaperone support for BelongsToMany pivot models#61152
calebdw wants to merge 1 commit into
laravel:13.xfrom
calebdw:calebdw/push-tqvuqtxvszyk

Conversation

@calebdw

@calebdw calebdw commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Hello!

Many times when using custom pivot models, the BelongsTo relations can be defined on the model---however, trying to use them requires loading the models from the database even though both of the models are available during the BelongsToMany hydration

This PR adds chaperone support to the BelongsToMany relation, if a custom pivot model is not provided then calling ->chaperone() is a no-op. Any relations that don't exist are skipped (some pivot models only define one relation or even no relations); however, if a relation name is explicitly provided and the corresponding relation does not exist then an exception is thrown

class PostTagPivot extends Pivot
{
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }

    public function tag(): BelongsTo
    {
        return $this->belongsTo(Tag::class);
    }
}

class Tag extends Model
{
    public function posts(): BelongsToMany
    {
        return $this->belongsToMany(Post::class)
            ->using(PostTagPivot::class)
            // automatically guesses relationship names
            ->chaperone();
            // but can override if needed or non-standard
            ->chaperone(declaring: 'tag', related: 'post');
    }
}

class Post extends Model
{
    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class)
            ->using(PostTagPivot::class)
            ->chaperone(declaring: 'post', related: 'tag');
    }
}

Thanks!

Add the ability to automatically set the declaring and related model
instances as loaded relationships on custom pivot models, mirroring
the chaperone concept from HasMany but adapted for the dual-parent
nature of many-to-many relationships.

This eliminates extra queries when a custom pivot class defines
belongsTo relationships back to either side of the many-to-many.
Both relation names can be explicitly provided or auto-guessed from
the foreign key column names and model class basenames. If guessing
fails for a side, that side is silently skipped; if an explicit name
is invalid, a RelationNotFoundException is thrown.

The implementation lives in a new SupportsPivotInverseRelations trait
used by BelongsToMany, with hooks in hydratePivotRelation() for lazy
loading and match() to correct the declaring side during eager
loading where $this->parent only reflects the first parent model.
@calebdw
calebdw force-pushed the calebdw/push-tqvuqtxvszyk branch from 1db8eed to 423b455 Compare August 15, 2026 23:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant