Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ All notable changes to this extension will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-04-12

### Added

- `deepclone_hydrate(object|string $object_or_class, array $scoped_vars = [], array $mangled_vars = []): object` —
instantiates a class (or takes an existing object) and sets its properties,
including private, protected, and readonly ones. Handles mangled key formats
(`"\0ClassName\0prop"`, `"\0*\0prop"`), SPL special cases (ArrayObject,
ArrayIterator, SplObjectStorage via `"\0"` key), and preserves PHP `&`
references with correct type source tracking for typed properties.
- Instantiability validation for `deepclone_hydrate`: rejects the same classes
as `deepclone_from_array` (abstract, interface, trait, enum, anonymous,
Reflector subclasses, internal classes without serialization API). Results
are cached per class for zero-cost repeated calls.
- `ValueError` on invalid input: integer keys in `$mangled_vars`, non-array
values in `$scoped_vars`.

### Changed

- All function parameters now use snake_case to follow PHP conventions:
`$allowed_classes`, `$object_or_class`, `$scoped_vars`, `$mangled_vars`.

## [0.1.1] - 2026-04-10

### Fixed
Expand Down
74 changes: 63 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,67 @@ $json = json_encode($payload); // safe — no objects in the array
$clone = deepclone_from_array(json_decode($json, true));
```

**Fast object instantiation and hydration.** Create objects and set their
properties — including private, protected, and readonly ones — without calling
their constructor, faster than Reflection:

```php
// Set private properties via scoped array (fastest path)
$user = deepclone_hydrate(User::class, [
User::class => ['id' => 42, 'name' => 'Alice'],
AbstractEntity::class => ['createdAt' => new \DateTimeImmutable()],
]);

// Instantiate from class name with mangled keys (same format as (array) cast)
$user = deepclone_hydrate(User::class, [], ['name' => 'Alice', 'email' => 'alice@example.com']);

// Hydrate an existing object
deepclone_hydrate($existingUser, ['User' => ['name' => 'Bob']]);
```

## API

```php
function deepclone_to_array(mixed $value, ?array $allowedClasses = null): array;
function deepclone_from_array(array $data, ?array $allowedClasses = null): mixed;
function deepclone_to_array(mixed $value, ?array $allowed_classes = null): array;
function deepclone_from_array(array $data, ?array $allowed_classes = null): mixed;
function deepclone_hydrate(object|string $object_or_class, array $scoped_vars = [], array $mangled_vars = []): object;
```

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

`deepclone_hydrate()` accepts either an object to hydrate in place or a class
name to instantiate without calling its constructor. Both `$scoped_vars` and
`$mangled_vars` can be used together; `$mangled_vars` values are resolved
and merged into `$scoped_vars` before hydration. PHP `&` references are
preserved in both.

`$scoped_vars` is the **fastest path** — it writes directly to property
slots with no key parsing, making it ideal for hot loops. It is keyed by
declaring class name, each value being an array of property names to values.

`$mangled_vars` accepts the same mangled key format as `(array) $object`
(`"\0ClassName\0prop"` for private, `"\0*\0prop"` for protected, bare name
for public/dynamic). It resolves each key to the correct scope automatically,
which is handy when round-tripping with `(array)` casts.

The special `"\0"` key sets the internal state of SPL classes. In
`$scoped_vars` it goes inside a scope entry; in `$mangled_vars` it is a
flat key:

```php
// Via $scoped_vars:
$ao = deepclone_hydrate('ArrayObject', ['ArrayObject' => ["\0" => [['x' => 1]]]]);

// Via $mangled_vars:
$ao = deepclone_hydrate('ArrayObject', [], ["\0" => [['x' => 1], ArrayObject::ARRAY_AS_PROPS]]);

// SplObjectStorage: "\0" => [$obj1, $info1, $obj2, $info2, ...]
$s = deepclone_hydrate('SplObjectStorage', [], ["\0" => [$obj, 'metadata']]);
```

## What it preserves

- Object identity (shared references stay shared)
Expand All @@ -76,11 +125,11 @@ the list.

## Error handling

| Exception | Thrown by | When |
| ------------------------------------ | --------------------- | -------------------------------------------------------- |
| `DeepClone\NotInstantiableException` | `deepclone_to_array` | Resource, anonymous class, `Reflection*`, internal class without serialization support |
| `DeepClone\ClassNotFoundException` | `deepclone_from_array`| Payload references a class that doesn't exist |
| `ValueError` | both | Malformed input, or class not in `$allowedClasses` |
| Exception | Thrown by | When |
| ------------------------------------ | ------------------------------------------ | -------------------------------------------------------- |
| `DeepClone\NotInstantiableException` | `deepclone_to_array`, `deepclone_hydrate` | Resource, anonymous class, `Reflection*`, internal class without serialization support |
| `DeepClone\ClassNotFoundException` | `deepclone_from_array`, `deepclone_hydrate`| Payload/class name references a class that doesn't exist |
| `ValueError` | all three | Malformed input, or class not in `$allowed_classes` |

Both exception classes extend `\InvalidArgumentException`.

Expand Down Expand Up @@ -114,9 +163,12 @@ sudo make install
## With Symfony

`symfony/var-exporter` and `symfony/polyfill-deepclone` provide the same
`deepclone_to_array()` / `deepclone_from_array()` functions in pure PHP.
When this extension is loaded it replaces the polyfill transparently —
no code change needed, just a 4–5× speedup.
`deepclone_to_array()`, `deepclone_from_array()`, and `deepclone_hydrate()`
functions in pure PHP. When this extension is loaded it replaces the polyfill
transparently — no code change needed.

Symfony's `Hydrator::hydrate()` and `Instantiator::instantiate()` delegate
directly to `deepclone_hydrate()`, making them thin one-liner wrappers.

## License

Expand Down
Loading