diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 28627aa7..c71b3145 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -33,6 +33,7 @@ use PhpParser\NodeAbstract; use PhpParser\Parser; use PhpParser\ParserFactory; use PhpParser\PrettyPrinter; +use PhpParser\Node\Expr\CallLike; class CompilerBase extends \PhpAot\Core\Translator { @@ -389,6 +390,10 @@ class CompilerBase extends \PhpAot\Core\Translator return $this->parseArrayDimFetch($expr, $this->inAssignExpr); case self::EXPR_PROPERTY_FETCH: return $this->parsePropertyFetch($expr, $this->inAssignExpr); + case 'Expr_NullsafePropertyFetch': + return $this->parseNullsafePropertyFetch($expr); + case 'Expr_NullsafeMethodCall': + return $this->parseNullsafeMethodCall($expr); case 'Expr_BinaryOp_ShiftLeft': return $this->parseBinaryOpShiftLeft($expr); case 'Expr_BinaryOp_ShiftRight': @@ -978,8 +983,15 @@ class CompilerBase extends \PhpAot\Core\Translator $this->fatalError($param, 'Promoted properties are not supported'); } $name = $this->parseIdentifier($param->var); - $type = $param->type === null ? '' : $param->type; - $propertyDef = new PropertyDef($name, $param->flags, $type, $this->parseParamDefaultValue($param->default)); + if ($param->type instanceof NullableType) { + $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; } 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); } - 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)) { return false; @@ -3904,7 +3916,7 @@ class CompilerBase extends \PhpAot\Core\Translator 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 = ''; $classDef = null; @@ -4102,4 +4114,66 @@ class CompilerBase extends \PhpAot\Core\Translator 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()"; + } } diff --git a/src/Php/Entity/PropertyDef.php b/src/Php/Entity/PropertyDef.php index e2160fc2..f3fb4505 100644 --- a/src/Php/Entity/PropertyDef.php +++ b/src/Php/Entity/PropertyDef.php @@ -16,13 +16,15 @@ class PropertyDef public string $type; public int $flags; 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->name = $name; $this->type = $type; $this->default = $default; + $this->nullable = $nullable; } public function isPrivate(): bool diff --git a/tests/aot/nullsafe-001.phpt b/tests/aot/nullsafe-001.phpt new file mode 100644 index 00000000..815a44a6 --- /dev/null +++ b/tests/aot/nullsafe-001.phpt @@ -0,0 +1,21 @@ +--TEST-- +Nullsafe operator - method and property access +--FILE-- +getName()); + $user2 = new User(); + var_dump($user2?->getName()); +} +?> +--EXPECT-- +NULL +string(5) "Alice" + diff --git a/tests/aot/nullsafe-002.phpt b/tests/aot/nullsafe-002.phpt new file mode 100644 index 00000000..f083d049 --- /dev/null +++ b/tests/aot/nullsafe-002.phpt @@ -0,0 +1,89 @@ +--TEST-- +Nullsafe operator - method and property access +--FILE-- +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 diff --git a/tests/aot/nullsafe-003.phpt b/tests/aot/nullsafe-003.phpt new file mode 100644 index 00000000..4db81d78 --- /dev/null +++ b/tests/aot/nullsafe-003.phpt @@ -0,0 +1,19 @@ +--TEST-- +Nullsafe operator - method and property access +--FILE-- +prop?->data); + $user->prop = new stdClass(); + $user->prop->data = 'hello'; + var_dump($user?->prop?->data); +} +?> +--EXPECT-- +NULL +string(5) "hello" diff --git a/tests/aot/nullsafe-004.phpt b/tests/aot/nullsafe-004.phpt new file mode 100644 index 00000000..3807f968 --- /dev/null +++ b/tests/aot/nullsafe-004.phpt @@ -0,0 +1,28 @@ +--TEST-- +Nullsafe operator - method and property access +--FILE-- +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"