Commit 44a2a2c3db9 for php
commit 44a2a2c3db9e4e06c41a0603c3826fac3b1b99f3
Author: Jorg Sowa <jorg.sowa@gmail.com>
Date: Tue Nov 3 11:07:28 2026 +0100
Fix FCC in trait const expressions resolving to the first using class
The resolved method was cached on the shared AST node, so self::/parent::
and visibility were only checked for the first class evaluating it.
Closes GH-25083
diff --git a/NEWS b/NEWS
index 5de475a2252..8fcac3a57a5 100644
--- a/NEWS
+++ b/NEWS
@@ -37,6 +37,9 @@ PHP NEWS
(Jorg Sowa)
. Fixed bug GH-25061 ($x = $x - 1 on null is optimized to PRE_DEC).
(Lazizbek Ergashev)
+ . Fixed first-class callables to static methods in trait constant
+ expressions resolving to the method of the first using class and
+ bypassing visibility checks. (Jorg Sowa)
- CLI:
. Fixed bug GH-24697 (PHP_HTTP_PARSER_STRICT build option doesn't work).
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_001.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_001.phpt
new file mode 100644
index 00000000000..8199b47afa9
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_001.phpt
@@ -0,0 +1,18 @@
+--TEST--
+FCC for self:: static method in trait default argument is resolved per using class
+--FILE--
+<?php
+
+trait T {
+ public static function make($f = self::who(...)) { return $f(); }
+}
+class A { use T; public static function who() { return 'A'; } }
+class B { use T; public static function who() { return 'B'; } }
+
+echo A::make(), PHP_EOL;
+echo B::make(), PHP_EOL;
+
+?>
+--EXPECT--
+A
+B
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_002.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_002.phpt
new file mode 100644
index 00000000000..5390f6337a5
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_002.phpt
@@ -0,0 +1,20 @@
+--TEST--
+FCC for parent:: static method in trait default argument is resolved per using class
+--FILE--
+<?php
+
+class P1 { public static function who() { return 'P1'; } }
+class P2 { public static function who() { return 'P2'; } }
+trait T {
+ public static function make($f = parent::who(...)) { return $f(); }
+}
+class A extends P1 { use T; }
+class B extends P2 { use T; }
+
+echo A::make(), PHP_EOL;
+echo B::make(), PHP_EOL;
+
+?>
+--EXPECT--
+P1
+P2
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_003.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_003.phpt
new file mode 100644
index 00000000000..ae63777392b
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_003.phpt
@@ -0,0 +1,23 @@
+--TEST--
+FCC for self:: static method in trait constant and property default is resolved per using class
+--FILE--
+<?php
+
+trait T {
+ const C = self::who(...);
+ public $p = self::who(...);
+}
+class A { use T; public static function who() { return 'A'; } }
+class B { use T; public static function who() { return 'B'; } }
+
+echo (A::C)(), PHP_EOL;
+echo (B::C)(), PHP_EOL;
+echo ((new A)->p)(), PHP_EOL;
+echo ((new B)->p)(), PHP_EOL;
+
+?>
+--EXPECT--
+A
+B
+A
+B
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_004.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_004.phpt
new file mode 100644
index 00000000000..da1608e2d91
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_004.phpt
@@ -0,0 +1,22 @@
+--TEST--
+FCC for self:: private static method in trait default argument does not leak to other using classes
+--FILE--
+<?php
+
+trait T {
+ public static function make($f = self::who(...)) { return $f(); }
+}
+class A { use T; private static function who() { return 'A'; } }
+class B { use T; }
+
+echo A::make(), PHP_EOL;
+try {
+ B::make();
+} catch (Error $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+A
+Error: Call to undefined method B::who()
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_005.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_005.phpt
new file mode 100644
index 00000000000..50382a44993
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_005.phpt
@@ -0,0 +1,22 @@
+--TEST--
+FCC for named class private static method in trait default argument checks visibility per using class
+--FILE--
+<?php
+
+trait T {
+ public static function make($f = A::who(...)) { return $f(); }
+}
+class A { use T; private static function who() { return 'A'; } }
+class B { use T; }
+
+echo A::make(), PHP_EOL;
+try {
+ B::make();
+} catch (Error $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+A
+Error: Call to private method A::who() from scope B
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_006.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_006.phpt
new file mode 100644
index 00000000000..4ffb56841ca
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_006.phpt
@@ -0,0 +1,20 @@
+--TEST--
+FCC for named class public static method in trait default argument is shared across using classes
+--FILE--
+<?php
+
+trait T {
+ public static function make($f = A::who(...)) { return $f(); }
+}
+class A { use T; public static function who() { return 'A'; } }
+class B { use T; }
+
+echo A::make(), PHP_EOL;
+echo B::make(), PHP_EOL;
+echo A::make(), PHP_EOL;
+
+?>
+--EXPECT--
+A
+A
+A
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index 791bc8c1f23..f7eb7ee05e6 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -1148,7 +1148,11 @@ ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate_inner(
}
}
- ZEND_MAP_PTR_SET(fcc_ast->fptr, fptr);
+ /* The AST may be shared between scopes (e.g. via traits), so only cache scope-independent results. */
+ if ((ast->child[0]->attr >> ZEND_CONST_EXPR_NEW_FETCH_TYPE_SHIFT) == ZEND_FETCH_CLASS_DEFAULT
+ && (fptr->common.fn_flags & ZEND_ACC_PUBLIC)) {
+ ZEND_MAP_PTR_SET(fcc_ast->fptr, fptr);
+ }
}
break;