Skip to content

Review fixups from @arnaudlb against v0.4.0#14

Merged
nicolas-grekas merged 4 commits intomainfrom
review-fixups
Apr 15, 2026
Merged

Review fixups from @arnaudlb against v0.4.0#14
nicolas-grekas merged 4 commits intomainfrom
review-fixups

Conversation

@nicolas-grekas
Copy link
Copy Markdown
Member

Addresses review feedback from @arnaudlb on the v0.4.0 work (PR #12), split into four semantic commits so each concern can be reviewed (and reverted) independently.

Commits

1. Simplify dc_is_backed_declared_property()

!(pi->flags & ZEND_ACC_VIRTUAL) already implies pi->offset == ZEND_VIRTUAL_PROPERTY_OFFSET (both set together for virtual props on the property_info side), and IS_HOOKED_PROPERTY_OFFSET() is only meaningful on offsets returned by zend_get_property_offset() — not on the raw pi->offset. Collapse to a single bitmask test.

2. Narrow backed-enum scalar cast to Enum / ?Enum only

The scalar→backed-enum coercion shouldn't fire for union types that already accept the scalar literally (e.g. Suit|string|int). Add (ZEND_TYPE_PURE_MASK(pi->type) & ~MAY_BE_NULL) == 0 so only pure-enum types (nullable or not) trigger the cast.

3. Route writes on lazy objects through zend_update_property_ex()

A lazy ghost keeps its backing slots uninitialized until the realization hook fires. Writing directly via OBJ_PROP would bypass that hook and leave the object in a half-initialized state. At the top of dc_write_backed_property(), check zend_lazy_object_initialized() and delegate to the engine's property write path in the default mode. DEEPCLONE_HYDRATE_NO_LAZY_INIT keeps its opt-out fast path.

4. Route dynamic-property fallback through zend_update_property_ex()

zend_std_write_property() skips any overridden write_property handler. For internal classes or user objects that swap handlers at create_object time, that bypasses semantic validation the class expects. Switch to zend_update_property_ex(scope_ce ?: obj_ce, obj, name, value).

Notes on not-applied comments

  • A2 (null → unset on non-nullable typed) and A3 (scalar → backed-enum cast): kept property-type-only as discussed — the coercions are driven by the target type at hydration time, which is stable within a single execution. Adding a hook or changing a property type is a code change, not a runtime surprise. Scenario A3 also gets the type-narrowing from commit 2.

Test plan

  • 30/30 .phpt tests green locally (PHP 8.4, NTS)
  • CI green on the matrix

Per @arnaudlb's review: !(pi->flags & ZEND_ACC_VIRTUAL) already implies
pi->offset == ZEND_VIRTUAL_PROPERTY_OFFSET (they're the same marker on
the property_info side), and IS_HOOKED_PROPERTY_OFFSET() is only
meaningful on offsets returned by zend_get_property_offset() — not on
the raw pi->offset stored in properties_info. The extra checks were
dead code. Collapse to a single bitmask test.
Per @arnaudlb's review: the scalar→backed-enum coercion should not fire
when the property type already accepts the scalar literally, e.g.
`Suit|string|int`. In that case the caller opted into accepting the raw
scalar and promoting it to an enum case would be a surprising silent
conversion.

Add `(ZEND_TYPE_PURE_MASK(pi->type) & ~MAY_BE_NULL) == 0` so the cast
only runs for pure-enum types (`Enum`) and their nullable variant
(`?Enum`). Union types get the engine's standard type check.
Per @arnaudlb's review: a lazy ghost has its backing slots uninitialized
until the realization hook fires. Writing directly via OBJ_PROP / slot
manipulation on such an object would bypass that hook and leave it in a
half-initialized state (lazy counters out of sync, lazy-init callback
never called).

At the top of dc_write_backed_property(), check
zend_lazy_object_initialized() and, in the default mode, delegate to
the engine's property write path which handles realization. The
DEEPCLONE_HYDRATE_NO_LAZY_INIT flag keeps its existing opt-out fast
path (skips lazy-init intentionally via Reflection).
Per @arnaudlb's review: zend_std_write_property() skips the object's
write_property handler. For internal classes (or user objects that
swap handlers at create_object time), that can bypass semantic
validation the class expects on property writes.

Switch to zend_update_property_ex(scope_ce ?: obj_ce, obj, name, value)
which dispatches through the handler, keeping compatibility with
overridden write paths. Scope falls back to obj_ce when scope_ce is
NULL (stdClass-scoped dynamic prop on a user class).
@nicolas-grekas nicolas-grekas merged commit 55aa4ec into main Apr 15, 2026
@nicolas-grekas nicolas-grekas deleted the review-fixups branch April 16, 2026 12: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.

1 participant