feat(php): 添加对PHP空安全操作符的支持

- 实现了 NullsafePropertyFetch 和 NullsafeMethodCall 的解析支持
- 添加了 parseNullsafePropertyFetch 和 parseNullsafeMethodCall 方法
- 实现了 nullsafe 表达式的完整解析逻辑,包括属性和方法调用链式访问
- 更新了 PropertyDef 类以支持可空类型标记
- 修改了 getNativeMethod 和 findNativeMethod 方法参数类型为 CallLike
- 添加了完整的测试用例覆盖 nullsafe 操作符的各种使用场景
pull/1/head
韩天峰 6 months ago
parent f12ceb1713
commit dd46d6fa5f
  1. 82
      src/Php/CompilerBase.php
  2. 4
      src/Php/Entity/PropertyDef.php
  3. 21
      tests/aot/nullsafe-001.phpt
  4. 89
      tests/aot/nullsafe-002.phpt
  5. 19
      tests/aot/nullsafe-003.phpt
  6. 28
      tests/aot/nullsafe-004.phpt

@ -33,6 +33,7 @@ use PhpParser\NodeAbstract;
use PhpParser\Parser; use PhpParser\Parser;
use PhpParser\ParserFactory; use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter; use PhpParser\PrettyPrinter;
use PhpParser\Node\Expr\CallLike;
class CompilerBase extends \PhpAot\Core\Translator class CompilerBase extends \PhpAot\Core\Translator
{ {
@ -389,6 +390,10 @@ class CompilerBase extends \PhpAot\Core\Translator
return $this->parseArrayDimFetch($expr, $this->inAssignExpr); return $this->parseArrayDimFetch($expr, $this->inAssignExpr);
case self::EXPR_PROPERTY_FETCH: case self::EXPR_PROPERTY_FETCH:
return $this->parsePropertyFetch($expr, $this->inAssignExpr); return $this->parsePropertyFetch($expr, $this->inAssignExpr);
case 'Expr_NullsafePropertyFetch':
return $this->parseNullsafePropertyFetch($expr);
case 'Expr_NullsafeMethodCall':
return $this->parseNullsafeMethodCall($expr);
case 'Expr_BinaryOp_ShiftLeft': case 'Expr_BinaryOp_ShiftLeft':
return $this->parseBinaryOpShiftLeft($expr); return $this->parseBinaryOpShiftLeft($expr);
case 'Expr_BinaryOp_ShiftRight': case 'Expr_BinaryOp_ShiftRight':
@ -978,8 +983,15 @@ class CompilerBase extends \PhpAot\Core\Translator
$this->fatalError($param, 'Promoted properties are not supported'); $this->fatalError($param, 'Promoted properties are not supported');
} }
$name = $this->parseIdentifier($param->var); $name = $this->parseIdentifier($param->var);
$type = $param->type === null ? '' : $param->type; if ($param->type instanceof NullableType) {
$propertyDef = new PropertyDef($name, $param->flags, $type, $this->parseParamDefaultValue($param->default)); $type = $param->type->type;
$nullable = true;
} else {
$type = $param->type === null ? '' : $param->type;
$nullable = false;
}
$default = $this->parseParamDefaultValue($param->default);
$propertyDef = new PropertyDef($name, $param->flags, $type, $default, $nullable);
$this->classDef->properties[$name] = $propertyDef; $this->classDef->properties[$name] = $propertyDef;
} }
if ($param->variadic and $i !== $last) { if ($param->variadic and $i !== $last) {
@ -1522,7 +1534,7 @@ class CompilerBase extends \PhpAot\Core\Translator
return array_key_exists($this->escapeFunction($name), $this->nativeFunctions); return array_key_exists($this->escapeFunction($name), $this->nativeFunctions);
} }
protected function getNativeMethod(Node\Expr\MethodCall|Node\Expr\StaticCall $expr, string $class, string $method): string|false protected function getNativeMethod(CallLike $expr, string $class, string $method): string|false
{ {
if (!$this->hasNativeClass($class)) { if (!$this->hasNativeClass($class)) {
return false; return false;
@ -3904,7 +3916,7 @@ class CompilerBase extends \PhpAot\Core\Translator
return $methodDef->flags & Modifiers::PUBLIC; return $methodDef->flags & Modifiers::PUBLIC;
} }
protected function findNativeMethod(Node\Expr\MethodCall $expr, string $object, string $method): string|false protected function findNativeMethod(CallLike $expr, string $object, string $method): string|false
{ {
$nativeFunc = ''; $nativeFunc = '';
$classDef = null; $classDef = null;
@ -4102,4 +4114,66 @@ class CompilerBase extends \PhpAot\Core\Translator
return $tmpVar; return $tmpVar;
} }
protected function parseNullsafePropertyFetch(Node\Expr\NullsafePropertyFetch $expr): string
{
return $this->parseNullsafeExpr($expr);
}
protected function parseNullsafeMethodCall(Node\Expr\NullsafeMethodCall $expr): string
{
return $this->parseNullsafeExpr($expr);
}
protected function parseNullsafeExpr(Node\Expr\NullsafePropertyFetch|Node\Expr\NullsafeMethodCall $expr): string
{
$list = [];
$comment = '// Nullsafe Operator: ' . $this->printer->prettyPrint([$expr]);
while (1) {
if ($expr instanceof Node\Expr\NullsafePropertyFetch) {
$list[] = ['property', $this->identifierToStr($expr->name, literal: true)];
$expr = $expr->var;
} elseif ($expr instanceof Node\Expr\NullsafeMethodCall) {
$list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args];
$expr = $expr->var;
} else {
if ($this->isVarExpr($expr)) {
$object = $this->parseIdentifier($expr);
if (!$this->hasVar($object)) {
$this->errorUndefinedVariable($expr);
}
$type = $this->getVarType($object);
if ($type === self::TYPE_OBJECT) {
break;
}
}
$object = $this->addTmpVar(self::TYPE_OBJECT);
$this->beforeStmtLines[] = $this->getIndent() . $object . ' = ' . $this->parseIdentifier($expr) . ';';
break;
}
}
$list = array_reverse($list);
$last = array_key_last($list);
$tmpFn = $this->genTmpVarName();
$code = $comment . PHP_EOL . 'auto ' . $tmpFn . ' = [&]() -> ' . self::TYPE_VAR . '{' . PHP_EOL;
$update = $this->escapeBool($this->inAssignExpr);
foreach ($list as $key => $item) {
$tmpVar = $this->addTmpVar($key !== $last ? self::TYPE_OBJECT : self::TYPE_VAR);
$code .= "if ($object.isNull()) { return " . self::VALUE_NULL . "; }";
if ($item[0] == 'property') {
$code .= $this->getIndent() . "$tmpVar = $object.attr({$item[1]}, $update);";
} else {
$args = $this->parseCallArgs($item[2]);
$code .= $this->getIndent() . "$tmpVar = $object.exec({$item[1]}, $args);";
}
$object = $tmpVar;
}
$code .= $this->getIndent() . "return $object; };";
$this->beforeStmtLines[] = $code;
return "$tmpFn()";
}
} }

