Commit f081c1854be for php.net
commit f081c1854be9dd3baec7df8e0b183504da32405f
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date: Mon Aug 24 17:56:04 2026 +0200
PFA: Fix magic method resolution (#23251)
We conveniently use the function's scope for the scope of the generated closure
as this allows const exprs referencing self:: or parent:: to behave normally.
However this affects method resolution for magic methods. Fix by using the
actual scope for PFAs of magic methods.
diff --git a/Zend/tests/partial_application/default_arg_scope.phpt b/Zend/tests/partial_application/default_arg_scope.phpt
new file mode 100644
index 00000000000..5cab50c5af2
--- /dev/null
+++ b/Zend/tests/partial_application/default_arg_scope.phpt
@@ -0,0 +1,47 @@
+--TEST--
+PFA default argument value scope
+--ENV--
+A=1
+--FILE--
+<?php
+
+if (getenv('A')) {
+ /* Relative class references are never resolved at compile time on traits */
+ trait T {
+ static function f($a, $b = self::VAL) {
+ var_dump($b);
+ }
+ }
+ trait U {
+ static function g($a, $b = parent::VAL) {
+ var_dump($b);
+ }
+ }
+}
+
+class C {
+ const VAL = 'C';
+ use T;
+}
+
+class D extends C {
+ const VAL = 'D';
+ use U;
+}
+
+C::f(0);
+D::f(0);
+C::f(?, ...)(0);
+D::f(?, ...)(0);
+
+D::g(0);
+D::g(?, ...)(0);
+
+?>
+--EXPECT--
+string(1) "C"
+string(1) "C"
+string(1) "C"
+string(1) "C"
+string(1) "C"
+string(1) "C"
diff --git a/Zend/tests/partial_application/magic_001.phpt b/Zend/tests/partial_application/magic_001.phpt
index bdcc1067578..a3a7a9673b8 100644
--- a/Zend/tests/partial_application/magic_001.phpt
+++ b/Zend/tests/partial_application/magic_001.phpt
@@ -46,7 +46,7 @@ public function __call($method, $arguments) {
Parameter #0 [ <required> mixed $arguments0 ]
}
}
-ArgumentCountError: Too few arguments to function Foo::{closure:%s:%d}(), 0 passed in %s on line %d and exactly 1 expected
+ArgumentCountError: Too few arguments to function Closure::{closure:%s:%d}(), 0 passed in %s on line %d and exactly 1 expected
Foo::method
int(1)
Foo::method
diff --git a/Zend/tests/partial_application/magic_002.phpt b/Zend/tests/partial_application/magic_002.phpt
index 1d5efaea7c6..2771c823e4d 100644
--- a/Zend/tests/partial_application/magic_002.phpt
+++ b/Zend/tests/partial_application/magic_002.phpt
@@ -31,7 +31,7 @@ public static function __callStatic($method, $arguments) {
$bar(100);
?>
--EXPECTF--
-Closure [ <user> static public method {closure:%s:%d} ] {
+Closure [ <user> static function {closure:%s:%d} ] {
@@ %s 10 - 10
- Parameters [1] {
@@ -42,7 +42,7 @@ public static function __callStatic($method, $arguments) {
int(1)
Foo::method
int(1)
-Closure [ <user> static public method {closure:%s:%d} ] {
+Closure [ <user> static function {closure:%s:%d} ] {
@@ %s 17 - 17
- Parameters [2] {
@@ -55,7 +55,7 @@ public static function __callStatic($method, $arguments) {
Foo::method
int(10)
int(20)
-Closure [ <user> static public method {closure:%s:%d} ] {
+Closure [ <user> static function {closure:%s:%d} ] {
@@ %s 24 - 24
- Bound Variables [1] {
diff --git a/Zend/tests/partial_application/magic_scope.phpt b/Zend/tests/partial_application/magic_scope.phpt
new file mode 100644
index 00000000000..82a69f955f9
--- /dev/null
+++ b/Zend/tests/partial_application/magic_scope.phpt
@@ -0,0 +1,49 @@
+--TEST--
+Magic method scope
+--CREDITS--
+Ryan @ Calif.io
+--FILE--
+<?php
+
+class InstanceTarget
+{
+ private function secret(string $value): void
+ {
+ echo "PRIVATE-INSTANCE:$value\n";
+ }
+
+ public function __call(string $name, array $arguments): void
+ {
+ echo "MAGIC-INSTANCE:$name:" . implode(',', $arguments) . "\n";
+ }
+}
+
+class StaticTarget
+{
+ private static function secret(string $value): void
+ {
+ echo "PRIVATE-STATIC:$value\n";
+ }
+
+ public static function __callStatic(string $name, array $arguments): void
+ {
+ echo "MAGIC-STATIC:$name:" . implode(',', $arguments) . "\n";
+ }
+}
+
+$instance = new InstanceTarget();
+
+$instance->secret('direct');
+StaticTarget::secret('direct');
+
+$instancePartial = $instance->secret(?);
+$staticPartial = StaticTarget::secret(?);
+$instancePartial('controlled');
+$staticPartial('controlled');
+
+?>
+--EXPECT--
+MAGIC-INSTANCE:secret:direct
+MAGIC-STATIC:secret:direct
+MAGIC-INSTANCE:secret:controlled
+MAGIC-STATIC:secret:controlled
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index 4b070d9d5d5..6a71fc5aeca 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -1349,7 +1349,7 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner(
if (uses_variadic_placeholder) {
flags |= ZEND_PARTIAL_USES_VARIADIC_PLACEHOLDER;
}
- zend_partial_create(result, &frame->This, fptr,
+ zend_partial_create(result, scope, &frame->This, fptr,
ZEND_CALL_NUM_ARGS(frame), ZEND_CALL_ARG(frame, 1),
extra_named_params, named_positions,
fcc_ast->filename, &ast->lineno,
diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c
index 643cc634e7e..de243bd7e65 100644
--- a/Zend/zend_partial.c
+++ b/Zend/zend_partial.c
@@ -1126,7 +1126,7 @@ static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval *
}
}
-void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
+void zend_partial_create(zval *result, zend_class_entry *scope, zval *this_ptr, zend_function *function,
uint32_t argc, zval *argv, zend_array *extra_named_params,
const zend_array *named_positions,
zend_string *declaring_filename,
@@ -1162,8 +1162,16 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
object = NULL;
}
+
+ /* We conveniently use the function's scope for the scope of the generated closure as this allows const exprs
+ * referencing self:: or parent:: to behave normally without rewriting them.
+ * This affects method resolution for magic methods, so use the actual scope for them. */
+ if (!(function->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) {
+ scope = function->common.scope;
+ }
+
zend_create_partial_closure(result, (zend_function*)op_array,
- function->common.scope, called_scope, object,
+ scope, called_scope, object,
(function->common.fn_flags & ZEND_ACC_CLOSURE) != 0);
zp_bind(result, function, argc, argv, extra_named_params, const_args);
diff --git a/Zend/zend_partial.h b/Zend/zend_partial.h
index d3fcdae6afc..285db1161e7 100644
--- a/Zend/zend_partial.h
+++ b/Zend/zend_partial.h
@@ -31,7 +31,7 @@ BEGIN_EXTERN_C()
* 'declaring_lineno_ptr' should be a pointer the zend_op.lineno or
* zend_ast.lineno that declares the PFA. The address is used to build a cache
* key. */
-void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
+void zend_partial_create(zval *result, zend_class_entry *scope, zval *this_ptr, zend_function *function,
uint32_t argc, zval *argv, zend_array *extra_named_params,
const zend_array *named_positions,
zend_string *declaring_filename,
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 01131b5d3ae..0e35b5bb95f 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -9897,7 +9897,7 @@ ZEND_VM_HANDLER(212, ZEND_CALLABLE_CONVERT_PARTIAL, CONST, CONST|UNUSED, NUM)
}
zend_partial_create(EX_VAR(opline->result.var),
- &call->This, call->func,
+ EX(func)->common.scope, &call->This, call->func,
ZEND_CALL_NUM_ARGS(call), ZEND_CALL_ARG(call, 1),
(ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) ?
call->extra_named_params : NULL,
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 5061d772ee8..c6158bd507d 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