diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2a707..cff582d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ 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.6.0] - 2026-04-26 + +### Removed + +- `deepclone_hydrate()` no longer treats the special `"\0"` key as SPL + internal state. `ArrayObject`, `ArrayIterator`, and `SplObjectStorage` + all ship `__serialize` / `__unserialize` since PHP 7.4 — callers can + populate them by instantiating with `deepclone_hydrate()` and calling + `__unserialize()` with the documented array shape, or by round-tripping + via `deepclone_from_array()` which routes through `__unserialize` + natively. The mangled-key resolution path (`"propName"`, `"\0*\0prop"`, + `"\0Class\0prop"`) is unchanged. + + This removes ~80 lines of bespoke SPL handling — `offsetSet` loops, + constructor invocation, packed-array shape validation, error paths — + that duplicated what the classes natively expose. Symfony's + `Hydrator::hydrate()` / `Instantiator::instantiate()` retain BC by + translating the legacy `"\0"` shape to `__unserialize()` in user-land. + ## [0.5.1] - 2026-04-17 ### Fixed diff --git a/README.md b/README.md index 200c8b4..1dd4143 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,6 @@ to keep them. | `"propName"` | public, protected (any declaring class), or private declared on the object's own class | | `"\0*\0propName"` | protected (the declaring class is resolved via the object) | | `"\0ClassName\0propName"` | private declared on `ClassName` — must be the object's own class or a parent | -| `"\0"` | SPL internal state (SplObjectStorage / ArrayObject / ArrayIterator) | Each key triggers one `properties_info` hash lookup followed by a direct slot write. @@ -172,18 +171,18 @@ strict-type errors. They run under every mode unless noted: enum-typed properties accordingly receive the enum case, not the raw scalar. -The special `"\0"` key sets the internal state of SPL classes: +SPL classes that hold internal state (`ArrayObject`, `ArrayIterator`, +`SplObjectStorage`, …) have shipped `__serialize` / `__unserialize` since +PHP 7.4. To populate them, instantiate with `deepclone_hydrate()` and call +`__unserialize()` with the array shape the class documents — or just use +`deepclone_from_array()`, which routes through `__unserialize` natively. ```php -// ArrayObject / ArrayIterator — ["\0" => [$array, $flags?, $iteratorClass?]] -$ao = deepclone_hydrate('ArrayObject', [ - "\0" => [['x' => 1, 'y' => 2], ArrayObject::ARRAY_AS_PROPS], -]); +$ao = deepclone_hydrate('ArrayObject'); +$ao->__unserialize([ArrayObject::ARRAY_AS_PROPS, ['x' => 1, 'y' => 2], []]); -// SplObjectStorage — ["\0" => [$obj1, $info1, $obj2, $info2, ...]] -$s = deepclone_hydrate('SplObjectStorage', [ - "\0" => [$obj, 'metadata'], -]); +$s = deepclone_hydrate('SplObjectStorage'); +$s->__unserialize([[$obj1, 'info1', $obj2, 'info2'], []]); ``` ## What it preserves diff --git a/deepclone.c b/deepclone.c index d9317ff..268efeb 100644 --- a/deepclone.c +++ b/deepclone.c @@ -59,8 +59,6 @@ #include "Zend/zend_interfaces.h" #include "ext/spl/spl_iterators.h" #include "ext/spl/spl_exceptions.h" -#include "ext/spl/spl_array.h" -#include "ext/spl/spl_observer.h" /* ext/reflection's class entries are PHPAPI but Debian's php-dev does not * ship ext/reflection/php_reflection.h. Forward-declare what we use; the @@ -3093,8 +3091,6 @@ PHP_FUNCTION(deepclone_hydrate) * "\0*\0propName" → protected (declaring class resolved via obj_ce) * "\0ClassName\0propName" → private declared on ClassName (must be obj_ce * or a parent; interfaces are not valid scopes) - * "\0" → SPL internal state (SplObjectStorage/ - * ArrayObject/ArrayIterator) */ zend_ulong prop_idx; zend_string *prop_key; @@ -3111,91 +3107,6 @@ PHP_FUNCTION(deepclone_hydrate) const char *key = ZSTR_VAL(prop_key); size_t key_len = ZSTR_LEN(prop_key); - /* "\0" = internal state for SplObjectStorage/ArrayObject/ArrayIterator */ - if (key_len == 1 && key[0] == '\0') { - if (UNEXPECTED(Z_TYPE_P(prop_val) != IS_ARRAY)) { - zend_value_error("deepclone_hydrate(): Argument #2 ($vars) special \"\\0\" value must be of type array, %s given", - zend_zval_value_name(prop_val)); - if (prop_key_owned) zend_string_release(prop_key); - zval_ptr_dtor(&obj_zval); - RETURN_THROWS(); - } - if (instanceof_function(obj_ce, spl_ce_SplObjectStorage)) { - /* [$obj1, $info1, $obj2, $info2, ...] → offsetSet(obj, info). - * Count must be even (pair-aligned); packed/contiguous indexing - * is required since the format is a flat pair stream. */ - uint32_t count = zend_hash_num_elements(Z_ARRVAL_P(prop_val)); - if (UNEXPECTED(count & 1)) { - zend_value_error("deepclone_hydrate(): Argument #2 ($vars) special \"\\0\" value for %s must have an even number of entries, %u given", - ZSTR_VAL(obj_ce->name), count); - if (prop_key_owned) zend_string_release(prop_key); - zval_ptr_dtor(&obj_zval); - RETURN_THROWS(); - } - zend_function *offset_set_fn = NULL; - for (uint32_t i = 0; i < count; i += 2) { - zval *zkey = zend_hash_index_find(Z_ARRVAL_P(prop_val), i); - zval *zinfo = zend_hash_index_find(Z_ARRVAL_P(prop_val), i + 1); - if (UNEXPECTED(!zkey || !zinfo)) { - zend_value_error("deepclone_hydrate(): Argument #2 ($vars) special \"\\0\" value for %s must be a packed array of [obj, info] pairs", - ZSTR_VAL(obj_ce->name)); - if (prop_key_owned) zend_string_release(prop_key); - zval_ptr_dtor(&obj_zval); - RETURN_THROWS(); - } - zend_call_method_with_2_params(obj, obj_ce, &offset_set_fn, "offsetset", NULL, zkey, zinfo); - if (UNEXPECTED(EG(exception))) { - if (prop_key_owned) zend_string_release(prop_key); - zval_ptr_dtor(&obj_zval); - RETURN_THROWS(); - } - } - } else if (instanceof_function(obj_ce, spl_ce_ArrayObject) - || instanceof_function(obj_ce, spl_ce_ArrayIterator)) { - /* [$array, $flags?, $iteratorClass?] — call constructor */ - uint32_t argc = zend_hash_num_elements(Z_ARRVAL_P(prop_val)); - if (UNEXPECTED(argc > 3)) { - zend_value_error("deepclone_hydrate(): Argument #2 ($vars) special \"\\0\" value for %s accepts at most 3 entries, %u given", - ZSTR_VAL(obj_ce->name), argc); - if (prop_key_owned) zend_string_release(prop_key); - zval_ptr_dtor(&obj_zval); - RETURN_THROWS(); - } - zend_function *ctor = obj_ce->constructor; - if (ctor) { - zval args[3]; - for (uint32_t a = 0; a < argc; a++) { - zval *arg = zend_hash_index_find(Z_ARRVAL_P(prop_val), a); - if (UNEXPECTED(!arg)) { - zend_value_error("deepclone_hydrate(): Argument #2 ($vars) special \"\\0\" value for %s must be a packed array", - ZSTR_VAL(obj_ce->name)); - if (prop_key_owned) zend_string_release(prop_key); - zval_ptr_dtor(&obj_zval); - RETURN_THROWS(); - } - ZVAL_COPY_VALUE(&args[a], arg); - } - zval retval; - ZVAL_UNDEF(&retval); - zend_call_known_function(ctor, obj, obj_ce, &retval, argc, args, NULL); - zval_ptr_dtor(&retval); - if (UNEXPECTED(EG(exception))) { - if (prop_key_owned) zend_string_release(prop_key); - zval_ptr_dtor(&obj_zval); - RETURN_THROWS(); - } - } - } else { - zend_value_error("deepclone_hydrate(): Argument #2 ($vars) uses the special \"\\0\" key, which is only supported for SplObjectStorage, ArrayObject, and ArrayIterator; got \"%s\"", - ZSTR_VAL(obj_ce->name)); - if (prop_key_owned) zend_string_release(prop_key); - zval_ptr_dtor(&obj_zval); - RETURN_THROWS(); - } - if (prop_key_owned) zend_string_release(prop_key); - continue; - } - /* Resolve (scope_ce, real_name) from the mangled key shape. * is_mangled is set when the key started with NUL — if the resolved * property_info lookup then misses, we reject the key rather than diff --git a/php_deepclone.h b/php_deepclone.h index 9975c08..2a0cd50 100644 --- a/php_deepclone.h +++ b/php_deepclone.h @@ -4,7 +4,7 @@ extern zend_module_entry deepclone_module_entry; #define phpext_deepclone_ptr &deepclone_module_entry -#define PHP_DEEPCLONE_VERSION "0.5.1" +#define PHP_DEEPCLONE_VERSION "0.6.0" ZEND_BEGIN_MODULE_GLOBALS(deepclone) HashTable hydrate_cache; diff --git a/tests/deepclone_hydrate.phpt b/tests/deepclone_hydrate.phpt index 2753c76..ae8f857 100644 --- a/tests/deepclone_hydrate.phpt +++ b/tests/deepclone_hydrate.phpt @@ -61,34 +61,31 @@ var_dump($o->y === 'hi'); $o = deepclone_hydrate('STDcLASS', ['p' => 123]); var_dump($o->p === 123); -// === "\0" key = SPL internal state === - -// SplObjectStorage via "\0" key -$s = new SplObjectStorage(); -$o1 = new stdClass(); $o1->name = 'a'; -$o2 = new stdClass(); $o2->name = 'b'; -$result = deepclone_hydrate($s, ["\0" => [$o1, 'info1', $o2, 'info2']]); -var_dump($result === $s); -var_dump($result->count() === 2); -$result->rewind(); -var_dump($result->current() === $o1); -var_dump($result->getInfo() === 'info1'); - -$o3 = new stdClass(); -$s = deepclone_hydrate('SplObjectStorage', ["\0" => [$o3, 'data']]); -var_dump($s->count() === 1); - -// ArrayObject via "\0" key -$ao = deepclone_hydrate('ArrayObject', ["\0" => [['x' => 1, 'y' => 2], ArrayObject::ARRAY_AS_PROPS]]); +// === SPL classes (ArrayObject, ArrayIterator, SplObjectStorage) === +// These ship __serialize/__unserialize since PHP 7.4. deepclone_hydrate() +// instantiates them; callers wire up state by calling __unserialize() with +// the array shape these classes document, or by going through +// deepclone_from_array() which handles it natively. + +// ArrayObject + __unserialize +$ao = deepclone_hydrate('ArrayObject'); +$ao->__unserialize([ArrayObject::ARRAY_AS_PROPS, ['x' => 1, 'y' => 2], []]); var_dump($ao instanceof ArrayObject); var_dump($ao['x'] === 1); var_dump($ao->getFlags() === ArrayObject::ARRAY_AS_PROPS); -// ArrayIterator via "\0" key -$ai = deepclone_hydrate('ArrayIterator', ["\0" => [['a', 'b', 'c']]]); +// ArrayIterator + __unserialize +$ai = deepclone_hydrate('ArrayIterator'); +$ai->__unserialize([0, ['a', 'b', 'c'], []]); var_dump($ai instanceof ArrayIterator); var_dump(count($ai) === 3); +// SplObjectStorage + __unserialize +$o1 = new stdClass(); $o2 = new stdClass(); +$s = deepclone_hydrate('SplObjectStorage'); +$s->__unserialize([[$o1, 'info1', $o2, 'info2'], []]); +var_dump($s->count() === 2); + // === (array) $obj round-trip — all four mangled-key shapes === $expected = [ @@ -265,35 +262,6 @@ try { var_dump(str_contains($e->getMessage(), 'invalid mangled key')); } -// "\0" special key with a non-array value → ValueError -try { - deepclone_hydrate('SplObjectStorage', ["\0" => 42]); -} catch (\ValueError $e) { - var_dump(str_contains($e->getMessage(), 'must be of type array')); -} - -// "\0" special key on a class that doesn't support it → ValueError -class NoSpl { public int $x = 0; } -try { - deepclone_hydrate('NoSpl', ["\0" => [1, 2]]); -} catch (\ValueError $e) { - var_dump(str_contains($e->getMessage(), 'SplObjectStorage')); -} - -// SplObjectStorage "\0" payload with odd count → ValueError -try { - deepclone_hydrate('SplObjectStorage', ["\0" => [new stdClass()]]); -} catch (\ValueError $e) { - var_dump(str_contains($e->getMessage(), 'even number of entries')); -} - -// ArrayObject "\0" payload with more than 3 args → ValueError -try { - deepclone_hydrate('ArrayObject', ["\0" => [[], 0, 'ArrayIterator', 'extra']]); -} catch (\ValueError $e) { - var_dump(str_contains($e->getMessage(), 'at most 3 entries')); -} - // Mangled-private key referencing a non-parent class → ValueError class HydrateUnrelated { public int $x = 0; } try { @@ -400,12 +368,4 @@ bool(true) bool(true) bool(true) bool(true) -bool(true) -bool(true) -bool(true) -bool(true) -bool(true) -bool(true) -bool(true) -bool(true) Done