Skip to content

Commit f38694a

Browse files
Add deepclone_hydrate() with $scoped_vars and $mangled_vars support
1 parent 288e4b5 commit f38694a

7 files changed

Lines changed: 868 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ 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.2.0] - 2026-04-12
9+
10+
### Added
11+
12+
- `deepclone_hydrate(object|string $object_or_class, array $scoped_vars = [], array $mangled_vars = []): object`
13+
instantiates a class (or takes an existing object) and sets its properties,
14+
including private, protected, and readonly ones. Handles mangled key formats
15+
(`"\0ClassName\0prop"`, `"\0*\0prop"`), SPL special cases (ArrayObject,
16+
ArrayIterator, SplObjectStorage via `"\0"` key), and preserves PHP `&`
17+
references with correct type source tracking for typed properties.
18+
- Instantiability validation for `deepclone_hydrate`: rejects the same classes
19+
as `deepclone_from_array` (abstract, interface, trait, enum, anonymous,
20+
Reflector subclasses, internal classes without serialization API). Results
21+
are cached per class for zero-cost repeated calls.
22+
- `ValueError` on invalid input: integer keys in `$mangled_vars`, non-array
23+
values in `$scoped_vars`.
24+
25+
### Changed
26+
27+
- All function parameters now use snake_case to follow PHP conventions:
28+
`$allowed_classes`, `$object_or_class`, `$scoped_vars`, `$mangled_vars`.
29+
830
## [0.1.1] - 2026-04-10
931

1032
### Fixed

README.md

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,63 @@ $json = json_encode($payload); // safe — no objects in the array
5151
$clone = deepclone_from_array(json_decode($json, true));
5252
```
5353

54+
**Fast object instantiation and hydration.** Create objects and set their
55+
properties — including private, protected, and readonly ones — without calling
56+
their constructor, faster than Reflection:
57+
58+
```php
59+
// Set private properties via scoped array (fastest path)
60+
$user = deepclone_hydrate(User::class, [
61+
User::class => ['id' => 42, 'name' => 'Alice'],
62+
AbstractEntity::class => ['createdAt' => new \DateTimeImmutable()],
63+
]);
64+
65+
// Instantiate from class name with mangled keys (same format as (array) cast)
66+
$user = deepclone_hydrate(User::class, [], ['name' => 'Alice', 'email' => 'alice@example.com']);
67+
68+
// Hydrate an existing object
69+
deepclone_hydrate($existingUser, ['User' => ['name' => 'Bob']]);
70+
```
71+
5472
## API
5573

5674
```php
57-
function deepclone_to_array(mixed $value, ?array $allowedClasses = null): array;
58-
function deepclone_from_array(array $data, ?array $allowedClasses = null): mixed;
75+
function deepclone_to_array(mixed $value, ?array $allowed_classes = null): array;
76+
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 = []): object;
5978
```
6079

61-
`$allowedClasses` restricts which classes may be serialized or deserialized
80+
`$allowed_classes` restricts which classes may be serialized or deserialized
6281
(`null` = allow all, `[]` = allow none). Case-insensitive, matching
6382
`unserialize()`'s `allowed_classes` option. Closures require `"Closure"` in
6483
the list.
6584

85+
`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.
90+
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.
94+
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.
99+
100+
The special `"\0"` key (in either parameter) sets the internal state of SPL
101+
classes:
102+
103+
```php
104+
// ArrayObject / ArrayIterator: "\0" => [$array, $flags?, $iteratorClass?]
105+
$ao = deepclone_hydrate('ArrayObject', [], ["\0" => [['x' => 1], ArrayObject::ARRAY_AS_PROPS]]);
106+
107+
// SplObjectStorage: "\0" => [$obj1, $info1, $obj2, $info2, ...]
108+
$s = deepclone_hydrate('SplObjectStorage', [], ["\0" => [$obj, 'metadata']]);
109+
```
110+
66111
## What it preserves
67112

68113
- Object identity (shared references stay shared)
@@ -76,11 +121,11 @@ the list.
76121

77122
## Error handling
78123

79-
| Exception | Thrown by | When |
80-
| ------------------------------------ | --------------------- | -------------------------------------------------------- |
81-
| `DeepClone\NotInstantiableException` | `deepclone_to_array` | Resource, anonymous class, `Reflection*`, internal class without serialization support |
82-
| `DeepClone\ClassNotFoundException` | `deepclone_from_array`| Payload references a class that doesn't exist |
83-
| `ValueError` | both | Malformed input, or class not in `$allowedClasses` |
124+
| Exception | Thrown by | When |
125+
| ------------------------------------ | ------------------------------------------ | -------------------------------------------------------- |
126+
| `DeepClone\NotInstantiableException` | `deepclone_to_array`, `deepclone_hydrate` | Resource, anonymous class, `Reflection*`, internal class without serialization support |
127+
| `DeepClone\ClassNotFoundException` | `deepclone_from_array`, `deepclone_hydrate`| Payload/class name references a class that doesn't exist |
128+
| `ValueError` | all three | Malformed input, or class not in `$allowed_classes` |
84129

85130
Both exception classes extend `\InvalidArgumentException`.
86131

@@ -114,9 +159,12 @@ sudo make install
114159
## With Symfony
115160

116161
`symfony/var-exporter` and `symfony/polyfill-deepclone` provide the same
117-
`deepclone_to_array()` / `deepclone_from_array()` functions in pure PHP.
118-
When this extension is loaded it replaces the polyfill transparently —
119-
no code change needed, just a 4–5× speedup.
162+
`deepclone_to_array()`, `deepclone_from_array()`, and `deepclone_hydrate()`
163+
functions in pure PHP. When this extension is loaded it replaces the polyfill
164+
transparently — no code change needed.
165+
166+
Symfony's `Hydrator::hydrate()` and `Instantiator::instantiate()` delegate
167+
directly to `deepclone_hydrate()`, making them thin one-liner wrappers.
120168

121169
## License
122170

0 commit comments

Comments
 (0)