From 1d2dab4875e0bebe2e82a20387fc92d9b4a6cda1 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 7 Jul 2026 15:40:51 +0800 Subject: [PATCH] =?UTF-8?q?test(symfony):=20=E6=B7=BB=E5=8A=A0=20AOT=20?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=85=BC=E5=AE=B9=E6=80=A7=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E4=B8=8E=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/INCOMPATIBLE_PHP_FEATURES.md | 9 +++- .../symfony/closure-bind-private-access.phpt | 46 +++++++++++++++++ ...coalesce-assign-comparison-precedence.phpt | 25 ++++++++++ .../symfony/dynamic-constant-enum-case.phpt | 36 +++++++++++++ .../aot/symfony/enum-case-class-coalesce.phpt | 50 +++++++++++++++++++ .../symfony/error-handler-arrow-throw.phpt | 24 +++++++++ .../symfony/event-class-coalesce-assign.phpt | 31 ++++++++++++ .../event-listener-array-coalesce-assign.phpt | 34 +++++++++++++ .../symfony/foreach-array-destructuring.phpt | 20 ++++++++ .../symfony/match-expression-array-key.phpt | 31 ++++++++++++ .../nullsafe-array-offset-coalesce.phpt | 33 ++++++++++++ .../aot/symfony/nullsafe-coalesce-throw.phpt | 38 ++++++++++++++ .../nullsafe-invoke-disabled-callback.phpt | 32 ++++++++++++ .../reflection-method-first-class-invoke.phpt | 16 ++++++ 14 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 tests/aot/symfony/closure-bind-private-access.phpt create mode 100644 tests/aot/symfony/coalesce-assign-comparison-precedence.phpt create mode 100644 tests/aot/symfony/dynamic-constant-enum-case.phpt create mode 100644 tests/aot/symfony/enum-case-class-coalesce.phpt create mode 100644 tests/aot/symfony/error-handler-arrow-throw.phpt create mode 100644 tests/aot/symfony/event-class-coalesce-assign.phpt create mode 100644 tests/aot/symfony/event-listener-array-coalesce-assign.phpt create mode 100644 tests/aot/symfony/foreach-array-destructuring.phpt create mode 100644 tests/aot/symfony/match-expression-array-key.phpt create mode 100644 tests/aot/symfony/nullsafe-array-offset-coalesce.phpt create mode 100644 tests/aot/symfony/nullsafe-coalesce-throw.phpt create mode 100644 tests/aot/symfony/nullsafe-invoke-disabled-callback.phpt create mode 100644 tests/aot/symfony/reflection-method-first-class-invoke.phpt diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index cea8a337..ba864b88 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -15,12 +15,14 @@ - 不支持 `yield` / `yield from`。 - 不支持可变变量 `$$var`。 -- 不支持 property hooks。 +- 不支持 PHP 8.4 property hooks。 - 不支持函数或方法按引用返回。 - `__construct()` 不允许返回值。 - 参数默认值不允许出现在必填参数之前(`PHP`允许,但会直接丢弃此默认参数)。 - 不支持引用可变参数 `&...$args`。 - 联合类型、交叉类型、`nullable` 类型在静态编译阶段按 `mixed/any` 处理,只保留运行时 type check。 +- 局部变量类型一旦被静态推断为具体 native 类型,不支持在同一作用域内重新赋值为不兼容类型。 +- attribute 参数不支持数组值。 ## declare @@ -40,6 +42,8 @@ - 禁止子类覆盖父类私有属性。 - `parent::method()` 的方法名必须是字面量。 +- 通过变量持有的 clone 对象写入私有 typed property 时,可能无法完全复现 PHP 的私有属性访问语义。 +- constructor property promotion 的运行时属性可用,但 `ReflectionProperty::isPromoted()` 目前不返回标准 PHP 结果。 ## 表达式与控制流 @@ -47,6 +51,7 @@ - `match` 的 arm condition 不能是 `match` 表达式。 - `foreach` by reference 的 value 只能是变量。 - `foreach` by reference 不支持 list destructuring。 +- `foreach` 遍历 `IteratorAggregate` 返回的 `ArrayObject` 时,当前行为与标准 PHP 不完全一致。 - 固定 native typed object property 不允许按 PHP 未初始化语义自由 `unset()`。 - native 类型变量执行 `unset()` 不会产生标准 PHP 的变量删除语义。 @@ -57,4 +62,6 @@ - `__CLASS__` 只允许在 `class` 定义的代码段中使用(`PHP`允许,返回空字符串)。 - `__TRAIT__` 只允许在 `trait` 定义的代码段中使用(`PHP`允许,返回空字符串)。 - 动态属性链、动态类名、动态函数名、动态回调在部分 native 优化路径上会退化或被拒绝。 +- `Closure::bind()` 绑定静态闭包访问私有成员时,当前行为与标准 PHP 不完全一致。 +- first-class callable 存入 typed nullable `Closure` 属性后,当前存在运行时稳定性限制。 - 所有源文件必须是 `UTF-8` 编码。 diff --git a/tests/aot/symfony/closure-bind-private-access.phpt b/tests/aot/symfony/closure-bind-private-access.phpt new file mode 100644 index 00000000..b3be10e1 --- /dev/null +++ b/tests/aot/symfony/closure-bind-private-access.phpt @@ -0,0 +1,46 @@ +--TEST-- +Symfony pattern: cache Closure::bind private property accessor +--XFAIL-- +Known AOT bug: Closure::bind() on a static closure can be treated as unbinding $this from a method closure. +--FILE-- +value; + } +} + +class SymfonyLikeCacheAdapter +{ + private static ?Closure $setValue = null; + + public function set(SymfonyLikeCacheItem $item, mixed $value): void + { + $setValue = self::$setValue ??= Closure::bind( + static function (SymfonyLikeCacheItem $item, mixed $value): void { + $item->value = $value; + }, + null, + SymfonyLikeCacheItem::class + ); + + $setValue($item, $value); + } +} + +function main(): void +{ + $item = new SymfonyLikeCacheItem(); + $adapter = new SymfonyLikeCacheAdapter(); + + $adapter->set($item, 'cached'); + var_dump($item->expose()); +} +?> +--EXPECT-- +string(6) "cached" diff --git a/tests/aot/symfony/coalesce-assign-comparison-precedence.phpt b/tests/aot/symfony/coalesce-assign-comparison-precedence.phpt new file mode 100644 index 00000000..e8d08d8c --- /dev/null +++ b/tests/aot/symfony/coalesce-assign-comparison-precedence.phpt @@ -0,0 +1,25 @@ +--TEST-- +Symfony pattern: comparison with coalesce assignment in condition +--FILE-- + $limit ??= PHP_INT_MAX) { + return 'small:'.$limit; + } + + return 'ok:'.$limit; +} + +function main(): void +{ + var_dump(splitLimit(null)); + var_dump(splitLimit(0)); + var_dump(splitLimit(2)); +} +?> +--EXPECT-- +string(22) "ok:9223372036854775807" +string(7) "small:0" +string(4) "ok:2" diff --git a/tests/aot/symfony/dynamic-constant-enum-case.phpt b/tests/aot/symfony/dynamic-constant-enum-case.phpt new file mode 100644 index 00000000..2af5726f --- /dev/null +++ b/tests/aot/symfony/dynamic-constant-enum-case.phpt @@ -0,0 +1,36 @@ +--TEST-- +Symfony pattern: dynamic constant resolves enum case +--FILE-- +name); + + try { + resolveEnumConstant('PHP_VERSION'); + } catch (Throwable $e) { + var_dump($e::class); + var_dump($e->getMessage()); + } +} +?> +--EXPECT-- +string(18) "SymfonyLikeFooEnum" +string(3) "Bar" +string(9) "TypeError" +string(17) "Not an enum case." diff --git a/tests/aot/symfony/enum-case-class-coalesce.phpt b/tests/aot/symfony/enum-case-class-coalesce.phpt new file mode 100644 index 00000000..2bf41f74 --- /dev/null +++ b/tests/aot/symfony/enum-case-class-coalesce.phpt @@ -0,0 +1,50 @@ +--TEST-- +Symfony pattern: enum case runtime class cached with ??= +--FILE-- +name]; + } + + return $result; +} + +function main(): void +{ + var_dump(enumCaseClasses(SymfonyLikeRequirement::Slug, SymfonyLikeRequirement::Uuid)); +} +?> +--EXPECT-- +array(2) { + [0]=> + array(3) { + [0]=> + string(22) "SymfonyLikeRequirement" + [1]=> + string(22) "SymfonyLikeRequirement" + [2]=> + string(4) "Slug" + } + [1]=> + array(3) { + [0]=> + string(22) "SymfonyLikeRequirement" + [1]=> + string(22) "SymfonyLikeRequirement" + [2]=> + string(4) "Uuid" + } +} diff --git a/tests/aot/symfony/error-handler-arrow-throw.phpt b/tests/aot/symfony/error-handler-arrow-throw.phpt new file mode 100644 index 00000000..c928d576 --- /dev/null +++ b/tests/aot/symfony/error-handler-arrow-throw.phpt @@ -0,0 +1,24 @@ +--TEST-- +Symfony pattern: set_error_handler with static arrow function throwing +--ENV-- +USE_ZEND_ALLOC=0 +--FILE-- + throw new ErrorException($message, 0, $type, $file, $line)); + + try { + trigger_error('symfony warning', E_USER_WARNING); + } catch (Throwable $e) { + var_dump($e::class); + var_dump($e->getMessage()); + } finally { + restore_error_handler(); + } +} +?> +--EXPECT-- +string(14) "ErrorException" +string(15) "symfony warning" diff --git a/tests/aot/symfony/event-class-coalesce-assign.phpt b/tests/aot/symfony/event-class-coalesce-assign.phpt new file mode 100644 index 00000000..44c120e9 --- /dev/null +++ b/tests/aot/symfony/event-class-coalesce-assign.phpt @@ -0,0 +1,31 @@ +--TEST-- +Symfony pattern: event name defaults to runtime object class with ??= +--FILE-- + +--EXPECT-- +string(16) "SymfonyLikeEvent" +string(21) "SymfonyLikeChildEvent" +string(12) "custom.event" diff --git a/tests/aot/symfony/event-listener-array-coalesce-assign.phpt b/tests/aot/symfony/event-listener-array-coalesce-assign.phpt new file mode 100644 index 00000000..830e0348 --- /dev/null +++ b/tests/aot/symfony/event-listener-array-coalesce-assign.phpt @@ -0,0 +1,34 @@ +--TEST-- +Symfony pattern: normalize array listener method with ??= +--FILE-- + +--EXPECT-- +string(29) "SymfonyLikeListener::__invoke" +string(28) "SymfonyLikeListener::onEvent" diff --git a/tests/aot/symfony/foreach-array-destructuring.phpt b/tests/aot/symfony/foreach-array-destructuring.phpt new file mode 100644 index 00000000..46a4f9ab --- /dev/null +++ b/tests/aot/symfony/foreach-array-destructuring.phpt @@ -0,0 +1,20 @@ +--TEST-- +Symfony pattern: foreach array destructuring with keyed rows +--FILE-- + [1, 'handle-a', 'buffer-a', static fn () => 'progress-a'], + 'b' => [2, 'handle-b', 'buffer-b', static fn () => 'progress-b'], + ]; + + foreach ($openHandles as $id => [$pauseExpiry, $handle, $buffer, $onProgress]) { + var_dump($id.':'.$pauseExpiry.':'.$handle.':'.$buffer.':'.$onProgress()); + } +} +?> +--EXPECT-- +string(32) "a:1:handle-a:buffer-a:progress-a" +string(32) "b:2:handle-b:buffer-b:progress-b" diff --git a/tests/aot/symfony/match-expression-array-key.phpt b/tests/aot/symfony/match-expression-array-key.phpt new file mode 100644 index 00000000..f9ecd473 --- /dev/null +++ b/tests/aot/symfony/match-expression-array-key.phpt @@ -0,0 +1,31 @@ +--TEST-- +Symfony pattern: match expression used as array key +--FILE-- + $scope.$name, + '*' => "\0*\0".$name, + default => $name, + }] = $value; +} + +function main(): void +{ + $properties = []; + exportProperty($properties, 'public', 'name', 'pub'); + exportProperty($properties, '*', 'name', 'prot'); + exportProperty($properties, "\0Private\0", 'name', 'priv'); + + foreach ($properties as $key => $value) { + var_dump(str_replace("\0", '\\0', $key).':'.$value); + } +} +?> +--EXPECT-- +string(8) "name:pub" +string(14) "\0*\0name:prot" +string(20) "\0Private\0name:priv" diff --git a/tests/aot/symfony/nullsafe-array-offset-coalesce.phpt b/tests/aot/symfony/nullsafe-array-offset-coalesce.phpt new file mode 100644 index 00000000..f9c1c6f4 --- /dev/null +++ b/tests/aot/symfony/nullsafe-array-offset-coalesce.phpt @@ -0,0 +1,33 @@ +--TEST-- +Symfony pattern: nullsafe method result array offset with coalesce +--FILE-- +attributes; + } +} + +function contentType(?SymfonyLikeStamp $stamp): ?string +{ + return $stamp?->getAttributes()['content_type'] ?? null; +} + +function main(): void +{ + var_dump(contentType(null)); + var_dump(contentType(new SymfonyLikeStamp([]))); + var_dump(contentType(new SymfonyLikeStamp(['content_type' => 'application/json']))); +} +?> +--EXPECT-- +NULL +NULL +string(16) "application/json" diff --git a/tests/aot/symfony/nullsafe-coalesce-throw.phpt b/tests/aot/symfony/nullsafe-coalesce-throw.phpt new file mode 100644 index 00000000..4c651717 --- /dev/null +++ b/tests/aot/symfony/nullsafe-coalesce-throw.phpt @@ -0,0 +1,38 @@ +--TEST-- +Symfony pattern: nullsafe call with coalesce throw expression +--FILE-- +id; + } +} + +function stampId(?SymfonyLikeReceivedStamp $stamp): string +{ + return $stamp?->getId() ?? throw new LogicException('No stamp found.'); +} + +function main(): void +{ + var_dump(stampId(new SymfonyLikeReceivedStamp('abc'))); + + try { + stampId(null); + } catch (Throwable $e) { + var_dump($e::class); + var_dump($e->getMessage()); + } +} +?> +--EXPECT-- +string(3) "abc" +string(14) "LogicException" +string(15) "No stamp found." diff --git a/tests/aot/symfony/nullsafe-invoke-disabled-callback.phpt b/tests/aot/symfony/nullsafe-invoke-disabled-callback.phpt new file mode 100644 index 00000000..7522c191 --- /dev/null +++ b/tests/aot/symfony/nullsafe-invoke-disabled-callback.phpt @@ -0,0 +1,32 @@ +--TEST-- +Symfony pattern: nullsafe __invoke callback check +--FILE-- +disabled?->__invoke()) { + return 'disabled'; + } + + return 'dispatch:'.$name; + } +} + +function main(): void +{ + var_dump((new SymfonyLikeTraceableDispatcher())->dispatch('kernel.request')); + var_dump((new SymfonyLikeTraceableDispatcher(static fn () => false))->dispatch('kernel.response')); + var_dump((new SymfonyLikeTraceableDispatcher(static fn () => true))->dispatch('kernel.exception')); +} +?> +--EXPECT-- +string(23) "dispatch:kernel.request" +string(24) "dispatch:kernel.response" +string(8) "disabled" diff --git a/tests/aot/symfony/reflection-method-first-class-invoke.phpt b/tests/aot/symfony/reflection-method-first-class-invoke.phpt new file mode 100644 index 00000000..badc686e --- /dev/null +++ b/tests/aot/symfony/reflection-method-first-class-invoke.phpt @@ -0,0 +1,16 @@ +--TEST-- +Symfony pattern: cache ReflectionMethod::invoke first-class callable +--FILE-- +invoke(...); + + $method = new ReflectionMethod(DateTimeZone::class, 'getName'); + var_dump($invoke($method)); +} +?> +--EXPECT-- +string(7) "getName"