Commit 8c90743609a for php.net
commit 8c90743609a0701bd0d4ed931f6cf6a073a69cf9
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Wed Aug 12 06:39:54 2026 -0400
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
diff --git a/Zend/tests/get_class_methods/gh17126.phpt b/Zend/tests/get_class_methods/gh17126.phpt
new file mode 100644
index 00000000000..94cc28a8a9b
--- /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 860413f8411..6aa47abd2c6 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);
+ }
}
/* }}} */