-
Notifications
You must be signed in to change notification settings - Fork 75
Infer $pivot property type for BelongsToMany related models #641
Description
Problem
When a model is loaded through a BelongsToMany relationship, Laravel sets a $pivot property dynamically. Psalm reports UndefinedMagicPropertyFetch for any access to $model->pivot:
$continent = $member->managedContinents()->first();
$continent->pivot->leader_since; // UndefinedMagicPropertyFetchCurrently the only workaround is adding @property-read Pivot $pivot to every model that participates in a BelongsToMany — imprecise and manual.
Proposed Solution
Detect BelongsToMany relationships during AfterCodebasePopulated and declare $pivot as a virtual property on the related model via PropertyExistenceProvider + PropertyTypeProvider.
Pivot type resolution (in priority order)
using(CustomPivot::class)— return the custom Pivot class (has declared properties with types)withPivot('col1', 'col2')+ schema — cross-reference column names with migration schema (already parsed bySchemaStateProvider) to get column types- Default — return
Illuminate\Database\Eloquent\Relations\Pivot
Additional: as() accessor name
BelongsToMany::as('membership') changes the accessor from $model->pivot to $model->membership. Track the as() call and register the property under the custom name.
Challenges
- Any model can be loaded with or without pivot —
$pivotonly exists when loaded through BelongsToMany, not viaModel::find(). Declaring it unconditionally is imprecise but matches the@property-readconvention. - Multiple BelongsToMany on the same model — if a model participates in two BelongsToMany with different pivots, the
$pivottype would need to be a union. - Parsing relationship method bodies —
RelationMethodParseralready parses relationship definitions; extend it to extractusing(),withPivot(),withTimestamps(), andas()chain calls.
Scope for first pass
Detect using(CustomPivot::class) only — this covers typed pivots (most common in well-structured apps). withPivot() string columns and schema cross-referencing can follow later.