From 99d76ad5dcf9c939435650eb2d9193cb3a78ab09 Mon Sep 17 00:00:00 2001 From: Yurun Date: Fri, 10 Jul 2026 19:28:53 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=B1=BB=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=E4=BD=BF=E7=94=A8self=E4=BD=9C=E4=B8=BA=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=8F=82=E6=95=B0=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpunit/code/class-const-default-value.php | 15 +++++ phpunit/src/ClassTest.php | 7 +++ src/CompilerBase.php | 11 +++- src/Translator.php | 57 ++++++++++++++++++- .../aot/const/class-const-default-value.phpt | 23 ++++++++ 5 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 phpunit/code/class-const-default-value.php create mode 100644 tests/aot/const/class-const-default-value.phpt 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 dd133582..9441dede 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1471,13 +1471,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..1f22fc84 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 $constDef->valueExpr->value; + } + // 内部类的常量没有 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,常量定义于 + // 内部父类)在此沿继承链解析(内部类在运行时已加载,可用反射取值)。 + $inherited = $this->resolveInheritedClassConst($class, $name); + if ($inherited !== null) { + 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,44 @@ CODE; $this->fatalError($expr, "Class constant `{$class}::{$name}` not found"); } + /** + * 沿类继承链解析类常量,支持继承自自定义父类或内部父类 + * (如 LazyArrayObject 继承自内部类 \ArrayObject,其常量 ARRAY_AS_PROPS + * 定义于内部父类中;内部类在运行时已加载,可用 PHP 反射取值)。 + * + * @return mixed|null 解析到的值;未找到返回 null + */ + protected function resolveInheritedClassConst(string $class, string $name): mixed + { + $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 $constDef->valueExpr->value; + } + if ($constDef->class !== '' && defined($constDef->class . '::' . $name)) { + return constant($constDef->class . '::' . $name); + } + } + $current = $classDef->extends; + } elseif (Reflection::isInternalClass($current)) { + $constName = $current . '::' . $name; + if (defined($constName)) { + return constant($constName); + } + break; + } else { + break; + } + } + return null; + } + 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..346c2f31 --- /dev/null +++ b/tests/aot/const/class-const-default-value.phpt @@ -0,0 +1,23 @@ +--TEST-- +Class constant as default parameter value (self / class name / FQCN, including constant inherited from an internal parent class) +--FILE-- +test(); +} +?> +--EXPECT-- +int(2) +int(2) +int(2) From 959f2d171220a8aa3d800d236630b912145e02bc Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 10 Jul 2026 20:03:50 +0800 Subject: [PATCH 2/2] fix: resolve inherited class constants in defaults --- src/Translator.php | 61 ++++++++++++++++--- .../aot/const/class-const-default-value.phpt | 24 ++++++++ 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/Translator.php b/src/Translator.php index 1f22fc84..f2b29a8b 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2063,7 +2063,7 @@ CODE; if ($nativeConst and $expr->hasAttribute('nativeConst')) { $constDef = $expr->getAttribute('nativeConst'); if ($constDef->valueExpr !== null) { - return $constDef->valueExpr->value; + return $this->evaluateClassConstValue($expr, $constDef, $class, $name); } // 内部类的常量没有 valueExpr(值来自 PHP 反射),用“定义该常量的类” // 通过反射取回标量值。继承自有内部父类(如 ArrayObject)的常量也走这里。 @@ -2083,8 +2083,8 @@ CODE; // findNativeClassConst 不会遍历到内部父类(如 \ArrayObject), // 因此继承自有内部父类的常量(例如 self::ARRAY_AS_PROPS,常量定义于 // 内部父类)在此沿继承链解析(内部类在运行时已加载,可用反射取值)。 - $inherited = $this->resolveInheritedClassConst($class, $name); - if ($inherited !== null) { + [$inheritedFound, $inherited] = $this->resolveInheritedClassConst($class, $name); + if ($inheritedFound) { return $inherited; } // Resolve enum case references. Enum cases are runtime objects; their @@ -2106,9 +2106,9 @@ CODE; * (如 LazyArrayObject 继承自内部类 \ArrayObject,其常量 ARRAY_AS_PROPS * 定义于内部父类中;内部类在运行时已加载,可用 PHP 反射取值)。 * - * @return mixed|null 解析到的值;未找到返回 null + * @return array{bool, mixed} [是否找到, 常量值] */ - protected function resolveInheritedClassConst(string $class, string $name): mixed + protected function resolveInheritedClassConst(string $class, string $name): array { $current = ltrim($class, '\\'); $visited = []; @@ -2119,24 +2119,67 @@ CODE; if ($classDef->hasConstant($name)) { $constDef = $classDef->getConstant($name); if ($constDef->valueExpr !== null) { - return $constDef->valueExpr->value; + return [true, $this->evaluateClassConstValue(null, $constDef, $current, $name)]; } if ($constDef->class !== '' && defined($constDef->class . '::' . $name)) { - return constant($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 constant($constName); + return [true, constant($constName)]; } break; } else { break; } } - return null; + 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 diff --git a/tests/aot/const/class-const-default-value.phpt b/tests/aot/const/class-const-default-value.phpt index 346c2f31..e66dc96e 100644 --- a/tests/aot/const/class-const-default-value.phpt +++ b/tests/aot/const/class-const-default-value.phpt @@ -5,19 +5,43 @@ Class constant as default parameter value (self / class name / FQCN, including c class LazyArrayObject extends \ArrayObject { + public const NULL_VALUE = null; + public function test($value1 = self::ARRAY_AS_PROPS, $value2 = LazyArrayObject::ARRAY_AS_PROPS, $value3 = \ArrayObject::ARRAY_AS_PROPS) { var_dump($value1, $value2, $value3); } + + public function nullDefault($value = self::NULL_VALUE) + { + var_dump($value); + } +} + +class ParentDefaultValue +{ + public const VALUE = 42; +} + +class ChildDefaultValue extends ParentDefaultValue +{ + public function inheritedDefault($value = self::VALUE) + { + var_dump($value); + } } function main() { $test = new LazyArrayObject; $test->test(); + $test->nullDefault(); + (new ChildDefaultValue)->inheritedDefault(); } ?> --EXPECT-- int(2) int(2) int(2) +NULL +int(42)