Skip to content

Commit 0ecb300

Browse files
Drop scoped mode — mangled-only is now the only shape (v0.5.0)
deepclone_hydrate() now interprets $vars exclusively as a flat mangled-key array; the per-class scoped shape is removed along with DEEPCLONE_HYDRATE_MANGLED_VARS. PRESERVE_REFS shifts from (1<<3) to (1<<2), filling the vacated slot. Also bundled follow-ups surfaced by review: - deepclone_hydrate: reject the SPL "\0" key on classes that don't support it; reject malformed SPL payloads (odd-count pairs for SplObjectStorage, >3 ctor args for ArrayObject/ArrayIterator); cache the offsetSet lookup across SplObjectStorage iterations; gate the null → uninitialized shortcut on zend_lazy_object_initialized(obj) so lazy objects don't bypass the Reflection-based write path. - deepclone_from_array: cross-validate objectMeta wakeup flags against states entries (positive → __wakeup, negative → __unserialize), rejecting impossible meta like [0, 999] or [0, -123] that used to be accepted silently; route writes to undeclared prop names on non-stdClass objects through zend_update_property_ex() to respect overridden write handlers; throw on out-of-range obj_id in "properties" entries (was silently skipped); replace the per-object obj_classes[] pointer scan with a direct class_id index, dropping an O(N × K) step.
1 parent 55aa4ec commit 0ecb300

18 files changed

Lines changed: 610 additions & 564 deletions

CHANGELOG.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,74 @@ All notable changes to this extension will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.5.0] - 2026-04-15
9+
10+
### BC Break
11+
12+
- `deepclone_hydrate()` now interprets `$vars` exclusively as a flat
13+
mangled-key array (the shape `(array) $obj` produces). The per-class
14+
scoped shape (`[$class => ['prop' => $val]]`) is no longer supported —
15+
callers passing the old shape will hit the `"invalid mangled key"` /
16+
`"not a parent"` errors on NUL-prefixed keys, or silently create a
17+
dynamic property named after the class on non-NUL keys. Migrate by
18+
flattening: for each scope entry, use bare names for public / protected
19+
/ most-derived-private, and `"\0ScopeClass\0prop"` for parent-private
20+
props. Motivation: the two shapes were functionally equivalent (same
21+
resolution path, same slot writes), and keeping both required an
22+
intermediate scoped_props HashTable + a double-pass write. Dropping
23+
scoped mode simplifies the dispatcher into a single key-parse + write
24+
loop, and removes ~200 lines of C.
25+
- `DEEPCLONE_HYDRATE_MANGLED_VARS` constant removed — flat mangled is
26+
now the only mode, so the flag is redundant. Callers who were passing
27+
the flag can simply drop it.
28+
- `DEEPCLONE_HYDRATE_PRESERVE_REFS` flag value changed from `1 << 3` to
29+
`1 << 2` (filling the slot vacated by `DEEPCLONE_HYDRATE_MANGLED_VARS`).
30+
Symbolic references via the constant name are unaffected; anyone using
31+
the raw integer value `4` now gets `PRESERVE_REFS` instead of the old
32+
`MANGLED_VARS` — in practice both are the flags real callers pass, so
33+
the arithmetic happens to line up.
34+
35+
### Fixed
36+
37+
- `deepclone_hydrate()` rejects the SPL-internal-state `"\0"` key on
38+
objects that don't support it (anything other than `SplObjectStorage`,
39+
`ArrayObject`, `ArrayIterator`) with a `ValueError`. Previously the
40+
value silently landed in `obj->properties` as a NUL-named dynamic
41+
property.
42+
- `deepclone_hydrate()` rejects malformed SPL `"\0"` payloads: a
43+
non-even-count pair stream for `SplObjectStorage` and a payload with
44+
more than 3 ctor args for `ArrayObject` / `ArrayIterator`. Both were
45+
previously tolerated silently (odd tail dropped; excess args truncated).
46+
- `deepclone_hydrate()` no longer direct-writes `IS_PROP_UNINIT` to a
47+
lazy object's slot via the `null` → uninitialized shortcut. The
48+
shortcut is now gated on `zend_lazy_object_initialized(obj)`, so
49+
`DEEPCLONE_HYDRATE_NO_LAZY_INIT` + lazy objects fall through to the
50+
Reflection-based path instead of bypassing the lazy-props bookkeeping.
51+
- `deepclone_from_array()` cross-validates `objectMeta` wakeup flags
52+
against `states` entries: each state entry must match the sign
53+
advertised in `objectMeta[id][1]` (positive → `__wakeup`, negative →
54+
`__unserialize`), and any id flagged for state replay without a
55+
matching entry is rejected. Closes a validation hole where payloads
56+
with impossible meta like `[0, 999]` or `[0, -123]` were accepted.
57+
- `deepclone_from_array()` routes writes to undeclared property names
58+
on non-stdClass objects through `zend_update_property_ex()` instead
59+
of `zend_std_write_property()`, respecting overridden `write_property`
60+
handlers on internal classes and extensions. Matches the
61+
`deepclone_hydrate()` path.
62+
- `deepclone_from_array()` throws `ValueError` on out-of-range object
63+
ids in `"properties"` entries (previously silently skipped).
64+
65+
### Changed
66+
67+
- `deepclone_from_array()` object-creation loop drops the pointer-scan
68+
over `class_names[]` that recovered the class id per object. A
69+
per-object `uint32_t class_id` is stored directly from the
70+
`objectMeta` parse, turning an O(N × K) step into O(N) on payloads
71+
with many objects across many classes.
72+
- `deepclone_hydrate()` caches the `offsetSet` method lookup across
73+
iterations on `SplObjectStorage` `"\0"` payloads (was re-resolved
74+
by name on every entry).
75+
876
## [0.4.0] - 2026-04-15
977

