Skip to content

fix(model): never invoke non-attribute methods on property access#32

Merged
3m1n3nc3 merged 2 commits into
mainfrom
fix/model-accessor-probe-and-relation-scope
Jul 9, 2026
Merged

fix(model): never invoke non-attribute methods on property access#32
3m1n3nc3 merged 2 commits into
mainfrom
fix/model-accessor-probe-and-relation-scope

Conversation

@3m1n3nc3

@3m1n3nc3 3m1n3nc3 commented Jul 9, 2026

Copy link
Copy Markdown
Member

The model Proxy probed every accessed property by invoking the same-named method to check whether it returned an Attribute (a Laravel-style accessor). That fired the method's side effects on a bare property read:

  • model.recordView ran recordView() before it was called, so model.recordView(viewer) (a read followed by a call) inserted twice — the reported 'relation-insert double-inserts' gotcha.
  • Reading a relation like model.posts invoked posts() on every access.

resolveAttributeMutator now:

  • only probes zero-arity methods (accessors take no args; actions/relations like recordView(viewer) never qualify),
  • caches the accessor determination per class (an arity-0 method is probed at most once), and
  • on the read path additionally requires the key to be a model attribute (loaded value, append, cast, or mapped column), so even a zero-arity side-effecting method is never fired by a property read. Writes still probe (the key being assigned is an attribute by definition), so Attribute-object set mutators keep working during hydration and on fresh models.

Tests: an argument-taking and a zero-arity side-effecting method are never invoked by property reads and run exactly once when called; Attribute-object get/set accessors and appends continue to resolve.

3m1n3nc3 added 2 commits July 9, 2026 07:59
The model Proxy probed every accessed property by invoking the same-named
method to check whether it returned an Attribute (a Laravel-style accessor).
That fired the method's side effects on a bare property read:

- `model.recordView` ran recordView() before it was called, so
  `model.recordView(viewer)` (a read followed by a call) inserted twice — the
  reported 'relation-insert double-inserts' gotcha.
- Reading a relation like `model.posts` invoked posts() on every access.

resolveAttributeMutator now:
- only probes zero-arity methods (accessors take no args; actions/relations
  like recordView(viewer) never qualify),
- caches the accessor determination per class (an arity-0 method is probed at
  most once), and
- on the read path additionally requires the key to be a model attribute
  (loaded value, append, cast, or mapped column), so even a zero-arity
  side-effecting method is never fired by a property read. Writes still probe
  (the key being assigned is an attribute by definition), so Attribute-object
  set mutators keep working during hydration and on fresh models.

Tests: an argument-taking and a zero-arity side-effecting method are never
invoked by property reads and run exactly once when called; Attribute-object
get/set accessors and appends continue to resolve.
@3m1n3nc3 3m1n3nc3 merged commit b72f4f2 into main Jul 9, 2026
6 checks passed
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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