diff --git a/phpunit/code/class-const-default-value.php b/phpunit/code/class-const-default-value.php new file mode 100644 index 00000000..4989600d --- /dev/null +++ b/phpunit/code/class-const-default-value.php @@ -0,0 +1,15 @@ +test(); +} diff --git a/phpunit/src/ClassTest.php b/phpunit/src/ClassTest.php index 2f86f9ef..32f47743 100644 --- a/phpunit/src/ClassTest.php +++ b/phpunit/src/ClassTest.php @@ -155,4 +155,11 @@ class ClassTest extends \BaseTest { $this->exec('Method MagicGetProtectedInvalid::__get() must have public visibility', 'magic-get-protected.php'); } + + public function testClassConstDefaultValue() + { + // 类常量(self:: / 类名:: / 完全限定名::,含继承自内部父类的常量) + // 作为函数/方法默认参数值应当能够在编译期正确解析。 + $this->compile('class-const-default-value.php'); + } } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 7b1cba50..2e554e01 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1485,13 +1485,18 @@ class CompilerBase implements PropertyAccessContext return null; } /* - * 函数参数默认值只能为字面量,无法使用表达式获取值 + * 函数参数默认值只能为字面量,无法使用表达式获取值。 + * 但 PHP 自 5.6 起支持在默认参数值中使用常量表达式,包括 + * 类常量(self::FOO、ClassName::BAR、\Full\Class::BAZ), + * 编译器需要在编译期将其折叠为对应的字面量。 */ if ($default instanceof Expr\ConstFetch) { return $this->parseConstFetch($default, true); - } else { - return $this->parseIdentifier($default); } + if ($default instanceof Expr\ClassConstFetch) { + return $this->parseClassConstFetch($default); + } + return $this->parseIdentifier($default); } protected function getComment(Node\Stmt $v, string $class): string diff --git a/src/Translator.php b/src/Translator.php index 456d1fca..f2b29a8b 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2062,7 +2062,17 @@ CODE; $nativeConst = $this->findNativeClassConst($expr, $class, $name); if ($nativeConst and $expr->hasAttribute('nativeConst')) { $constDef = $expr->getAttribute('nativeConst'); - return $constDef->valueExpr->value; + if ($constDef->valueExpr !== null) { + return $this->evaluateClassConstValue($expr, $constDef, $class, $name); + } + // 内部类的常量没有 valueExpr(值来自 PHP 反射),用“定义该常量的类” + // 通过反射取回标量值。继承自有内部父类(如 ArrayObject)的常量也走这里。 + if ($constDef->class !== '') { + $refConst = $constDef->class . '::' . $name; + if (defined($refConst)) { + return constant($refConst); + } + } } if ($this->isInternalClass($class)) { $constName = $class . '::' . $name; @@ -2070,6 +2080,13 @@ CODE; return constant($constName); } } + // findNativeClassConst 不会遍历到内部父类(如 \ArrayObject), + // 因此继承自有内部父类的常量(例如 self::ARRAY_AS_PROPS,常量定义于 + // 内部父类)在此沿继承链解析(内部类在运行时已加载,可用反射取值)。 + [$inheritedFound, $inherited] = $this->resolveInheritedClassConst($class, $name); + if ($inheritedFound) { + return $inherited; + } // Resolve enum case references. Enum cases are runtime objects; their // actual values in class constant arrays are set at runtime by // genClassArrayConstants() via php::getEnumCase(). Return the backing @@ -2084,6 +2101,87 @@ CODE; $this->fatalError($expr, "Class constant `{$class}::{$name}` not found"); } + /** + * 沿类继承链解析类常量,支持继承自自定义父类或内部父类 + * (如 LazyArrayObject 继承自内部类 \ArrayObject,其常量 ARRAY_AS_PROPS + * 定义于内部父类中;内部类在运行时已加载,可用 PHP 反射取值)。 + * + * @return array{bool, mixed} [是否找到, 常量值] + */ + protected function resolveInheritedClassConst(string $class, string $name): array + { + $current = ltrim($class, '\\'); + $visited = []; + while ($current !== '' && $current !== '\\' && !isset($visited[strtolower($current)])) { + $visited[strtolower($current)] = true; + if ($this->hasClass($current)) { + $classDef = $this->getClass($current); + if ($classDef->hasConstant($name)) { + $constDef = $classDef->getConstant($name); + if ($constDef->valueExpr !== null) { + return [true, $this->evaluateClassConstValue(null, $constDef, $current, $name)]; + } + if ($constDef->class !== '' && defined($constDef->class . '::' . $name)) { + return [true, constant($constDef->class . '::' . $name)]; + } + } + $current = $classDef->extends; + } elseif (($parent = $this->getParentClass($current)) !== '') { + // 生成 stub 时,当前文件的类尚未注册到 $this->classes, + // 但预处理阶段已经记录了其父类关系。 + $current = $parent; + } elseif (Reflection::isInternalClass($current)) { + $constName = $current . '::' . $name; + if (defined($constName)) { + return [true, constant($constName)]; + } + break; + } else { + break; + } + } + return [false, null]; + } + + /** + * Evaluates a parsed class-constant expression for arginfo generation. + * ConstantDef::value is C++ code, so it must not be used as the PHP value. + */ + protected function evaluateClassConstValue(?NodeAbstract $origin, ConstantDef $constDef, string $class, string $name): mixed + { + $valueExpr = $constDef->valueExpr; + if (!$valueExpr instanceof Node\Expr) { + $this->fatalError($origin, "Class constant `{$class}::{$name}` has no constant expression"); + } + + $evaluator = new \PhpParser\ConstExprEvaluator(function (Node\Expr $expr) use ($origin, $class) { + if ($expr instanceof Node\Expr\ConstFetch) { + $constName = $expr->name->toString(); + return match (strtolower($constName)) { + 'true' => true, + 'false' => false, + 'null' => null, + default => defined($constName) + ? constant($constName) + : throw new \RuntimeException("Constant `{$constName}` not found"), + }; + } + if ($expr instanceof Node\Expr\ClassConstFetch && $expr->class instanceof Node\Name) { + $constName = $expr->name->toString(); + $className = $expr->class->toString(); + if (strcasecmp($className, 'self') === 0) { + $className = $class; + } elseif (strcasecmp($className, 'parent') === 0) { + $className = $this->getParentClass($class); + } + return $this->getClassConstValue($origin ?? $expr, $className, $constName, $class); + } + throw new \RuntimeException('Unsupported class constant expression'); + }); + + return $evaluator->evaluateDirectly($valueExpr); + } + public function getConstValue(string $name): mixed { if ($this->isInternalConstant($name)) { diff --git a/tests/aot/const/class-const-default-value.phpt b/tests/aot/const/class-const-default-value.phpt new file mode 100644 index 00000000..e66dc96e --- /dev/null +++ b/tests/aot/const/class-const-default-value.phpt @@ -0,0 +1,47 @@ +--TEST-- +Class constant as default parameter value (self / class name / FQCN, including constant inherited from an internal parent class) +--FILE-- +test(); + $test->nullDefault(); + (new ChildDefaultValue)->inheritedDefault(); +} +?> +--EXPECT-- +int(2) +int(2) +int(2) +NULL +int(42)