parseNullsafeExpr($expr); } protected function parseNullsafePropertyFetchUpdate(Expr\NullsafePropertyFetch $expr): string { return $this->parseNodeWithUpdateAttribute( $expr, self::ATTR_PROPERTY_FETCH_UPDATE, true, fn() => $this->parseNullsafePropertyFetch($expr) ); } protected function parseNullsafeMethodCall(Expr\NullsafeMethodCall $expr): string { return $this->parseNullsafeExpr($expr); } protected function parseNullsafeExpr( Expr\PropertyFetch|Expr\MethodCall|Expr\NullsafePropertyFetch|Expr\NullsafeMethodCall $expr ): string { $list = []; $comment = $this->formatCppLineComment('Nullsafe Operator: ', $this->printer->prettyPrint([$expr])); while (1) { if ($expr instanceof Expr\NullsafePropertyFetch) { $list[] = ['property', $this->propertyNameToStr($expr->name, literal: true), $expr, true]; $expr = $expr->var; } elseif ($expr instanceof Expr\NullsafeMethodCall) { $list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args, true, $expr]; $expr = $expr->var; } elseif ($expr instanceof Expr\PropertyFetch) { $list[] = ['property', $this->propertyNameToStr($expr->name, literal: true), $expr, false]; $expr = $expr->var; } elseif ($expr instanceof Expr\MethodCall) { $list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args, false, $expr]; $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 === Type::OBJECT) { break; } } $object = $this->addTmpVar(Type::OBJECT); $this->context->beforeStmtLines[] = $this->getIndent() . $object . ' = ' . $this->parseIdentifier($expr) . ';'; break; } } $list = array_reverse($list); $this->checkNullsafePropertyAccesses($expr, $list); $last = array_key_last($list); $tmpFn = $this->genTmpVarName(); $code = $comment . PHP_EOL . 'auto ' . $tmpFn . ' = [&]() -> ' . Type::VAR . '{' . PHP_EOL; foreach ($list as $key => $item) { $tmpVar = $this->addTmpVar($key !== $last ? Type::OBJECT : Type::VAR); if ($item[3]) { $code .= "if ({$object}.isNull()) { return " . self::VALUE_NULL . '; }'; } if ($item[0] == 'property') { $update = $this->escapeAttrMode($this->isPropertyFetchUpdate($item[2])); $code .= $this->getIndent() . "{$tmpVar} = {$object}.attr({$item[1]}, {$update});"; } else { $methodName = $this->isNamedMethod($item[4]->name) ? $this->parseIdentifier($item[4]->name) : ''; $receiverClass = $this->detectClassOfExpr($item[4]->var); $requiresDynamicScope = $this->runtimeMethodRequiresDynamicScope($receiverClass, $methodName); $beforeStmtCount = count($this->context->beforeStmtLines); $afterStmtCount = count($this->context->afterStmtLines); $args = $this->parseCallArgs($item[2]); $argBeforeStmts = array_slice($this->context->beforeStmtLines, $beforeStmtCount); $argAfterStmts = array_slice($this->context->afterStmtLines, $afterStmtCount); $this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $beforeStmtCount); $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $afterStmtCount); if ($argBeforeStmts) { $code .= $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $argBeforeStmts) . PHP_EOL; } if ($requiresDynamicScope && $this->methodDef) { $code .= $this->getIndent() . "{$tmpVar} = php::callScoped({$object}, {$item[1]}, " . $this->getCallableScopeExpr() . ", {$args});"; } else { $code .= $this->getIndent() . "{$tmpVar} = {$object}.call({$item[1]}, {$args});"; } if ($argAfterStmts) { $code .= $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $argAfterStmts) . PHP_EOL; } } $object = $tmpVar; } $code .= $this->getIndent() . "return {$object}; };"; $this->context->beforeStmtLines[] = $code; return "{$tmpFn}()"; } protected function containsNullsafeChain(NodeAbstract $expr): bool { while ($expr instanceof Expr\PropertyFetch || $expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) { if ($expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) { return true; } $expr = $expr->var; } return false; } private function checkNullsafePropertyAccesses(NodeAbstract $baseExpr, array $list): void { $properties = []; foreach ($list as $item) { if ($item[0] !== 'property') { break; } /** @var Expr\NullsafePropertyFetch $node */ $node = $item[2]; if (!$this->isIdExpr($node->name)) { break; } $properties[] = [ 'node' => $node, 'property' => $this->parseIdentifier($node->name), ]; } if (!$properties) { return; } $scope = $this->class ? $this->getFullClassName() : ''; $results = $this->createPropertyAccessResolver()->resolveNullsafePropertyChain( $this->detectClassOfExpr($baseExpr), $properties, $scope, Type::OBJECT, ); foreach ($results as $index => $result) { $this->applyNativePropertyAccessResult($properties[$index]['node'], $result); } } }