Skip to content

GH-76: Decide the extension story — strategies internal, TypeHandlerInterface public#122

Merged
magicsunday merged 2 commits into
mainfrom
GH-76
Jul 19, 2026
Merged

GH-76: Decide the extension story — strategies internal, TypeHandlerInterface public#122
magicsunday merged 2 commits into
mainfrom
GH-76

Conversation

@magicsunday

Copy link
Copy Markdown
Owner

Fixes #76.

The issue asks whether the value-conversion strategies are a public extension point or internal, and whether to align two supports() signatures. The docs had already answered it and the code had not: TypeHandlerInterface is documented everywhere, ValueConversionStrategyInterface nowhere, and there is no public hook to register a strategy — ValueConverter is built internally and never exposed.

The decision

Every Value\Strategy\* class, the trait, ValueConversionStrategyInterface and ValueConverter are now @internal, each pointing at TypeHandlerInterface via addTypeHandler() as the supported path. AGENTS.md and docs/API.md say the same.

This makes the two supports() signatures a non-issue rather than drift to reconcile: the internal one carries a MappingContext the public handler contract does not, and they serve different layers — no alignment, and no major-version break, needed. The audit on the issue records this is also what unblocks #87 in a patch rather than a major: if the chain order is not a contract, converging the two value-conversion entry points is a behaviour-preserving refactor.

Scope additions from the audit

ClassResolver::add() accepted only a Closure while the constructor $classMap accepts class-string|Closure — a static SdkFoo => Foo mapping could be given at construction but not at runtime. Both add() and the public addCustomClassMapEntry() now accept Closure|string, validated at registration like the constructor, with a test driving a static mapping through the public API.

register()/registerHandler() on CustomTypeRegistry are not the duplicate the issue supposed — register() is the callable convenience that wraps a ClosureTypeHandler and delegates to the handler primitive. Left as is.

What review caught

  • The @internal note in docs/API.md was placed between a table's delimiter and body, breaking the whole Methods table in GitHub Markdown. Moved above the header.
  • "consulted first" was inaccurate — NullValueConversionStrategy runs before the custom-type strategy, so a handler never sees a null. Corrected to "ahead of the built-in deciding strategies (after null handling)".
  • The add() widening was dead code as first committed (its only caller still took Closure); completing the public entry point made it live.

Verification

composer ci:test green, verified by exit code: 384 tests, 1233 assertions; PHPStan max, Rector, CGL and CPD clean.

…nterface public

The issue asks whether the conversion strategies are a public extension
point or internal. The docs already answered it and the code had not:
TypeHandlerInterface is documented everywhere, ValueConversionStrategyInterface
nowhere, and there is no public hook to register a strategy - ValueConverter
is built internally and never exposed. So this is one documented contract
plus an implementation detail that happened to be public.

Resolved that way. Every Value\Strategy\* class, the trait,
ValueConversionStrategyInterface and ValueConverter are now @internal, each
pointing at TypeHandlerInterface via addTypeHandler() as the supported path.
AGENTS.md and docs/API.md say the same.

This makes the two supports() signatures a non-issue rather than drift to
reconcile: the internal one carries a MappingContext the public handler
contract does not, and they are meant to serve different layers, so no
alignment - and no major-version break - is needed. The audit note on the
issue records this is also what unblocks #87 in a patch rather than a major:
if the chain order is not a contract, converging the entry points is a
refactor.

Two smaller items the audit added to scope:

ClassResolver::add() accepted only a Closure while the constructor $classMap
accepts class-string|Closure, so a static SdkFoo => Foo mapping could be
given at construction but not at runtime without a trivial closure wrapper.
add() now takes Closure|string and validates a class-string target at
registration, as the constructor does. resolve() already handled both.

register()/registerHandler() on CustomTypeRegistry are NOT the duplicate the
issue supposed - register() is the callable convenience that wraps a
ClosureTypeHandler and delegates to the handler primitive, a two-tier API.
Left as is. And the AGENTS.md "supports/map" wording the audit flagged is
already "supports()/convert()" at the point that matters; no fix needed.
…ening

Three review findings.

The @internal note in docs/API.md sat between a table's delimiter row and
its body, which ends the table in GitHub-flavoured Markdown - the whole
public Methods reference then rendered as raw piped text. Moved above the
header.

The note also said a handler is "consulted first / ahead of them". It is
not: NullValueConversionStrategy runs before the custom-type strategy, so a
handler never sees a null. Corrected in both docs/API.md and AGENTS.md to
"ahead of the built-in deciding strategies, after null handling".

And the ClassResolver::add() widening was dead code as committed: its only
non-test caller, addCustomClassMapEntry(), still took Closure only, so the
new string branch was reachable from a unit test alone. The architecture
review was right to flag it. Rather than drop it, I completed the chain -
addCustomClassMapEntry() now accepts Closure|string too, so a static
SdkFoo => Foo mapping is registrable at runtime through the public API, and
a test drives it that way. The add() docblock also notes an allowlist is
inert beside a static target, since such a target has no payload-derived
choice to constrain.
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@magicsunday
magicsunday merged commit 0e01625 into main Jul 19, 2026
19 checks passed
@magicsunday
magicsunday deleted the GH-76 branch July 19, 2026 21:09
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.

Clarify strategy extensibility and align the two supports() signatures

1 participant