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
40 changes: 30 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,22 @@ properties — including private, protected, and readonly ones — without calli
their constructor, faster than Reflection:

```php
// Set private properties via scoped array (fastest path)
// Scoped array — keyed by declaring class
$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)
// Flat bare-name array — ideal for hydrating from a flat row
// (e.g. a PDO result), no scope grouping needed
$user = deepclone_hydrate(User::class,
['name' => 'Alice', 'email' => 'alice@example.com'],
['id' => 42, 'name' => 'Alice', 'email' => 'alice@example.com'],
DEEPCLONE_HYDRATE_MANGLED_VARS,
);

// Mangled keys (same format as (array) $obj cast)
$user = deepclone_hydrate(User::class,
['name' => 'Alice', "\0User\0email" => 'alice@example.com'],
DEEPCLONE_HYDRATE_MANGLED_VARS,
);

Expand All @@ -86,12 +93,12 @@ function deepclone_hydrate(object|string $object_or_class, array $vars = [], int
the list.

`deepclone_hydrate()` accepts either an object to hydrate in place or a class
name to instantiate without calling its constructor. PHP `&` references in
`$vars` are preserved through the hydrate.
name to instantiate without calling its constructor. By default, PHP `&`
references in `$vars` are dropped on write; pass `DEEPCLONE_HYDRATE_PRESERVE_REFS`
to keep them.

By default, `$vars` is keyed by declaring class name; each value is an array
of property names to values. This is the fastest path — direct slot writes,
no key parsing.
of property names to values:

```php
$user = deepclone_hydrate(User::class, [
Expand All @@ -101,17 +108,30 @@ $user = deepclone_hydrate(User::class, [
```

Pass `DEEPCLONE_HYDRATE_MANGLED_VARS` in `$flags` to interpret `$vars` as a
flat mangled-key array (the same shape `(array) $object` produces):
`"\0ClassName\0prop"` for private, `"\0*\0prop"` for protected, bare name
for public/dynamic. Each key is resolved to its scope automatically.
flat key array. Keys can be bare property names (auto-resolved to the
declaring class, the ideal shape for hydrating from a PDO row), or
mangled (`"\0ClassName\0prop"` for private, `"\0*\0prop"` for protected — the
same shape `(array) $object` produces). The two forms can be mixed:

```php
// Bare names — each is resolved to its declaring class automatically
$user = deepclone_hydrate(User::class,
['id' => 42, 'name' => 'Alice', 'email' => 'alice@example.com'],
DEEPCLONE_HYDRATE_MANGLED_VARS,
);

// Mangled keys, typical (array) cast shape
$user = deepclone_hydrate(User::class,
['name' => 'Alice', "\0User\0email" => 'alice@example.com'],
DEEPCLONE_HYDRATE_MANGLED_VARS,
);
```

Both the scoped shape and the flat-bare-name shape take a direct
`properties_info` lookup per key followed by a direct slot write — there's
no meaningful performance difference between the two, pick whichever
representation your caller already has on hand.

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

| Flag | Semantics |
Expand Down
19 changes: 19 additions & 0 deletions deepclone.c
Original file line number Diff line number Diff line change
Expand Up @@ -3101,6 +3101,25 @@ PHP_FUNCTION(deepclone_hydrate)
/* Bare name — find declaring class */
real_name = prop_key;
zend_property_info *pi = zend_hash_find_ptr(&obj_ce->properties_info, real_name);

/* Fast path: the resolved property has a real backing slot (typed or
* non-hooked). Write directly via dc_write_backed_property and skip the
* scoped_props accumulation + second-pass write. This is the hot case
* for Doctrine-style `fetchAll()` flows where the row is a flat dict
* of declared field names. */
if (pi && !(pi->flags & ZEND_ACC_STATIC) && dc_is_backed_declared_property(pi)) {
zval *v = prop_val;
if (!(flags & DEEPCLONE_HYDRATE_PRESERVE_REFS)) {
ZVAL_DEREF(v);
}
if (UNEXPECTED(!dc_write_backed_property(Z_OBJ(obj_zval), pi, real_name, v, flags))) {
if (scoped_owned) zval_ptr_dtor(&local_scoped);
zval_ptr_dtor(&obj_zval);
RETURN_THROWS();
}
continue;
}

if (pi && !(pi->flags & ZEND_ACC_STATIC)) {
scope_str = pi->ce->name;
/* For private-set properties, use the declaring class as write scope */
Expand Down