1078
### BC Break

README.md

Lines changed: 43 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -56,27 +56,23 @@ properties — including private, protected, and readonly ones — without calli
5656
their constructor, faster than Reflection:
5757

5858
```php
59-
// Scoped array — keyed by declaring class
59+
// Flat bare-name array — ideal for hydrating from a flat row
60+
// (e.g. a PDO result).
6061
$user = deepclone_hydrate(User::class, [
61-
User::class => ['id' => 42, 'name' => 'Alice'],
62-
AbstractEntity::class => ['createdAt' => new \DateTimeImmutable()],
62+
'id' => 42,
63+
'name' => 'Alice',
64+
'email' => 'alice@example.com',
6365
]);
6466

65-
// Flat bare-name array — ideal for hydrating from a flat row
66-
// (e.g. a PDO result), no scope grouping needed
67-
$user = deepclone_hydrate(User::class,
68-
['id' => 42, 'name' => 'Alice', 'email' => 'alice@example.com'],
69-
DEEPCLONE_HYDRATE_MANGLED_VARS,
70-
);
71-
72-
// Mangled keys (same format as (array) $obj cast)
73-
$user = deepclone_hydrate(User::class,
74-
['name' => 'Alice', "\0User\0email" => 'alice@example.com'],
75-
DEEPCLONE_HYDRATE_MANGLED_VARS,
76-
);
67+
// Mangled keys for parent-declared private properties — same format as
68+
// (array) $obj cast produces.
69+
$user = deepclone_hydrate(User::class, [
70+
'name' => 'Alice',
71+
"\0AbstractEntity\0createdAt" => new \DateTimeImmutable(),
72+
]);
7773

7874
// Hydrate an existing object
79-
deepclone_hydrate($existingUser, ['User' => ['name' => 'Bob']]);
75+
deepclone_hydrate($existingUser, ['name' => 'Bob']);
8076
```
8177

8278
## API
@@ -97,40 +93,32 @@ name to instantiate without calling its constructor. By default, PHP `&`
9793
references in `$vars` are dropped on write; pass `DEEPCLONE_HYDRATE_PRESERVE_REFS`
9894
to keep them.
9995

100-
By default, `$vars` is keyed by declaring class name; each value is an array
101-
of property names to values:
96+
`$vars` is a flat array keyed by property name — the exact shape
97+
`(array) $obj` produces:
10298

103-
```php
104-
$user = deepclone_hydrate(User::class, [
105-
User::class => ['id' => 42, 'name' => 'Alice'],
106-
AbstractEntity::class => ['createdAt' => new \DateTimeImmutable()],
107-
]);
108-
```
99+
| key shape | target |
100+
|-----------------------------|-----------------------------------------------------------|
101+
| `"propName"` | public, protected (any declaring class), or private declared on the object's own class |
102+
| `"\0*\0propName"` | protected (the declaring class is resolved via the object) |
103+
| `"\0ClassName\0propName"` | private declared on `ClassName` — must be the object's own class or a parent |
104+
| `"\0"` | SPL internal state (SplObjectStorage / ArrayObject / ArrayIterator) |
109105

110-
Pass `DEEPCLONE_HYDRATE_MANGLED_VARS` in `$flags` to interpret `$vars` as a
111-
flat key array. Keys can be bare property names (auto-resolved to the
112-
declaring class, the ideal shape for hydrating from a PDO row), or
113-
mangled (`"\0ClassName\0prop"` for private, `"\0*\0prop"` for protected — the
114-
same shape `(array) $object` produces). The two forms can be mixed:
106+
Each key triggers one `properties_info` hash lookup followed by a direct
107+
slot write.
115108

