Skip to content

Commit 85dd4a9

Browse files
Collapse $scoped_vars + $mangled_vars into a single $vars param
Adds DEEPCLONE_HYDRATE_MANGLED_VARS flag. $vars is scoped per-class by default; pass the flag to interpret it as the flat mangled-key array (array) $obj produces. Mixed-mode (scoped + mangled merged in one call) is removed — split into two calls if needed. Footgun guard: a NUL-prefixed key at the top level of $vars in scoped mode raises ValueError pointing at the missing flag.
1 parent cfef436 commit 85dd4a9

11 files changed

Lines changed: 146 additions & 143 deletions

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### BC Break
11+
12+
- `deepclone_hydrate()` now takes a single `$vars` array instead of
13+
separate `$scoped_vars` and `$mangled_vars`. The default interpretation
14+
is the scoped per-class shape; pass the new `DEEPCLONE_HYDRATE_MANGLED_VARS`
15+
flag in `$flags` to interpret `$vars` as a flat mangled-key array (the
16+
shape `(array) $object` produces). Old positional callers
17+
(`deepclone_hydrate($obj, [], $mangled)`) need to be updated to
18+
`deepclone_hydrate($obj, $mangled, DEEPCLONE_HYDRATE_MANGLED_VARS)`.
19+
As a footgun guard, passing a NUL-prefixed key in scoped mode raises
20+
a `ValueError` pointing at the missing flag.
21+
22+
### Added
23+
24+
- `DEEPCLONE_HYDRATE_MANGLED_VARS` constant — see BC break above.
25+
1026
### Changed
1127

1228
- `deepclone_hydrate()` silently skips readonly writes when the target

README.md

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ $user = deepclone_hydrate(User::class, [
6363
]);
6464

6565
// Instantiate from class name with mangled keys (same format as (array) cast)
66-
$user = deepclone_hydrate(User::class, [], ['name' => 'Alice', 'email' => 'alice@example.com']);
66+
$user = deepclone_hydrate(User::class,
67+
['name' => 'Alice', 'email' => 'alice@example.com'],
68+
DEEPCLONE_HYDRATE_MANGLED_VARS,
69+
);
6770

6871
// Hydrate an existing object
6972
deepclone_hydrate($existingUser, ['User' => ['name' => 'Bob']]);
@@ -74,7 +77,7 @@ deepclone_hydrate($existingUser, ['User' => ['name' => 'Bob']]);
7477
```php
7578
function deepclone_to_array(mixed $value, ?array $allowed_classes = null): array;
7679
function deepclone_from_array(array $data, ?array $allowed_classes = null): mixed;
77-
function deepclone_hydrate(object|string $object_or_class, array $scoped_vars = [], array $mangled_vars = [], int $flags = 0): object;
80+
function deepclone_hydrate(object|string $object_or_class, array $vars = [], int $flags = 0): object;
7881
```
7982

