Skip to content

Commit b838c3e

Browse files
Drop scoped mode — mangled-only is now the only shape (v0.5.0)
The scoped shape `[$className => ['prop' => $val]]` and the flat mangled shape `['prop' => $val, "\0Class\0priv" => $val2]` were functionally equivalent: both resolved each key to the same `property_info` entry and performed the same direct slot write. Keeping both required an intermediate `scoped_props` HashTable built by the `MANGLED_VARS` path, a double-pass write loop, and a footgun guard to catch users passing mangled keys without the flag. Drop scoped mode entirely. `$vars` is always the flat `(array)`-cast shape, which is also what Doctrine-style ORM hydrators get from PDO. The parser now writes directly during the key-parse pass — no `scoped_props` accumulation, no second-pass loop. ### BC breaks - Scoped-shape input `[$class => ['prop' => $val]]` is no longer understood. Callers migrate by flattening: bare names for public / protected / most-derived-private, `"\0Scope\0prop"` for parent-declared private. - `DEEPCLONE_HYDRATE_MANGLED_VARS` constant removed (the mode is now implicit). Drop the flag from `$flags` arguments. - `DEEPCLONE_HYDRATE_PRESERVE_REFS` moves from `1 << 3` to `1 << 2`, filling the slot vacated by `MANGLED_VARS`. Symbolic references via the constant name are unaffected. - A mangled-form key (`"\0*\0prop"` or `"\0Class\0prop"`) that doesn't resolve to a declared slot on the resolved scope now raises `ValueError("key scope X does not declare a Y property")` instead of silently creating a dynamic property. A mangled-form key whose class name isn't `obj_ce` or a parent still raises the existing `"key scope X is not a parent of Y"` error. ### Code impact - `deepclone.c`: -480 / +330 lines in `deepclone_hydrate()`. The scoped-mode second-pass write loop (~200 lines), the `MANGLED_VARS→scoped_props` intermediate builder (~140 lines), and the footgun guard are gone. - `deepclone.stub.php` + regenerated `deepclone_arginfo.h`: `DEEPCLONE_HYDRATE_MANGLED_VARS` constant entry removed. - `README.md`: rewritten the `$vars` section around the `(array)`-cast shape, with a table of key shapes and their targets. The "scoped mode" examples and the "which shape is faster" paragraph are gone. - `CHANGELOG.md`: 0.5.0 entry documenting the BC break. - `php_deepclone.h`: `PHP_DEEPCLONE_VERSION` `0.4.0` → `0.5.0`. ### Tests All 30 phpt tests converted and passing locally on PHP 8.4 NTS. New coverage: - Bare name reaches a parent-private slot when the name is unambiguous. - Mangled-form key with undeclared prop raises "does not declare" (`"\0*\0undeclared"` and `"\0Parent\0undeclared"` cases).
1 parent 55aa4ec commit b838c3e

16 files changed

Lines changed: 395 additions & 540 deletions

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,33 @@ 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+
835
## [0.4.0] - 2026-04-15
936

1037
### 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)