Commit d0852d54bab for php.net

commit d0852d54babbc668e27a3b95577830f9e6e8b237
Author: Christian Schneider <github@cschneid.com>
Date:   Thu Apr 2 18:49:33 2026 +0200

    [RFC] Add DocComments for function parameters (#21279)

    RFC: https://wiki.php.net/rfc/parameter-doccomments

    ---------

    Co-authored-by: Christian Schneider <schneider@search.ch>
    Co-authored-by: Tim Düsterhus <tim@tideways-gmbh.com>
    Co-authored-by: Daniel Scherzer <daniel.e.scherzer@gmail.com>

diff --git a/NEWS b/NEWS
index 44f53eb99c7..bb7df3d41c9 100644
--- a/NEWS
+++ b/NEWS
@@ -100,6 +100,7 @@ PHP                                                                        NEWS
     (ilutov)
   . Fixed bug GH-21362 (ReflectionMethod::invoke/invokeArgs() did not verify
     Closure instance identity for Closure::__invoke()). (Ilia Alshanetsky)
+  . Added ReflectionParameter::getDocComment(). (chschneider)

 - Session:
   . Fixed bug 71162 (updateTimestamp never called when session data is empty).
diff --git a/UPGRADING b/UPGRADING
index 8c312f1814a..560e0a5eca8 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -144,6 +144,8 @@ PHP 8.6 UPGRADE NOTES
   . ReflectionConstant::inNamespace()
   . Added ReflectionProperty::isReadable() and ReflectionProperty::isWritable().
     RFC: https://wiki.php.net/rfc/isreadable-iswriteable
+  . Added ReflectionParameter::getDocComment().
+    RFC: https://wiki.php.net/rfc/parameter-doccomments

 - Intl:
   . `grapheme_strrev()` returns strrev for grapheme cluster unit.
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index 211a5d14e6c..bd3f89a2450 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -2997,6 +2997,7 @@ ZEND_API void zend_convert_internal_arg_info(zend_arg_info *new_arg_info, const
 		new_arg_info->name = NULL;
 		new_arg_info->default_value = NULL;
 	}
+	new_arg_info->doc_comment = NULL;
 	new_arg_info->type = arg_info->type;
 	zend_convert_internal_arg_info_type(&new_arg_info->type, persistent);
 }
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index ab6f2fb1e98..6734db09a2e 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -8027,6 +8027,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
 		} else {
 			arg_infos->type = (zend_type) ZEND_TYPE_INIT_CODE(fallback_return_type, 0, 0);
 		}
+		arg_infos->doc_comment = NULL;
 		arg_infos++;
 		op_array->fn_flags |= ZEND_ACC_HAS_RETURN_TYPE;

@@ -8125,6 +8126,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
 		arg_info->name = zend_string_copy(name);
 		arg_info->type = (zend_type) ZEND_TYPE_INIT_NONE(0);
 		arg_info->default_value = NULL;
+		arg_info->doc_comment = doc_comment_ast ? zend_string_copy(zend_ast_get_str(doc_comment_ast)) : NULL;

 		if (attributes_ast) {
 			zend_compile_attributes(
diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h
index 5414467f3f8..abe2a53fe74 100644
--- a/Zend/zend_compile.h
+++ b/Zend/zend_compile.h
@@ -515,6 +515,7 @@ typedef struct _zend_arg_info {
 	zend_string *name;
 	zend_type type;
 	zend_string *default_value;
+	zend_string *doc_comment;
 } zend_arg_info;

 /* the following structure repeats the layout of zend_internal_arg_info,
diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y
index e2686c7e1c5..57ebf02fe02 100644
--- a/Zend/zend_language_parser.y
+++ b/Zend/zend_language_parser.y
@@ -821,9 +821,9 @@ parameter:
 			{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $1 | $3 | $4, $2, $5, NULL,
 					NULL, $6 ? zend_ast_create_zval_from_str($6) : NULL, $7); }
 	|	optional_cpp_modifiers optional_type_without_static
-		is_reference is_variadic T_VARIABLE backup_doc_comment '=' expr optional_property_hook_list
-			{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $1 | $3 | $4, $2, $5, $8,
-					NULL, $6 ? zend_ast_create_zval_from_str($6) : NULL, $9); }
+		is_reference is_variadic T_VARIABLE '=' expr backup_doc_comment optional_property_hook_list
+			{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $1 | $3 | $4, $2, $5, $7,
+					NULL, $8 ? zend_ast_create_zval_from_str($8) : NULL, $9); }
 ;

 optional_type_without_static:
diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c
index 24b480ad71e..35de02b5572 100644
--- a/Zend/zend_opcode.c
+++ b/Zend/zend_opcode.c
@@ -648,6 +648,9 @@ ZEND_API void destroy_op_array(zend_op_array *op_array)
 			if (arg_info[i].name) {
 				zend_string_release_ex(arg_info[i].name, 0);
 			}
+			if (arg_info[i].doc_comment) {
+				zend_string_release_ex(arg_info[i].doc_comment, 0);
+			}
 			zend_type_release(arg_info[i].type, /* persistent */ false);
 		}
 		efree(arg_info);
diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c
index 9bc2496837c..568db085bb2 100644
--- a/ext/opcache/zend_persist.c
+++ b/ext/opcache/zend_persist.c
@@ -652,6 +652,9 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc
 				zend_accel_store_interned_string(arg_info[i].name);
 			}
 			zend_persist_type(&arg_info[i].type);
+			if (arg_info[i].doc_comment) {
+				zend_accel_store_interned_string(arg_info[i].doc_comment);
+			}
 		}
 		if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
 			arg_info++;
diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c
index 0b0ff51d0d4..657cc03eb39 100644
--- a/ext/opcache/zend_persist_calc.c
+++ b/ext/opcache/zend_persist_calc.c
@@ -306,6 +306,9 @@ static void zend_persist_op_array_calc_ex(zend_op_array *op_array)
 				ADD_INTERNED_STRING(arg_info[i].name);
 			}
 			zend_persist_type_calc(&arg_info[i].type);
+			if (arg_info[i].doc_comment) {
+				ADD_INTERNED_STRING(arg_info[i].doc_comment);
+			}
 		}
 	}

diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c
index 957339b869e..9665d145328 100644
--- a/ext/reflection/php_reflection.c
+++ b/ext/reflection/php_reflection.c
@@ -2649,6 +2649,22 @@ ZEND_METHOD(ReflectionParameter, __toString)

 /* }}} */

+/* {{{ Returns the doc comment for this parameter */
+ZEND_METHOD(ReflectionParameter, getDocComment)
+{
+	reflection_object *intern;
+	parameter_reference *param;
+
+	ZEND_PARSE_PARAMETERS_NONE();
+
+	GET_REFLECTION_OBJECT_PTR(param);
+	if (param->arg_info->doc_comment) {
+		RETURN_STR_COPY(param->arg_info->doc_comment);
+	}
+	RETURN_FALSE;
+}
+/* }}} */
+
 /* {{{ Returns this parameter's name */
 ZEND_METHOD(ReflectionParameter, getName)
 {
diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php
index b0273a3174f..dd605100f8b 100644
--- a/ext/reflection/php_reflection.stub.php
+++ b/ext/reflection/php_reflection.stub.php
@@ -652,6 +652,8 @@ public function __construct($function, int|string $param) {}

     public function __toString(): string {}

+    public function getDocComment(): string|false {}
+
     /** @tentative-return-type */
     public function getName(): string {}

diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h
index 66605a22bbd..65571f38d43 100644
Binary files a/ext/reflection/php_reflection_arginfo.h and b/ext/reflection/php_reflection_arginfo.h differ
diff --git a/ext/reflection/php_reflection_decl.h b/ext/reflection/php_reflection_decl.h
index a5e8affd0be..a87e1635419 100644
Binary files a/ext/reflection/php_reflection_decl.h and b/ext/reflection/php_reflection_decl.h differ
diff --git a/ext/reflection/tests/ReflectionParameter_getDocComment_basic.phpt b/ext/reflection/tests/ReflectionParameter_getDocComment_basic.phpt
new file mode 100644
index 00000000000..46e47bf6032
--- /dev/null
+++ b/ext/reflection/tests/ReflectionParameter_getDocComment_basic.phpt
@@ -0,0 +1,248 @@
+--TEST--
+Test ReflectionParameter::getDocComment() usage.
+--INI--
+opcache.save_comments=1
+--FILE--
+<?php
+
+class A
+{
+function method(
+    /**
+     * My Doc Comment for $a
+     *
+     */
+    $a, $b, $c,
+    /**
+     * My Doc Comment for $d
+     */
+    $d,
+    // Not a doc comment
+    /**Not a doc comment */
+    $e,
+    /**
+     * Doc comment for $f
+     */
+    $f,
+	$g /** Doc comment for $g after parameter */,
+	/** Doc comment for $h */
+	$h /** Doc comment for $h after parameter */,
+) {}
+
+public string $property {
+     set(
+          /** Doc Comment for property hook parameter $value */
+          string $value
+     ) { $this->property = $value; }
+}
+}
+
+function global_function(
+    /**
+     * My Doc Comment for $a
+     *
+     */
+    $a, $b, $c,
+    /**
+     * My Doc Comment for $d
+     */
+    $d,
+    // Not a doc comment
+    /**Not a doc comment */
+    $e,
+    /**
+     * Doc comment for $f
+     */
+    $f,
+	$g /** Doc comment for $g after parameter */,
+	/** Doc comment for $h */
+	$h /** Doc comment for $h after parameter */,
+) {}
+
+$closure = function(
+    /**
+     * My Doc Comment for $a
+     *
+     */
+    $a, $b, $c,
+    /**
+     * My Doc Comment for $d
+     */
+    $d,
+    // Not a doc comment
+    /**Not a doc comment */
+    $e,
+    /**
+     * Doc comment for $f
+     */
+    $f,
+	$g /** Doc comment for $g after parameter */,
+	/** Doc comment for $h */
+	$h /** Doc comment for $h after parameter */,
+) {};
+
+$arrow_function = fn(
+    /**
+     * My Doc Comment for $a
+     *
+     */
+    $a, $b, $c,
+    /**
+     * My Doc Comment for $d
+     */
+    $d,
+    // Not a doc comment
+    /**Not a doc comment */
+    $e,
+    /**
+     * Doc comment for $f
+     */
+    $f,
+	$g /** Doc comment for $g after parameter */,
+	/** Doc comment for $h */
+	$h /** Doc comment for $h after parameter */,
+) => true;
+
+foreach([
+		'A::method'       => (new ReflectionClass('A'))->getMethod('method'),
+		'global_function' => new ReflectionFunction('global_function'),
+		'closure'         => new ReflectionFunction($closure),
+		'arrow_function'  => new ReflectionFunction($arrow_function),
+		'property hook'   => (new ReflectionClass('A'))->getProperty('property')->getHook(PropertyHookType::Set),
+	] as $function => $rc) {
+    $rps = $rc->getParameters();
+    foreach($rps as $rp) {
+        echo "\n---> Doc comment for $function parameter $" . $rp->getName() . ":\n";
+        var_dump($rp->getDocComment());
+    }
+}
+
+?>
+--EXPECTF--
+---> Doc comment for A::method parameter $a:
+string(%d) "/**
+     * My Doc Comment for $a
+     *
+     */"
+
+---> Doc comment for A::method parameter $b:
+bool(false)
+
+---> Doc comment for A::method parameter $c:
+bool(false)
+
+---> Doc comment for A::method parameter $d:
+string(%d) "/**
+     * My Doc Comment for $d
+     */"
+
+---> Doc comment for A::method parameter $e:
+bool(false)
+
+---> Doc comment for A::method parameter $f:
+string(%d) "/**
+     * Doc comment for $f
+     */"
+
+---> Doc comment for A::method parameter $g:
+string(%d) "/** Doc comment for $g after parameter */"
+
+---> Doc comment for A::method parameter $h:
+string(%d) "/** Doc comment for $h after parameter */"
+
+---> Doc comment for global_function parameter $a:
+string(%d) "/**
+     * My Doc Comment for $a
+     *
+     */"
+
+---> Doc comment for global_function parameter $b:
+bool(false)
+
+---> Doc comment for global_function parameter $c:
+bool(false)
+
+---> Doc comment for global_function parameter $d:
+string(%d) "/**
+     * My Doc Comment for $d
+     */"
+
+---> Doc comment for global_function parameter $e:
+bool(false)
+
+---> Doc comment for global_function parameter $f:
+string(%d) "/**
+     * Doc comment for $f
+     */"
+
+---> Doc comment for global_function parameter $g:
+string(%d) "/** Doc comment for $g after parameter */"
+
+---> Doc comment for global_function parameter $h:
+string(%d) "/** Doc comment for $h after parameter */"
+
+---> Doc comment for closure parameter $a:
+string(%d) "/**
+     * My Doc Comment for $a
+     *
+     */"
+
+---> Doc comment for closure parameter $b:
+bool(false)
+
+---> Doc comment for closure parameter $c:
+bool(false)
+
+---> Doc comment for closure parameter $d:
+string(%d) "/**
+     * My Doc Comment for $d
+     */"
+
+---> Doc comment for closure parameter $e:
+bool(false)
+
+---> Doc comment for closure parameter $f:
+string(%d) "/**
+     * Doc comment for $f
+     */"
+
+---> Doc comment for closure parameter $g:
+string(%d) "/** Doc comment for $g after parameter */"
+
+---> Doc comment for closure parameter $h:
+string(%d) "/** Doc comment for $h after parameter */"
+
+---> Doc comment for arrow_function parameter $a:
+string(%d) "/**
+     * My Doc Comment for $a
+     *
+     */"
+
+---> Doc comment for arrow_function parameter $b:
+bool(false)
+
+---> Doc comment for arrow_function parameter $c:
+bool(false)
+
+---> Doc comment for arrow_function parameter $d:
+string(%d) "/**
+     * My Doc Comment for $d
+     */"
+
+---> Doc comment for arrow_function parameter $e:
+bool(false)
+
+---> Doc comment for arrow_function parameter $f:
+string(%d) "/**
+     * Doc comment for $f
+     */"
+
+---> Doc comment for arrow_function parameter $g:
+string(%d) "/** Doc comment for $g after parameter */"
+
+---> Doc comment for arrow_function parameter $h:
+string(%d) "/** Doc comment for $h after parameter */"
+
+---> Doc comment for property hook parameter $value:
+string(%d) "/** Doc Comment for property hook parameter $value */"
+
diff --git a/ext/reflection/tests/ReflectionParameter_getDocComment_indented.phpt b/ext/reflection/tests/ReflectionParameter_getDocComment_indented.phpt
new file mode 100644
index 00000000000..ede7a00e0ed
--- /dev/null
+++ b/ext/reflection/tests/ReflectionParameter_getDocComment_indented.phpt
@@ -0,0 +1,84 @@
+--TEST--
+Test ReflectionParameter::getDocComment() usage when methods are indented.
+--INI--
+opcache.save_comments=1
+--FILE--
+<?php
+
+class A {
+     function method(
+     /**
+          * My Doc Comment for $a
+          *
+          */
+     $a, $b, $c,
+     /**
+          * My Doc Comment for $d
+          */
+     $d,
+     // Not a doc comment
+     /**Not a doc comment */
+     $e,
+     /**
+          * Doc comment for $f
+          */
+     $f,
+          $g /** Doc comment for $g after parameter */,
+          /** Doc comment for $h */
+          $h /** Doc comment for $h after parameter */,
+     ) {}
+
+     public string $property {
+          set(
+               /** Doc Comment for property hook parameter $value */
+               string $value
+          ) { $this->property = $value; }
+     }
+}
+
+foreach([
+        'A::method'       => (new ReflectionClass('A'))->getMethod('method'),
+        'property hook'   => (new ReflectionClass('A'))->getProperty('property')->getHook(PropertyHookType::Set),
+    ] as $function => $rc) {
+    $rps = $rc->getParameters();
+    foreach($rps as $rp) {
+        echo "\n---> Doc comment for $function parameter $" . $rp->getName() . ":\n";
+        var_dump($rp->getDocComment());
+    }
+}
+
+?>
+--EXPECTF--
+---> Doc comment for A::method parameter $a:
+string(%d) "/**
+          * My Doc Comment for $a
+          *
+          */"
+
+---> Doc comment for A::method parameter $b:
+bool(false)
+
+---> Doc comment for A::method parameter $c:
+bool(false)
+
+---> Doc comment for A::method parameter $d:
+string(%d) "/**
+          * My Doc Comment for $d
+          */"
+
+---> Doc comment for A::method parameter $e:
+bool(false)
+
+---> Doc comment for A::method parameter $f:
+string(%d) "/**
+          * Doc comment for $f
+          */"
+
+---> Doc comment for A::method parameter $g:
+string(%d) "/** Doc comment for $g after parameter */"
+
+---> Doc comment for A::method parameter $h:
+string(%d) "/** Doc comment for $h after parameter */"
+
+---> Doc comment for property hook parameter $value:
+string(%d) "/** Doc Comment for property hook parameter $value */"
diff --git a/ext/reflection/tests/ReflectionParameter_getDocComment_property_hooks.phpt b/ext/reflection/tests/ReflectionParameter_getDocComment_property_hooks.phpt
new file mode 100644
index 00000000000..5689a2c46ac
--- /dev/null
+++ b/ext/reflection/tests/ReflectionParameter_getDocComment_property_hooks.phpt
@@ -0,0 +1,143 @@
+--TEST--
+Test ReflectionParameter::getDocComment() usage for property with hook.
+--INI--
+opcache.save_comments=1
+--FILE--
+<?php
+
+class A
+{
+     public function __construct(
+          /** $foo */
+          public string $foo {
+               /** getter */
+               get { return "foo"; }
+               /** setter */
+               set(string $value /** $value */) {}
+          },
+          public string $bar /** $bar */ {
+               /** getter */
+               get { return "bar"; }
+               /** setter */
+               set(string $value /** $value */) {}
+          },
+          public string $baz {
+               /** getter */
+               get { return "foo"; }
+               /** setter */
+               set(string $value /** $value */) {}
+          } /** $baz */,
+     ) {}
+}
+
+$rc = new ReflectionClass('A');
+$rp = $rc->getProperty('foo');
+echo "\n---> Doc comment for A::property $" . $rp->getName() . ":\n";
+var_dump($rp->getDocComment());
+
+$rh = $rp->getHook(PropertyHookType::Get);
+echo "\n---> Doc comment for A::property " . $rh->getName() . ":\n";
+var_dump($rh->getDocComment());
+
+$rh = $rp->getHook(PropertyHookType::Set);
+echo "\n---> Doc comment for A::property " . $rh->getName() . ":\n";
+var_dump($rh->getDocComment());
+
+$rp = $rh->getParameters()[0];
+echo "\n---> Doc comment for A::property \$foo::set parameter $" . $rp->getName() . ":\n";
+var_dump($rp->getDocComment());
+
+// ---------
+
+$rp = $rc->getConstructor()->getParameters()[1];
+echo "\n---> Doc comment for A::constructor parameter $" . $rp->getName() . ":\n";
+var_dump($rp->getDocComment());
+
+$rp = $rc->getProperty('bar');
+echo "\n---> Doc comment for A::property $" . $rp->getName() . ":\n";
+var_dump($rp->getDocComment());
+
+$rh = $rp->getHook(PropertyHookType::Get);
+echo "\n---> Doc comment for A::property " . $rh->getName() . ":\n";
+var_dump($rh->getDocComment());
+
+$rh = $rp->getHook(PropertyHookType::Set);
+echo "\n---> Doc comment for A::property " . $rh->getName() . ":\n";
+var_dump($rh->getDocComment());
+
+$rp = $rh->getParameters()[0];
+echo "\n---> Doc comment for A::property \$bar::set parameter $" . $rp->getName() . ":\n";
+var_dump($rp->getDocComment());
+
+// ---------
+
+$rp = $rc->getConstructor()->getParameters()[2];
+echo "\n---> Doc comment for A::constructor parameter $" . $rp->getName() . ":\n";
+var_dump($rp->getDocComment());
+
+$rp = $rc->getProperty('baz');
+echo "\n---> Doc comment for A::property $" . $rp->getName() . ":\n";
+var_dump($rp->getDocComment());
+
+$rh = $rp->getHook(PropertyHookType::Get);
+echo "\n---> Doc comment for A::property " . $rh->getName() . ":\n";
+var_dump($rh->getDocComment());
+
+$rh = $rp->getHook(PropertyHookType::Set);
+echo "\n---> Doc comment for A::property " . $rh->getName() . ":\n";
+var_dump($rh->getDocComment());
+
+$rp = $rh->getParameters()[0];
+echo "\n---> Doc comment for A::property \$baz::set parameter $" . $rp->getName() . ":\n";
+var_dump($rp->getDocComment());
+
+$rp = $rc->getConstructor()->getParameters()[0];
+echo "\n---> Doc comment for A::constructor parameter $" . $rp->getName() . ":\n";
+var_dump($rp->getDocComment());
+
+?>
+--EXPECTF--
+---> Doc comment for A::property $foo:
+string(11) "/** $foo */"
+
+---> Doc comment for A::property $foo::get:
+string(13) "/** getter */"
+
+---> Doc comment for A::property $foo::set:
+string(13) "/** setter */"
+
+---> Doc comment for A::property $foo::set parameter $value:
+string(13) "/** $value */"
+
+---> Doc comment for A::constructor parameter $bar:
+string(11) "/** $bar */"
+
+---> Doc comment for A::property $bar:
+string(11) "/** $bar */"
+
+---> Doc comment for A::property $bar::get:
+string(13) "/** getter */"
+
+---> Doc comment for A::property $bar::set:
+string(13) "/** setter */"
+
+---> Doc comment for A::property $bar::set parameter $value:
+string(13) "/** $value */"
+
+---> Doc comment for A::constructor parameter $baz:
+bool(false)
+
+---> Doc comment for A::property $baz:
+bool(false)
+
+---> Doc comment for A::property $baz::get:
+string(13) "/** getter */"
+
+---> Doc comment for A::property $baz::set:
+string(13) "/** setter */"
+
+---> Doc comment for A::property $baz::set parameter $value:
+string(13) "/** $value */"
+
+---> Doc comment for A::constructor parameter $foo:
+string(11) "/** $foo */"