Skip to content

Isolate PropertyProxy attribute mutations from the shared underlying property - #151

Open
torian257x wants to merge 1 commit into
wol-soft:masterfrom
torian257x:fix/b4-propertyproxy
Open

torian257x wants to merge 1 commit into
wol-soft:masterfrom
torian257x:fix/b4-propertyproxy

Conversation

@torian257x

@torian257x torian257x commented Jun 15, 2026 •

Copy link
Copy Markdown

Summary

Rebased onto current master and narrowed to the one issue that still reproduces there.

The attribute merge-on-read you asked for in the original B4 review is already present on master — PropertyProxy::getAttributes() reads from the underlying property, so ReadOnly, WriteOnly and Deprecated are already inherited. Verified with tests; those pass on master unchanged.

What still reproduces on master is an attribute leak: PropertyProxy::addAttribute() delegates to the shared underlying property. So when two properties $ref the same $def (the second becomes a PropertyProxy sharing the first's underlying object) and something adds an attribute to one proxy — e.g. a post processor tagging a single property Deprecated — it lands on the shared underlying and every sibling reference inherits it.

Fix

PropertyProxy now stores added attributes on itself (via AttributesTrait, whose backing array is made protected) instead of on the underlying, and getAttributes() merges them in with local attributes winning by type. The existing SchemaName rename and overrideJsonPointer behaviour are unchanged.

Test

tests/Issues/Issue/Issue151Test.php — a post processor adds Deprecated to ref_two; the test asserts ref_one does not inherit it. Fails on master, passes with this change.

@torian257x

Copy link
Copy Markdown
Author

This PR addresses the B4 feedback from PR #150. I am an AI assistant working on behalf of @torian257x.

"B4 might be the correct solution but it misses some attributes (e.g. ReadOnly, WriteOnly, Deprecated). Probably the mechanic would be to merge all attributes which are not explicitly set on the proxy from the underlying property on read."

Approach: PropertyProxy::getAttributes() now merges local proxy attributes with the underlying property's attributes via a generic merge. When the same attribute FQCN appears in both lists, the proxy's local version wins. This covers ReadOnly, WriteOnly, Deprecated and any future attribute types automatically.

The old approach (enumerating SchemaName, JsonPointer, Required in processReference()) was removed. SchemaName is still set per-instance on the proxy. Everything else inherits.

Why correct: resolveReference() includes $required in its cache key, so different requiredness produces separate Properties (never a proxy). Inherited Required is always correct.

Comment thread tests/B4PropertyProxyAttributeTest.php Resolved
Comment thread src/Model/Property/PropertyProxy.php Resolved
torian257x added a commit to torian257x/php-json-schema-model-generator that referenced this pull request Jun 29, 2026
- Strip verbose comments from PropertyProxy::getAttributes() docblock
- Remove removeAttribute() from AttributesTrait (callers now use filterAttributes)
- Replace removeAttribute calls with filterAttributes
- Move test to tests/Issues/Issue/Issue151Test.php
- Add clear docblock explaining the merge mechanism
@torian257x
torian257x force-pushed the fix/b4-propertyproxy branch from 50b5d69 to c3babca Compare June 29, 2026 22:02
…property

PropertyProxy::addAttribute() delegated to the shared underlying property, so an
attribute added to one proxy (e.g. by a post processor) leaked onto every sibling
property referencing the same $def. Store proxy-added attributes locally (via
AttributesTrait) and merge them in getAttributes(), local attributes winning by type.
Inherited ReadOnly/WriteOnly/Deprecated and the SchemaName/JsonPointer overrides are
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@torian257x
torian257x force-pushed the fix/b4-propertyproxy branch from 7afdff4 to 200333d Compare July 23, 2026 14:06
@torian257x torian257x changed the title Fix B4: PropertyProxy attribute inheritance via merge getAttributes() Isolate PropertyProxy attribute mutations from the shared underlying property Jul 23, 2026
@torian257x

Copy link
Copy Markdown
Author

@wol-soft — heads up, I rebased this PR onto current master and narrowed its scope after re-checking against it:

  • The attribute merge-on-read you asked for in the original B4 review is already present on master — PropertyProxy::getAttributes() reads from the underlying property, so ReadOnly / WriteOnly / Deprecated are already inherited. So I dropped those assertions (they pass on master unchanged and don't demonstrate a defect).
  • What still reproduces on master is an attribute leak: PropertyProxy::addAttribute() mutates the shared underlying property, so adding an attribute to one proxy (e.g. a post processor tagging a single property) lands on every sibling reference to the same $def. This PR now fixes just that — the proxy stores added attributes locally (via AttributesTrait, backing array made protected) and merges them in getAttributes(), local winning by type. overrideJsonPointer and the SchemaName rename are untouched.

Proving test tests/Issues/Issue/Issue151Test.php fails on master, passes with the change. Full suite is green apart from pre-existing Issue168Test failures already on your master tip. mergeable: true.

wol-soft pushed a commit that referenced this pull request Jul 26, 2026
Use tests/Schema/Issues/151/*.json instead of inline PHP-array schemas, matching
the rest of the issue test suite, and fold the duplicate-JsonPointer regression
test into Issue151Test alongside the original leak test rather than a separate
class. The regression test now also asserts the JsonPointer attribute's actual
value and that both the composed property and its plain sibling still function
correctly, not just an attribute count.
@wol-soft

Copy link
Copy Markdown
Owner

@torian257x thanks for the cleanup.

This currently introduces a regression on top of the leak fix. PropertyProxy::addAttribute() now stores newly-added attributes on the proxy's own local array (via AttributesTrait) instead of the shared underlying property — that's what fixes the leak. But PropertyProxy::filterAttributes() still only delegates to the underlying property's filterAttributes(), so it never touches the proxy's own local attributes.

Reproduced with a schema where a $ref'd property foo is defined in both an allOf and an anyOf branch (plus a warmup sibling whose only job is to consume the first, non-proxy resolution of the shared $ref, forcing foo to resolve as a PropertyProxy). That makes JsonPointer synthesis run twice against the same proxy instance, leaving two #[JsonPointer] attributes instead of one.

Compare (based on your commit): https://github.com/wol-soft/php-json-schema-model-generator/compare/claude/pr-151-review-oaznmm
Also made the Issue151 test follow the librarys pattern to have the JSON schemas for the tests stored as JSON files rather than having them as PHP arrays in the test code.

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.

2 participants