116109
```php
117-
// Bare names — each is resolved to its declaring class automatically
118-
$user = deepclone_hydrate(User::class,
119-
['id' => 42, 'name' => 'Alice', 'email' => 'alice@example.com'],
120-
DEEPCLONE_HYDRATE_MANGLED_VARS,
121-
);
122-
123-
// Mangled keys, typical (array) cast shape
124-
$user = deepclone_hydrate(User::class,
125-
['name' => 'Alice', "\0User\0email" => 'alice@example.com'],
126-
DEEPCLONE_HYDRATE_MANGLED_VARS,
127-
);
110+
$user = deepclone_hydrate(User::class, [
111+
'id' => 42, // bare — public or own-private
112+
'name' => 'Alice',
113+
"\0*\0createdAt" => new \DateTimeImmutable(), // protected
114+
"\0AbstractEntity\0metadata" => [...], // parent-private
115+
]);
128116
```
129117

130-
Both the scoped shape and the flat-bare-name shape take a direct
131-
`properties_info` lookup per key followed by a direct slot write — there's
132-
no meaningful performance difference between the two, pick whichever
133-
representation your caller already has on hand.
118+
Bare names are enough for every public, protected, or most-derived-private
119+
property. Parent-declared private properties need the explicit
120+
`"\0ClassName\0prop"` mangled form (the engine keys them that way in the
121+
child's `properties_info`).
134122

135123
`$flags` selects the write semantics for declared-property assignments:
136124

@@ -139,11 +127,10 @@ representation your caller already has on hand.
139127
| `0` (default) | `ReflectionProperty::setRawValue` — bypass set hooks, type-check, respect readonly |
140128
| `DEEPCLONE_HYDRATE_CALL_HOOKS` | `ReflectionProperty::setValue` — invoke set hooks |
141129
| `DEEPCLONE_HYDRATE_NO_LAZY_INIT` | `ReflectionProperty::setRawValueWithoutLazyInitialization` — skip the lazy initializer; realize the object when the last lazy property is set |
142-
| `DEEPCLONE_HYDRATE_MANGLED_VARS` | interpret `$vars` as a flat mangled-key array (above) |
143130
| `DEEPCLONE_HYDRATE_PRESERVE_REFS` | preserve PHP `&` references from `$vars` onto the target property slots; by default, references are dropped (dereferenced) on write |
144131

145132
`DEEPCLONE_HYDRATE_CALL_HOOKS` and `DEEPCLONE_HYDRATE_NO_LAZY_INIT` are
146-
mutually exclusive; `MANGLED_VARS` and `PRESERVE_REFS` compose with either.
133+
mutually exclusive; `PRESERVE_REFS` composes with either.
147134
`deepclone_from_array()` always uses the default setRawValue semantics,
148135
mirroring `unserialize()`.
149136

@@ -185,24 +172,18 @@ strict-type errors. They run under every mode unless noted:
185172
enum-typed properties accordingly receive the enum case, not the
186173
raw scalar.
187174

188-
The special `"\0"` key sets the internal state of SPL classes. In scoped
189-
mode it goes inside a scope entry; in `MANGLED_VARS` mode it is a flat key:
175+
The special `"\0"` key sets the internal state of SPL classes:
190176

191177
```php
192-
// Scoped mode:
193-
$ao = deepclone_hydrate('ArrayObject', ['ArrayObject' => ["\0" => [['x' => 1]]]]);
194-
195-
// MANGLED_VARS mode:
196-
$ao = deepclone_hydrate('ArrayObject',
197-
["\0" => [['x' => 1], ArrayObject::ARRAY_AS_PROPS]],
198-
DEEPCLONE_HYDRATE_MANGLED_VARS,
199-
);
200-
201-
// SplObjectStorage: "\0" => [$obj1, $info1, $obj2, $info2, ...]
202-
$s = deepclone_hydrate('SplObjectStorage',
203-
["\0" => [$obj, 'metadata']],
204-
DEEPCLONE_HYDRATE_MANGLED_VARS,
205-
);
178+
// ArrayObject / ArrayIterator — ["\0" => [$array, $flags?, $iteratorClass?]]
179+
$ao = deepclone_hydrate('ArrayObject', [
180+
"\0" => [['x' => 1, 'y' => 2], ArrayObject::ARRAY_AS_PROPS],
181+
]);
182+
183+
// SplObjectStorage — ["\0" => [$obj1, $info1, $obj2, $info2, ...]]
184+
$s = deepclone_hydrate('SplObjectStorage', [
185+
"\0" => [$obj, 'metadata'],
186+
]);
206187
```
207188

208189
## What it preserves

0 commit comments

Comments
 (0)