[PHP-CVS] [php-src] master: PFA: Fix magic method resolution (#23251)

[email protected] (Arnaud Le Blanc via GitHub)
Newsgroups php.cvs
Message-ID <[email protected]>
Author: Arnaud Le Blanc (arnaud-lb)
Committer: GitHub (web-flow)
Pusher: arnaud-lb
Date: 2026-08-24T17:56:04+02:00

Commit: https://github.com/php/php-src/commit/f081c1854be9dd3baec7df8e0b183504da32405f
Raw diff: https://github.com/php/php-src/commit/f081c1854be9dd3baec7df8e0b183504da32405f.diff

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.

Changed paths:
  A  Zend/tests/partial_application/default_arg_scope.phpt
  A  Zend/tests/partial_application/magic_scope.phpt
  M  Zend/tests/partial_application/magic_001.phpt
  M  Zend/tests/partial_application/magic_002.phpt
  M  Zend/zend_ast.c
  M  Zend/zend_partial.c
  M  Zend/zend_partial.h
  M  Zend/zend_vm_def.h
  M  Zend/zend_vm_execute.h


Diff:

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 000000000000..5cab50c5af26
--- /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 bdcc10675785..a3a7a9673b8f 100644
--- a/Zend/tests/partial_application/magic_001.phpt
+++ b/Zend/tests/partial_application/magic_001.phpt
@@ -46,7 +46,7 @@ Closure [ <user> public method {closure:%s:%d} ] {
     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 1d5efaea7c63..2771c823e4d4 100644
--- a/Zend/tests/partial_application/magic_002.phpt
+++ b/Zend/tests/partial_application/magic_002.phpt
@@ -31,7 +31,7 @@ echo (string) new ReflectionFunction($bar);
 $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 @@ Foo::method
 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 @@ int(10)
 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 000000000000..82a69f955f96
--- /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 4b070d9d5d58..6a71fc5aeca3 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 643cc634e7e8..de243bd7e65f 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 d3fcdae6afc8..285db1161e78 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 01131b5d3ae0..0e35b5bb95fa 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 5061d772ee82..c6158bd507d9 100644
--- a/Zend/zend_vm_execute.h
+++ b/Zend/zend_vm_execute.h
@@ -8654,7 +8654,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV
 	}
 
 	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,
@@ -12027,7 +12027,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV
 	}
 
 	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,
@@ -61499,7 +61499,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_P
 	}
 
 	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,
@@ -64770,7 +64770,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_P
 	}
 
 	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,
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.