Skip to content

Infer $pivot property type for BelongsToMany related models #641

@alies-dev

Description

@alies-dev

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; // UndefinedMagicPropertyFetch

Currently 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)

  1. using(CustomPivot::class) — return the custom Pivot class (has declared properties with types)
  2. withPivot('col1', 'col2') + schema — cross-reference column names with migration schema (already parsed by SchemaStateProvider) to get column types
  3. 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$pivot only exists when loaded through BelongsToMany, not via Model::find(). Declaring it unconditionally is imprecise but matches the @property-read convention.
  • Multiple BelongsToMany on the same model — if a model participates in two BelongsToMany with different pivots, the $pivot type would need to be a union.
  • Parsing relationship method bodiesRelationMethodParser already parses relationship definitions; extend it to extract using(), withPivot(), withTimestamps(), and as() 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions