test(symfony): 添加 AOT 模式兼容性测试与文档

pull/14/head
韩天峰 2 months ago
parent 67d795a506
commit 1d2dab4875
  1. 9
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 46
      tests/aot/symfony/closure-bind-private-access.phpt
  3. 25
      tests/aot/symfony/coalesce-assign-comparison-precedence.phpt
  4. 36
      tests/aot/symfony/dynamic-constant-enum-case.phpt
  5. 50
      tests/aot/symfony/enum-case-class-coalesce.phpt
  6. 24
      tests/aot/symfony/error-handler-arrow-throw.phpt
  7. 31
      tests/aot/symfony/event-class-coalesce-assign.phpt
  8. 34
      tests/aot/symfony/event-listener-array-coalesce-assign.phpt
  9. 20
      tests/aot/symfony/foreach-array-destructuring.phpt
  10. 31
      tests/aot/symfony/match-expression-array-key.phpt
  11. 33
      tests/aot/symfony/nullsafe-array-offset-coalesce.phpt
  12. 38
      tests/aot/symfony/nullsafe-coalesce-throw.phpt
  13. 32
      tests/aot/symfony/nullsafe-invoke-disabled-callback.phpt
  14. 16
      tests/aot/symfony/reflection-method-first-class-invoke.phpt

@ -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` 编码。

@ -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--
<?php
class SymfonyLikeCacheItem
{
private mixed $value = null;
public function expose(): mixed
{
return $this->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"

@ -0,0 +1,25 @@
--TEST--
Symfony pattern: comparison with coalesce assignment in condition
--FILE--
<?php
function splitLimit(?int $limit): string
{
if (1 > $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"

@ -0,0 +1,36 @@
--TEST--
Symfony pattern: dynamic constant resolves enum case
--FILE--
<?php
enum SymfonyLikeFooEnum
{
case Bar;
}
function resolveEnumConstant(string $name): UnitEnum
{
return constant($name) instanceof UnitEnum
? constant($name)
: throw new TypeError('Not an enum case.');
}
function main(): void
{
$case = resolveEnumConstant(SymfonyLikeFooEnum::class.'::Bar');
var_dump($case::class);
var_dump($case->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."

@ -0,0 +1,50 @@
--TEST--
Symfony pattern: enum case runtime class cached with ??=
--FILE--
<?php
enum SymfonyLikeRequirement
{
case Slug;
case Uuid;
}
function enumCaseClasses(UnitEnum ...$cases): array
{
$class = null;
$result = [];
foreach ($cases as $case) {
$class ??= $case::class;
$result[] = [$class, $case::class, $case->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"
}
}

@ -0,0 +1,24 @@
--TEST--
Symfony pattern: set_error_handler with static arrow function throwing
--ENV--
USE_ZEND_ALLOC=0
--FILE--
<?php
function main(): void
{
set_error_handler(static fn ($type, $message, $file, $line) => 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"

@ -0,0 +1,31 @@
--TEST--
Symfony pattern: event name defaults to runtime object class with ??=
--FILE--
<?php
class SymfonyLikeEvent
{
}
class SymfonyLikeChildEvent extends SymfonyLikeEvent
{
}
function dispatch(object $event, ?string $eventName = null): string
{
$eventName ??= $event::class;
return $eventName;
}
function main(): void
{
var_dump(dispatch(new SymfonyLikeEvent()));
var_dump(dispatch(new SymfonyLikeChildEvent()));
var_dump(dispatch(new SymfonyLikeChildEvent(), 'custom.event'));
}
?>
--EXPECT--
string(16) "SymfonyLikeEvent"
string(21) "SymfonyLikeChildEvent"
string(12) "custom.event"

@ -0,0 +1,34 @@
--TEST--
Symfony pattern: normalize array listener method with ??=
--FILE--
<?php
class SymfonyLikeListener
{
public function __invoke(): string
{
return 'invoked';
}
public function onEvent(): string
{
return 'event';
}
}
function normalizeListener(array $listener): string
{
$listener[1] ??= '__invoke';
return $listener[0]::class.'::'.$listener[1];
}
function main(): void
{
var_dump(normalizeListener([new SymfonyLikeListener()]));
var_dump(normalizeListener([new SymfonyLikeListener(), 'onEvent']));
}
?>
--EXPECT--
string(29) "SymfonyLikeListener::__invoke"
string(28) "SymfonyLikeListener::onEvent"

@ -0,0 +1,20 @@
--TEST--
Symfony pattern: foreach array destructuring with keyed rows
--FILE--
<?php
function main(): void
{
$openHandles = [
'a' => [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"

@ -0,0 +1,31 @@
--TEST--
Symfony pattern: match expression used as array key
--FILE--
<?php
function exportProperty(array &$properties, string $scope, string $name, mixed $value): void
{
$prefix = $scope[0] ?? '';
$properties[match ($prefix) {
"\0" => $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"

@ -0,0 +1,33 @@
--TEST--
Symfony pattern: nullsafe method result array offset with coalesce
--FILE--
<?php
class SymfonyLikeStamp
{
public function __construct(private array $attributes)
{
}
public function getAttributes(): array
{
return $this->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"

@ -0,0 +1,38 @@
--TEST--
Symfony pattern: nullsafe call with coalesce throw expression
--FILE--
<?php
class SymfonyLikeReceivedStamp
{
public function __construct(private string $id)
{
}
public function getId(): string
{
return $this->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."

@ -0,0 +1,32 @@
--TEST--
Symfony pattern: nullsafe __invoke callback check
--FILE--
<?php
class SymfonyLikeTraceableDispatcher
{
public function __construct(private ?Closure $disabled = null)
{
}
public function dispatch(string $name): string
{
if ($this->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"

@ -0,0 +1,16 @@
--TEST--
Symfony pattern: cache ReflectionMethod::invoke first-class callable
--FILE--
<?php
function main(): void
{
$invoke = null;
$invoke ??= (new ReflectionMethod(ReflectionMethod::class, 'getName'))->invoke(...);
$method = new ReflectionMethod(DateTimeZone::class, 'getName');
var_dump($invoke($method));
}
?>
--EXPECT--
string(7) "getName"
Loading…
Cancel
Save