[php-src] master: Fix GH-17126: get_class_methods omits __invoke for Closure (#21879)

Ilia Alshanetsky via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Committer: GitHub (web-flow)
Pusher: iliaal
Date: 2026-08-12T06:39:54-04:00

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

Fix GH-17126: get_class_methods omits __invoke for Closure (#21879)

Closure::__invoke is documented and reachable via method_exists() and
ReflectionClass (getMethod, hasMethod, getMethods), but
get_class_methods() walks ce->function_table directly and Closure does
not register __invoke there: the parameter list is materialized
per-instance by zend_get_closure_invoke_method().

Append __invoke to the result when ce == zend_ce_closure, the same
predicate is_closure_invoke() already uses in ext/reflection.

Closes GH-17126

Changed paths:
  A  Zend/tests/get_class_methods/gh17126.phpt
  M  Zend/zend_builtin_functions.c


Diff:

diff --git a/Zend/tests/get_class_methods/gh17126.phpt b/Zend/tests/get_class_methods/gh17126.phpt
new file mode 100644
index 000000000000..94cc28a8a9b4
--- /dev/null
+++ b/Zend/tests/get_class_methods/gh17126.phpt
@@ -0,0 +1,48 @@
+--TEST--
+GH-17126 (get_class_methods($closure) doesn't return __invoke)
+--FILE--
+<?php
+
+$closure = fn (string $str) => "hello {$str}";
+
+echo "from object:\n";
+$methods = get_class_methods($closure);
+sort($methods);
+print_r($methods);
+
+echo "from class name:\n";
+$methods = get_class_methods('Closure');
+sort($methods);
+print_r($methods);
+
+echo "unrelated class unaffected:\n";
+class NoInvoke { public function foo() {} }
+print_r(get_class_methods('NoInvoke'));
+
+?>
+--EXPECT--
+from object:
+Array
+(
+    [0] => __invoke
+    [1] => bind
+    [2] => bindTo
+    [3] => call
+    [4] => fromCallable
+    [5] => getCurrent
+)
+from class name:
+Array
+(
+    [0] => __invoke
+    [1] => bind
+    [2] => bindTo
+    [3] => call
+    [4] => fromCallable
+    [5] => getCurrent
+)
+unrelated class unaffected:
+Array
+(
+    [0] => foo
+)
diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c
index 860413f84111..6aa47abd2c6b 100644
--- a/Zend/zend_builtin_functions.c
+++ b/Zend/zend_builtin_functions.c
@@ -957,6 +957,11 @@ ZEND_FUNCTION(get_class_methods)
 			zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &method_name);
 		}
 	} ZEND_HASH_FOREACH_END();
+
+	if (ce == zend_ce_closure) {
+		ZVAL_STR_COPY(&method_name, ZSTR_KNOWN(ZEND_STR_MAGIC_INVOKE));
+		zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &method_name);
+	}
 }
 /* }}} */
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.