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