@ -16,13 +16,15 @@ class PropertyDef
public string $type; public string $type;
public int $flags; public int $flags;
public ?string $default = null; public ?string $default = null;
public bool $nullable = false;
public function __construct(string $name, int $flags, string $type, ?string $default = null) public function __construct(string $name, int $flags, string $type, ?string $default = null, bool $nullable = false)
{ {
$this->flags = $flags; $this->flags = $flags;
$this->name = $name; $this->name = $name;
$this->type = $type; $this->type = $type;
$this->default = $default; $this->default = $default;
$this->nullable = $nullable;
} }
public function isPrivate(): bool public function isPrivate(): bool

@ -0,0 +1,21 @@
--TEST--
Nullsafe operator - method and property access
--FILE--
<?php
class User {
public function getName(): string {
return 'Alice';
}
}
function main() {
$user1 = null;
var_dump($user1?->getName());
$user2 = new User();
var_dump($user2?->getName());
}
?>
--EXPECT--
NULL
string(5) "Alice"

@ -0,0 +1,89 @@
--TEST--
Nullsafe operator - method and property access
--FILE--
<?php
class Customer {
public function __construct(
public ?Address $address = null,
public string $name = "John"
) {}
public function getCountry(): ?string {
return $this->address?->country ?? 'Unknown';
}
}
class Address {
public function __construct(
public string $street = "Main St",
public string $city = "NYC",
public string $country = "USA"
) {}
public function getFullAddress(): string {
return "{$this->street}, {$this->city}";
}
}
// Test 6: Nullsafe with method returning object
class ChainTest {
public ?Child $child = null;
public function getChild(): ?Child {
return $this->child;
}
}
class Child {
public string $name = "Alice";
public function getName(): string {
return $this->name;
}
}
function main() {
// Test 1: Nullsafe method call on non-null object
$customer1 = new Customer(new Address("5th Avenue", "New York", "USA"));
echo $customer1->address?->getFullAddress() . "\n";
// Test 2: Nullsafe method call on null object
$customer2 = new Customer(null);
var_dump($customer2->address?->getFullAddress());
// Test 3: Nullsafe property access on non-null object
echo $customer1->address?->city . "\n";
// Test 4: Nullsafe property access on null object
var_dump($customer2->address?->city);
// Test 5: Chained nullsafe calls
echo strtoupper($customer1?->address?->getFullAddress());
$test = new ChainTest();
$test->child = new Child();
// Test 7: Chained nullsafe method calls
echo $test->getChild()?->getName() . "\n";
// Test 8: Chained nullsafe with null in middle
$test2 = new ChainTest();
var_dump($test2->getChild()?->getName());
// Test 9: Nullsafe property access in expression
$value = $customer1->address?->street ?? 'No street';
echo $value . "\n";
// Test 10: Multiple nullsafe in same expression
echo ($customer1->address?->city ?? 'Unknown') . ", " . ($customer1->address?->country ?? 'Unknown') . "\n";
}
?>
--EXPECT--
5th Avenue, New York
NULL
New York
NULL
5TH AVENUE, NEW YORKAlice
NULL
5th Avenue
New York, USA

@ -0,0 +1,19 @@
--TEST--
Nullsafe operator - method and property access
--FILE--
<?php
class User {
public ?stdClass $prop = null;
}
function main() {
$user = new User();
var_dump($user?->prop?->data);
$user->prop = new stdClass();
$user->prop->data = 'hello';
var_dump($user?->prop?->data);
}
?>
--EXPECT--
NULL
string(5) "hello"

@ -0,0 +1,28 @@
--TEST--
Nullsafe operator - method and property access
--FILE--
<?php
class User1 {
public function getName(): string {
return 'Alice';
}
}
class User2 {
public ?User1 $user = null;
public function getUser() {
return $this->user;
}
}
function main() {
$user = new User2();
var_dump($user?->getUser()?->getName());
$user->user = new User1();
var_dump($user?->getUser()?->getName());
}
?>
--EXPECT--
NULL
string(5) "Alice"
Loading…
Cancel
Save