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 */"