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