Skip to content

Run user type initializers before the catch-all ObjectTypeInitializer#44

Merged
ruudk merged 1 commit intomainfrom
fix
Apr 18, 2026
Merged

Run user type initializers before the catch-all ObjectTypeInitializer#44
ruudk merged 1 commit intomainfrom
fix

Conversation

@ruudk
Copy link
Copy Markdown
Owner

@ruudk ruudk commented Apr 18, 2026

The @hook directive added in #43 forwards $this->hooks into child constructors via ObjectTypeInitializer, which consults a ClassHookUsageRegistry owned by PlanExecutor. Projects that registered their own ObjectTypeInitializer via Config::withTypeInitializer() to customize a specific value object (Money, Url, …) broke the hook forwarding: the dedup-by-class-name in DelegatingTypeInitializer let the user instance — constructed without the registry — overwrite the built-in one, so every new Child($data) was emitted without hooks.

Splitting the two roles removes the footgun. ObjectTypeInitializer is now strictly the internal catch-all (registry injected via constructor, no extension surface). Userland registers plain TypeInitializer instances that match only their specific class; the delegation chain runs them ahead of the catch-all, so type-specific handlers win for their own types and everything else falls through to the built-in one with hooks intact.

Breaking change for any config that wrapped a handler in ObjectTypeInitializer — unwrap it and pass the inner TypeInitializer directly to withTypeInitializer().

The `@hook` directive added in #43 forwards `$this->hooks` into child
constructors via `ObjectTypeInitializer`, which consults a
`ClassHookUsageRegistry` owned by `PlanExecutor`. Projects that registered
their own `ObjectTypeInitializer` via `Config::withTypeInitializer()` to
customize a specific value object (Money, Url, …) broke the hook
forwarding: the dedup-by-class-name in `DelegatingTypeInitializer` let
the user instance — constructed without the registry — overwrite the
built-in one, so every `new Child($data)` was emitted without hooks.

Splitting the two roles removes the footgun. `ObjectTypeInitializer` is
now strictly the internal catch-all (registry injected via constructor,
no extension surface). Userland registers plain `TypeInitializer`
instances that match only their specific class; the delegation chain
runs them ahead of the catch-all, so type-specific handlers win for
their own types and everything else falls through to the built-in one
with hooks intact.

Breaking change for any config that wrapped a handler in
`ObjectTypeInitializer` — unwrap it and pass the inner `TypeInitializer`
directly to `withTypeInitializer()`.
@ruudk ruudk merged commit 5167fd5 into main Apr 18, 2026
3 checks passed
@ruudk ruudk deleted the fix branch April 18, 2026 12:34
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