From b2246cd45625afe1462fa9f122214adf1be891dd Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 24 Aug 2026 18:38:50 +0800 Subject: [PATCH] test(clone-with): add comprehensive tests for PHP 8.5 clone-with functionality - Add test for property hooks preservation and invocation after __clone - Add test for private/protected/readonly property scope handling in clone-with - Add test for trait method scope usage inside clone-with operations - Update documentation to reflect completed clone-with implementation coverage - Remove pending classification for clone-with scoped/readonly/hooked properties - Remove TODO comment about property handlers preservation in clone-with operation --- docs/INCOMPATIBLE_PHP_FEATURES.md | 2 +- docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md | 1 - src/Translator.php | 4 -- .../basic/clone-with-property-hooks.phpt | 71 +++++++++++++++++++ .../basic/clone-with-scope-readonly.phpt | 39 +++++++++- .../basic/clone-with-trait-scope.phpt | 45 ++++++++++++ 6 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 tests/compiler/basic/clone-with-property-hooks.phpt create mode 100644 tests/compiler/basic/clone-with-trait-scope.phpt diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index 8571c1ea..554160d3 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -16,7 +16,7 @@ - 不支持可变变量 `$$var`。 - 暂不支持 PHP 8.5 `#[NoDiscard]`。 - 支持 PHP 8.5 `(void)` 显式丢弃语句;操作数仍会求值并保留副作用,不能在赋值、返回、参数或条件等值上下文中使用。 -- PHP 8.5 `clone()` / clone-with 依赖实际链接的 `libphp` 版本不低于 8.5。普通公开属性、动态属性、调用顺序、错误传播和 callable 路径已有 PHPT;TypePHP 内部类的 private/protected/readonly 更新仍是 XFAIL,property hook 类的自定义 object handlers 也尚未兼容 `clone_obj_with`。 +- PHP 8.5 `clone()` / clone-with 依赖实际链接的 `libphp` 版本不低于 8.5。公开、动态、private/protected/readonly 和 property hook 属性,以及调用顺序、错误传播和 callable 路径均有 PHPT 覆盖。 - PHP 8.4 property hooks 会编译为 AOT getter/setter,并注册对应的 Zend hook 元数据;直接属性读写、Reflection 和对象遍历均受支持。当前不支持对 hook 属性取引用。 - PHP 8.4 Reflection Lazy Object 不能用于 TypePHP AOT 类。AOT 类以 persistent internal class 注册,而 Zend 的 `zend_object_make_lazy()` 明确拒绝 internal class;运行时动态加载的 ZendPHP user class 不受此限制。 - 支持 `private(set)` 与 `protected(set)` 非对称属性可见性,并通过 PHP 8.4+ 的类级对象 handler 执行同等作用域检查。 diff --git a/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md b/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md index cdbbea61..ba03f28d 100644 --- a/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md +++ b/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md @@ -82,7 +82,6 @@ These items should be documented with the exact boundary. | Feature | Classification | Implementation Direction | |---|---|---| | PHP 8.5 `#[NoDiscard]` | Pending | Preserve PHP's warning behavior and timing consistently for native direct calls and Zend runtime fallbacks. TypePHP's compile-time `#[MustUse]` remains a separate feature. | -| PHP 8.5 clone-with for scoped/readonly/hooked TypePHP properties | Pending | Preserve the lexical class scope in the Zend call frame and teach TypePHP's custom object handlers to clone through `clone_obj_with` without losing hook handlers. Public/dynamic properties and callable/error paths already delegate correctly to a PHP 8.5 runtime. | | Variable variables (`$$var`) | Pending | Add a function-local symbol table mirror for dynamic locals, and disable or synchronize native locals that escape into dynamic lookup. | | Closure or arrow function returning by reference | Pending | Closure metadata and wrappers must preserve return-by-reference and emit `ReturnRef`. | | PHP 8.5 closures in constants, parameter defaults or property defaults | Pending | Use context-aware runtime initializers: cache constants and property defaults per request, create parameter defaults per omitted call, and never place request-local zvals in persistent MINIT storage. | diff --git a/src/Translator.php b/src/Translator.php index 72b629c5..d435588a 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2214,10 +2214,6 @@ CODE; return $body . "return obj;\n"; }; - // TODO(PHP 8.5 clone-with): typephp_install_property_handlers() - // must preserve these custom handlers on the cloned object and - // provide compatible clone_obj_with behavior. Zend's default - // clone path currently drops hook dispatch for TypePHP classes. $code .= "typephp_install_property_handlers({$ce}, &{$handlers});\n"; if ($classDef->requireCtor) { $code .= "create_object_{$className} = php::getCreateObjectFn({$ce});\n"; diff --git a/tests/compiler/basic/clone-with-property-hooks.phpt b/tests/compiler/basic/clone-with-property-hooks.phpt new file mode 100644 index 00000000..53b2f1cc --- /dev/null +++ b/tests/compiler/basic/clone-with-property-hooks.phpt @@ -0,0 +1,71 @@ +--TEST-- +PHP 8.5 clone-with preserves TypePHP property handlers and invokes hooks after __clone +--SKIPIF-- + +--FILE-- + $this->value * 2; + set { + echo 'set:', $value, "\n"; + if ($value < 0) { + throw new InvalidArgumentException('negative value'); + } + $this->value = $value + 1; + } + } + + public function __construct() + { + $this->value = 1; + } + + public function __clone(): void + { + echo '__clone:', $this->value, "\n"; + } + + public function withValue(int $value): self + { + return clone($this, ['value' => $value]); + } +} + +class CloneWithHookChild extends CloneWithHookValue {} + +function read_clone_hook_dynamically(mixed $object): int +{ + return $object->value; +} + +function main(): void +{ + $source = new CloneWithHookChild(); + $copy = $source->withValue(5); + + var_dump(read_clone_hook_dynamically($source)); + var_dump(read_clone_hook_dynamically($copy)); + + try { + $source->withValue(-1); + } catch (InvalidArgumentException $error) { + echo $error->getMessage(), "\n"; + } +} +?> +--EXPECT-- +set:1 +__clone:4 +set:5 +int(4) +int(12) +__clone:4 +set:-1 +negative value diff --git a/tests/compiler/basic/clone-with-scope-readonly.phpt b/tests/compiler/basic/clone-with-scope-readonly.phpt index 2be1a771..b8afb604 100644 --- a/tests/compiler/basic/clone-with-scope-readonly.phpt +++ b/tests/compiler/basic/clone-with-scope-readonly.phpt @@ -6,8 +6,6 @@ if (PHP_VERSION_ID < 80500) { die('skip requires PHP 8.5'); } ?> ---XFAIL-- -TypePHP internal classes do not yet preserve private/protected/readonly property scope during clone-with --FILE-- privateValue, $this->protectedValue, $this->readonlyValue]; } + + public function withInvalidPrivate(): self + { + return clone($this, ['privateValue' => 'invalid']); + } + + public function reinitializeReadonly(int $value): void + { + $this->readonlyValue = $value; + } } class CloneWithScopeChild extends CloneWithScopeBase @@ -54,6 +62,23 @@ function main(): void var_dump($privateCopy->values()); var_dump($protectedCopy->values()); + // PHP leaves readonly slots omitted from the update array reinitializable + // once on the clone. A slot explicitly updated by clone-with is locked. + $protectedCopy->reinitializeReadonly(31); + var_dump($protectedCopy->values()); + + try { + $privateCopy->reinitializeReadonly(40); + } catch (Error $error) { + echo $error->getMessage(), "\n"; + } + + try { + $source->withInvalidPrivate(); + } catch (TypeError $error) { + echo $error::class, ":private\n"; + } + try { clone($source, ['protectedValue' => 99]); } catch (Error $error) { @@ -92,5 +117,15 @@ array(3) { [2]=> int(3) } +array(3) { + [0]=> + int(1) + [1]=> + int(20) + [2]=> + int(31) +} +Cannot modify readonly property CloneWithScopeBase::$readonlyValue +TypeError:private Cannot access protected property CloneWithScopeChild::$protectedValue Cannot modify protected(set) readonly property CloneWithScopeBase::$readonlyValue from global scope diff --git a/tests/compiler/basic/clone-with-trait-scope.phpt b/tests/compiler/basic/clone-with-trait-scope.phpt new file mode 100644 index 00000000..9cb30bbb --- /dev/null +++ b/tests/compiler/basic/clone-with-trait-scope.phpt @@ -0,0 +1,45 @@ +--TEST-- +PHP 8.5 clone-with uses the consuming class scope inside trait methods +--SKIPIF-- + +--FILE-- + $value]); + } + + public function traitPrivate(): int + { + return $this->traitPrivate; + } +} + +class CloneWithTraitBase +{ + use CloneWithTraitScope; +} + +class CloneWithTraitChild extends CloneWithTraitBase {} + +function main(): void +{ + $source = new CloneWithTraitChild(); + $copy = $source->withTraitPrivate(9); + + var_dump($source->traitPrivate()); + var_dump($copy->traitPrivate()); +} +?> +--EXPECT-- +int(1) +int(9)