8083
`$allowed_classes` restricts which classes may be serialized or deserialized
@@ -83,19 +86,31 @@ function deepclone_hydrate(object|string $object_or_class, array $scoped_vars =
8386
the list.
8487

8588
`deepclone_hydrate()` accepts either an object to hydrate in place or a class
86-
name to instantiate without calling its constructor. Both `$scoped_vars` and
87-
`$mangled_vars` can be used together; `$mangled_vars` values are resolved
88-
and merged into `$scoped_vars` before hydration. PHP `&` references are
89-
preserved in both.
89+
name to instantiate without calling its constructor. PHP `&` references in
90+
`$vars` are preserved through the hydrate.
9091

91-
`$scoped_vars` is the **fastest path** — it writes directly to property
92-
slots with no key parsing, making it ideal for hot loops. It is keyed by
93-
declaring class name, each value being an array of property names to values.
92+
By default, `$vars` is keyed by declaring class name; each value is an array
93+
of property names to values. This is the fastest path — direct slot writes,
94+
no key parsing.
9495

95-
`$mangled_vars` accepts the same mangled key format as `(array) $object`
96-
(`"\0ClassName\0prop"` for private, `"\0*\0prop"` for protected, bare name
97-
for public/dynamic). It resolves each key to the correct scope automatically,
98-
which is handy when round-tripping with `(array)` casts.
96+
```php
97+
$user = deepclone_hydrate(User::class, [
98+
User::class => ['id' => 42, 'name' => 'Alice'],
99+
AbstractEntity::class => ['createdAt' => new \DateTimeImmutable()],
100+
]);
101+
```
102+
103+
Pass `DEEPCLONE_HYDRATE_MANGLED_VARS` in `$flags` to interpret `$vars` as a
104+
flat mangled-key array (the same shape `(array) $object` produces):
105+
`"\0ClassName\0prop"` for private, `"\0*\0prop"` for protected, bare name
106+
for public/dynamic. Each key is resolved to its scope automatically.
107+
108+
```php
109+
$user = deepclone_hydrate(User::class,
110+
['name' => 'Alice', "\0User\0email" => 'alice@example.com'],
111+
DEEPCLONE_HYDRATE_MANGLED_VARS,
112+
);
113+
```
99114

100115
`$flags` selects the write semantics for declared-property assignments:
101116

@@ -104,10 +119,11 @@ which is handy when round-tripping with `(array)` casts.
104119
| `0` (default) | `ReflectionProperty::setRawValue` — bypass set hooks, type-check, respect readonly |
105120
| `DEEPCLONE_HYDRATE_CALL_HOOKS` | `ReflectionProperty::setValue` — invoke set hooks |
106121
| `DEEPCLONE_HYDRATE_NO_LAZY_INIT` | `ReflectionProperty::setRawValueWithoutLazyInitialization` — skip the lazy initializer; realize the object when the last lazy property is set |
122+
| `DEEPCLONE_HYDRATE_MANGLED_VARS` | interpret `$vars` as a flat mangled-key array (above) |
107123

108124
`DEEPCLONE_HYDRATE_CALL_HOOKS` and `DEEPCLONE_HYDRATE_NO_LAZY_INIT` are
109-
mutually exclusive. `deepclone_from_array()` always uses the default
110-
setRawValue semantics, mirroring `unserialize()`.
125+
mutually exclusive; `MANGLED_VARS` composes with either. `deepclone_from_array()`
126+
always uses the default setRawValue semantics, mirroring `unserialize()`.
111127

112128
### Forgiving payload handling
113129

@@ -140,19 +156,24 @@ strict-type errors. They run under every mode unless noted:
140156
enum-typed properties accordingly receive the enum case, not the
141157
raw scalar.
142158

143-
The special `"\0"` key sets the internal state of SPL classes. In
144-
`$scoped_vars` it goes inside a scope entry; in `$mangled_vars` it is a
145-
flat key:
159+
The special `"\0"` key sets the internal state of SPL classes. In scoped
160+
mode it goes inside a scope entry; in `MANGLED_VARS` mode it is a flat key:
146161

147162
```php
148-
// Via $scoped_vars:
163+
// Scoped mode:
149164
$ao = deepclone_hydrate('ArrayObject', ['ArrayObject' => ["\0" => [['x' => 1]]]]);
150165

151-
// Via $mangled_vars:
152-
$ao = deepclone_hydrate('ArrayObject', [], ["\0" => [['x' => 1], ArrayObject::ARRAY_AS_PROPS]]);
166+
// MANGLED_VARS mode:
167+
$ao = deepclone_hydrate('ArrayObject',
168+
["\0" => [['x' => 1], ArrayObject::ARRAY_AS_PROPS]],
169+
DEEPCLONE_HYDRATE_MANGLED_VARS,
170+
);
153171

154172
// SplObjectStorage: "\0" => [$obj1, $info1, $obj2, $info2, ...]
155-
$s = deepclone_hydrate('SplObjectStorage', [], ["\0" => [$obj, 'metadata']]);
173+
$s = deepclone_hydrate('SplObjectStorage',
174+
["\0" => [$obj, 'metadata']],
175+
DEEPCLONE_HYDRATE_MANGLED_VARS,
176+
);
156177
```
157178

158179
## What it preserves

0 commit comments

Comments
 (0)