Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eager Load Pivot Relations Laravel Package

ajcastro/eager-load-pivot-relations

Laravel package that eager loads relations defined on pivot models in many-to-many relationships, eliminating N+1 queries when accessing nested pivot data. Use familiar Eloquent syntax to keep role_user, post_tag, and similar pivot records fast and clean. Drop-in trait and builder methods make setup...

View on GitHub
Deep Wiki
Context7

Getting Started

ajcastro/eager-load-pivot-relations solves one specific gap in Eloquent: a belongsToMany pivot row that is a real model with its own relations (e.g. Membership → assignedBy user), which you cannot eager load with withPivot() alone.

Install and pin. The package is pre-1.0 (current line: v0.3.x), so minor bumps can carry breaking changes. Use composer require ajcastro/eager-load-pivot-relations:^0.3 and commit the lock file rather than floating on ^0.3 in CI images you don't rebuild often.

Where to look first. The package is small — read src/ in this order: the service provider (what gets registered/macro'd), the pivot trait (what you must use in your pivot model), and the relation extension (how withPivotRelations is wired). Then skim the README examples and the linked PR (https://github.com/ajcastro/eager-load-pivot-relations/pull/11) to see exactly what landed in v0.3.0 before you upgrade.

First use case. An API endpoint listing team members where the membership row carries audit data and a foreign key:

// app/Models/Membership.php
use Illuminate\Database\Eloquent\Relations\Pivot;
use AjCastro\EagerLoadPivotRelations\EagerLoadPivotTrait;

class Membership extends Pivot
{
    use EagerLoadPivotTrait;

    protected $table = 'memberships';

    public function assignedBy()
    {
        return $this->belongsTo(User::class, 'assigned_by');
    }
}

// app/Models/Team.php
public function members()
{
    return $this->belongsToMany(User::class)
        ->using(Membership::class)          // required — no custom pivot, no relations
        ->withPivot(['assigned_by', 'role', 'created_at']);
}

Then eager load the pivot relation alongside the relation itself:

$teams = Team::with(['members' => function ($q) {
    $q->withPivotRelations(['assignedBy']);
}])->paginate(25);

If the pivot trait is in place and using() is set, $member->pivot->assignedBy is hydrated for every row with a single additional query instead of one per member.

Implementation Patterns

Centralize the eager load in the relationship, not the controller. Append the pivot relations inside the relation definition (or a dedicated scope) so every consumer — controllers, resources, console commands, queued exports — gets consistent hydration:

public function members()
{
    return $this->belongsToMany(User::class)
        ->using(Membership::class)
        ->withPivot(['assigned_by', 'role'])
        ->withPivotRelations(['assignedBy']);   // default everywhere
}

Override with ->withoutPivotRelations()-style scoping or a narrower relation (e.g. membersLite()) for hot paths that only need IDs.

Combine with using() + withPivot() deliberately. withPivot() selects columns; withPivotRelations() loads models. You need both: the foreign key column (assigned_by) must be in withPivot() or the belongsTo on the pivot has nothing to resolve.

Shape output in API Resources, not in the model. Pivot relations serialize nested under pivot, which is convenient for debugging and dangerous for public APIs:

return [
    'id'         => $this->id,
    'name'       => $this->name,
    'assigned_by'=> $this->pivot->assignedBy?->only(['id', 'name']),
    'since'      => $this->pivot->created_at?->toIso8601String(),
];

Explicit keys also protect you from a v0.3.x serialization change.

Nest one level, no deeper. Pivot → relation is the supported shape. If you find yourself wanting a pivot's relation's pivot, that data usually belongs in its own table with a normal hasMany.

Reusable scopes for filtered lists. Pair with wherePivot and whereHas so filtering and hydration happen in the same query plan:

Team::with(['members' => fn ($q) => $q->wherePivot('role', 'owner')
                                        ->withPivotRelations(['assignedBy'])])
    ->whereHas('members', fn ($q) => $q->wherePivot('role', 'owner'))
    ->get();

Verify with a query-count test. This is the whole value proposition, so lock it in:

DB::enableQueryLog();
Team::with(['members' => fn ($q) => $q->withPivotRelations(['assignedBy'])])->get();
$this->assertCount(3, DB::getQueryLog()); // teams + members + assignedBy

Any regression to N+1 shows up immediately in CI rather than in a slow dashboard.

Console/queue contexts. Eager loading pays off most in exports: ->chunkById(500, fn ($chunk) => ...) with the pivot relations preloaded turns a multi-minute job into seconds. Use chunkById, not chunk, once you have eager-loaded relations.

Gotchas and Tips

->using(CustomPivot::class) is mandatory. Without it, Laravel hydrates a generic Pivot instance and the trait never runs — withPivotRelations() will appear to work (no exception) but relations come back null. This is the #1 silent failure.

The foreign key column must be in withPivot(). The package hydrates relations from pivot columns; it does not add columns for you. Missing column → null relation, no warning.

Spell relation names exactly. A typo in withPivotRelations(['assignedby']) typically results in a missing relation or an exception deep in the eager-load builder, not a clear "relation not found" message. Cross-check against the method name on the pivot class.

Check the pivot actually has the right model class before debugging the package. $member->pivot instanceof Membership is the fastest sanity check; if it's false, the problem is your relation definition, not v0.3.0.

Serialization surface. Pivot relations land inside the pivot key in toArray()/toJson(). On a public endpoint this can leak user records (emails, internal flags) that the pivot's belongsTo points at. Always go through API Resources, or add $hidden on the pivot model.

Watch query count, not just correctness. Each registered pivot relation is one extra query across all parents — cheap, but stacking four of them on a paginated index still multiplies. Profile the endpoint, and keep hot paths to one or two pivot relations.

Pre-1.0 upgrade discipline for v0.3.0. The release note is a single PR link, so the diff is the changelog. Before bumping: read the PR, check composer.json for the Laravel version constraint (a common breakage point when you're on Laravel 10/11), run the full suite, and re-run your query-count tests. Treat 0.x minor bumps as potentially breaking — they are.

Watch for double eager loading. Defining withPivotRelations() inside the relation and passing the same relations again in the controller is usually harmless (Eloquent dedupes by relation name) but can produce surprises if you pass differing constraint closures. Pick one place.

Extension points. The service provider and the pivot trait are the two seams worth knowing: the trait is where you'd add project-wide pivot helpers (e.g. a pivotAssignedBy() accessor), and the provider is where relation macros/extensions are registered if you need to adapt behaviour rather than fork.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager