Commit eef0f71aa7f for php
commit eef0f71aa7f624cac8e428221b5c355dfecdebb5
Author: Kamil Tekiela <tekiela246@gmail.com>
Date: Thu Oct 22 15:34:57 2026 +0100
ext/reflection: Make properties readonly (#24282)
diff --git a/UPGRADING b/UPGRADING
index a232e35aa00..adfe422d90a 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -29,6 +29,13 @@ PHP 8.7 UPGRADE NOTES
not present in the result set. It previously reported the condition
through PDO::ATTR_ERRMODE and returned false.
+- Reflection:
+ . The public $name and $class properties of ReflectionClass,
+ ReflectionClassConstant, ReflectionProperty, ReflectionMethod,
+ ReflectionFunctionAbstract, ReflectionParameter, ReflectionExtension,
+ ReflectionZendExtension, ReflectionAttribute and ReflectionConstant are
+ now readonly.
+
- Session:
. Setting session.cookie_path to a value containing one of ";\r\n", or
session.cookie_domain to a value containing one of ",; \t\r\n\013\014",
diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php
index b95f01d2ad2..b55978cd1df 100644
--- a/ext/reflection/php_reflection.stub.php
+++ b/ext/reflection/php_reflection.stub.php
@@ -22,7 +22,7 @@ interface Reflector extends Stringable
/** @not-serializable */
abstract class ReflectionFunctionAbstract implements Reflector
{
- public string $name;
+ public readonly string $name;
/** @implementation-alias ReflectionClass::__clone */
private function __clone(): void {}
@@ -179,7 +179,7 @@ class ReflectionMethod extends ReflectionFunctionAbstract
/** @cvalue ZEND_ACC_FINAL */
public const int IS_FINAL = UNKNOWN;
- public string $class;
+ public readonly string $class;
public function __construct(object|string $objectOrMethod, ?string $method = null) {}
@@ -254,7 +254,7 @@ class ReflectionClass implements Reflector
/** @cvalue ZEND_LAZY_OBJECT_SKIP_DESTRUCTOR */
public const int SKIP_DESTRUCTOR = UNKNOWN;
- public string $name;
+ public readonly string $name;
private function __clone(): void {}
@@ -473,8 +473,8 @@ class ReflectionProperty implements Reflector
/** @cvalue ZEND_ACC_FINAL */
public const int IS_FINAL = UNKNOWN;
- public string $name;
- public string $class;
+ public readonly string $name;
+ public readonly string $class;
/** @implementation-alias ReflectionClass::__clone */
private function __clone(): void {}
@@ -592,8 +592,8 @@ class ReflectionClassConstant implements Reflector
/** @cvalue ZEND_ACC_FINAL */
public const int IS_FINAL = UNKNOWN;
- public string $name;
- public string $class;
+ public readonly string $name;
+ public readonly string $class;
/** @implementation-alias ReflectionClass::__clone */
private function __clone(): void {}
@@ -642,7 +642,7 @@ public function getType(): ?ReflectionType {}
/** @not-serializable */
class ReflectionParameter implements Reflector
{
- public string $name;
+ public readonly string $name;
/** @implementation-alias ReflectionClass::__clone */
private function __clone(): void {}
@@ -756,7 +756,7 @@ public function getTypes(): array {}
/** @not-serializable */
class ReflectionExtension implements Reflector
{
- public string $name;
+ public readonly string $name;
/** @implementation-alias ReflectionClass::__clone */
private function __clone(): void {}
@@ -802,7 +802,7 @@ public function isTemporary(): bool {}
/** @not-serializable */
class ReflectionZendExtension implements Reflector
{
- public string $name;
+ public readonly string $name;
/** @implementation-alias ReflectionClass::__clone */
private function __clone(): void {}
@@ -846,7 +846,7 @@ class ReflectionAttribute implements Reflector
/** @cvalue REFLECTION_ATTRIBUTE_IS_INSTANCEOF */
public const int IS_INSTANCEOF = UNKNOWN;
- public string $name;
+ public readonly string $name;
public function getName(): string {}
public function inNamespace(): bool {}
@@ -918,7 +918,7 @@ public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT): array {
*/
class ReflectionConstant implements Reflector
{
- public string $name;
+ public readonly string $name;
public function __construct(string $name) {}
diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h
index 661372516d5..eafbc9c0bd2 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 555ed84feeb..e58dce51b39 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/ReflectionClassConstant_unset_name.phpt b/ext/reflection/tests/ReflectionClassConstant_unset_name.phpt
index a571f4a4776..2e7e3e75fe5 100644
--- a/ext/reflection/tests/ReflectionClassConstant_unset_name.phpt
+++ b/ext/reflection/tests/ReflectionClassConstant_unset_name.phpt
@@ -10,19 +10,18 @@ class Test {
// This is unsupported and the actual behavior doesn't matter.
// Just make sure it doesn't crash.
$rc = new ReflectionClassConstant(Test::class, 'C');
-unset($rc->name);
try {
- var_dump($rc->getName());
-} catch (Error $e) {
- echo $e->getMessage(), "\n";
-}
-try {
- echo $rc, "\n";
+ unset($rc->name);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
+var_dump($rc->getName());
+
+echo $rc, "\n";
+
?>
--EXPECT--
-Typed property ReflectionClassConstant::$name must not be accessed before initialization
-Typed property ReflectionClassConstant::$name must not be accessed before initialization
+Cannot unset readonly property ReflectionClassConstant::$name
+string(1) "C"
+Constant [ public int C ] { 1 }
diff --git a/ext/reflection/tests/ReflectionClass_toString_001.phpt b/ext/reflection/tests/ReflectionClass_toString_001.phpt
index fd5d83e9174..03495847857 100644
--- a/ext/reflection/tests/ReflectionClass_toString_001.phpt
+++ b/ext/reflection/tests/ReflectionClass_toString_001.phpt
@@ -27,7 +27,7 @@
}
- Properties [1] {
- Property [ public string $name ]
+ Property [ public protected(set) readonly string $name ]
}
- Methods [64] {
diff --git a/ext/reflection/tests/gh24147.phpt b/ext/reflection/tests/gh24147.phpt
new file mode 100644
index 00000000000..a19bb87dc9d
--- /dev/null
+++ b/ext/reflection/tests/gh24147.phpt
@@ -0,0 +1,97 @@
+--TEST--
+GH-24147: Reflection $name and $class properties cannot be modified
+--FILE--
+<?php
+
+class B {
+ public string $x;
+}
+
+function byRef(&$x) { $x = 'Z'; }
+
+$tests = [
+ 'compound assignment' => function ($r) { $r->class .= 'Z'; },
+ 'reference' => function ($r) { $ref = &$r->name; $ref = 'Z'; },
+ 'string offset' => function ($r) { $r->name[0] = 'Z'; },
+ 'by-ref argument' => function ($r) { byRef($r->class); },
+ 'dynamic by-ref argument' => function ($r) { $fn = 'byRef'; $fn($r->class); },
+ 'foreach by reference' => function ($r) { foreach ($r as &$v) { $v = 'Z'; } },
+ 'unset' => function ($r) { unset($r->name); },
+ 'setValue' => function ($r) { (new ReflectionProperty($r, 'name'))->setValue($r, 'Z'); },
+ 'setRawValue' => function ($r) { (new ReflectionProperty($r, 'name'))->setRawValue($r, 'Z'); },
+];
+
+foreach ($tests as $label => $test) {
+ $r = new ReflectionProperty('B', 'x');
+ try {
+ $test($r);
+ echo "$label: no error\n";
+ } catch (Throwable $e) {
+ echo "$label: ", $e::class, ": ", $e->getMessage(), "\n";
+ }
+ if (!isset($r->name, $r->class) || $r->name !== $r->getName() || $r->class !== $r->getDeclaringClass()->name) {
+ echo "$label: properties diverged from getters\n";
+ }
+}
+
+class WriteBeforeConstruct extends ReflectionClass {
+ public function __construct() {
+ $this->name = 'Z';
+ }
+}
+
+class UnsetAfterConstruct extends ReflectionClass {
+ public function __construct() {
+ parent::__construct('B');
+ unset($this->name);
+ }
+}
+
+foreach (['WriteBeforeConstruct', 'UnsetAfterConstruct'] as $class) {
+ try {
+ new $class();
+ echo "$class: no error\n";
+ } catch (Throwable $e) {
+ echo "$class: ", $e::class, ": ", $e->getMessage(), "\n";
+ }
+}
+
+// Unsetting the uninitialized property in a subclass to route reads through __get() stays possible
+class UnsetBeforeConstruct extends ReflectionClass {
+ public function __construct() {
+ unset($this->name);
+ }
+
+ public function __get($name) {
+ return "magic $name";
+ }
+}
+
+var_dump((new UnsetBeforeConstruct())->name);
+
+foreach (['setValue', 'setRawValue'] as $method) {
+ $r = (new ReflectionClass('ReflectionClass'))->newInstanceWithoutConstructor();
+ try {
+ (new ReflectionProperty($r, 'name'))->$method($r, 'Z');
+ echo "uninitialized $method: no error\n";
+ } catch (Throwable $e) {
+ echo "uninitialized $method: ", $e::class, ": ", $e->getMessage(), "\n";
+ }
+}
+
+?>
+--EXPECT--
+compound assignment: ReflectionException: Cannot set read-only property ReflectionProperty::$class
+reference: Error: Cannot indirectly modify readonly property ReflectionProperty::$name
+string offset: Error: Cannot indirectly modify readonly property ReflectionProperty::$name
+by-ref argument: Error: Cannot indirectly modify readonly property ReflectionProperty::$class
+dynamic by-ref argument: Error: Cannot indirectly modify readonly property ReflectionProperty::$class
+foreach by reference: Error: Cannot acquire reference to readonly property ReflectionProperty::$name
+unset: Error: Cannot unset readonly property ReflectionProperty::$name
+setValue: ReflectionException: Cannot set read-only property ReflectionProperty::$name
+setRawValue: ReflectionException: Cannot set read-only property ReflectionProperty::$name
+WriteBeforeConstruct: ReflectionException: Cannot set read-only property WriteBeforeConstruct::$name
+UnsetAfterConstruct: Error: Cannot unset readonly property ReflectionClass::$name
+string(10) "magic name"
+uninitialized setValue: ReflectionException: Cannot set read-only property ReflectionClass::$name
+uninitialized setRawValue: ReflectionException: Cannot set read-only property ReflectionClass::$name