Commit 2945c72a145 for php.net

commit 2945c72a14508363e0d8588c1e3feedd7e1fdde9
Author: David CARLIER <devnexen@gmail.com>
Date:   Sat Apr 18 16:47:59 2026 +0100

    build/gen_stub: support asymmetric visibility modifiers. (#21773)

    Emit ZEND_ACC_PUBLIC_SET / PROTECTED_SET / PRIVATE_SET from the
    corresponding Modifiers::*_SET flags in generated arginfo, gated
    to PHP 8.4+ where asymmetric visibility was introduced. Previously
    private(set) and friends in stubs parsed without error but produced
    no set-visibility flag.

    @readonly on DOM property stubs was documentation only and did not
    translate to any runtime flag, so reflection reported the properties
    as writable while the write_property handler threw on assignment.
    Declaring them public private(set) lets the engine reject external
    writes via the normal visibility check and lets ReflectionProperty::
    isWritable() answer honestly.

    After converting virtual properties from @readonly to private(set),
    dom_write_property still threw a readonly-modification error. Since
    the handler replaces zend_std_write_property, the engine's own
    asymmetric-visibility check is bypassed on the write path, so the
    DOM handler now raises it explicitly via
    zend_asymmetric_visibility_property_modification_error() when the
    caller lacks set access. The readonly error is kept as a fallback.

diff --git a/build/gen_stub.php b/build/gen_stub.php
index eca1ab3e8c1..e68c0b77c61 100755
--- a/build/gen_stub.php
+++ b/build/gen_stub.php
@@ -2354,7 +2354,17 @@ protected function getFlagsByPhpVersion(): VersionFlags
             $flags = "ZEND_ACC_PRIVATE";
         }

-        return new VersionFlags([$flags]);
+        $versionFlags = new VersionFlags([$flags]);
+
+        if ($this->flags & Modifiers::PUBLIC_SET) {
+            $versionFlags->addForVersionsAbove("ZEND_ACC_PUBLIC_SET", PHP_84_VERSION_ID);
+        } elseif ($this->flags & Modifiers::PROTECTED_SET) {
+            $versionFlags->addForVersionsAbove("ZEND_ACC_PROTECTED_SET", PHP_84_VERSION_ID);
+        } elseif ($this->flags & Modifiers::PRIVATE_SET) {
+            $versionFlags->addForVersionsAbove("ZEND_ACC_PRIVATE_SET", PHP_84_VERSION_ID);
+        }
+
+        return $versionFlags;
     }

     protected function getTypeCode(string $variableLikeName, string &$code): string
@@ -2450,6 +2460,17 @@ protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fie
             $fieldsynopsisElement->appendChild(new DOMText("\n$indentation"));
             $fieldsynopsisElement->appendChild($doc->createElement("modifier", "private"));
         }
+
+        if ($this->flags & Modifiers::PUBLIC_SET) {
+            $fieldsynopsisElement->appendChild(new DOMText("\n$indentation"));
+            $fieldsynopsisElement->appendChild($doc->createElement("modifier", "public(set)"));
+        } elseif ($this->flags & Modifiers::PROTECTED_SET) {
+            $fieldsynopsisElement->appendChild(new DOMText("\n$indentation"));
+            $fieldsynopsisElement->appendChild($doc->createElement("modifier", "protected(set)"));
+        } elseif ($this->flags & Modifiers::PRIVATE_SET) {
+            $fieldsynopsisElement->appendChild(new DOMText("\n$indentation"));
+            $fieldsynopsisElement->appendChild($doc->createElement("modifier", "private(set)"));
+        }
     }

 }
diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c
index 87b359b2dcb..b188dea6eb4 100644
--- a/ext/dom/php_dom.c
+++ b/ext/dom/php_dom.c
@@ -412,11 +412,6 @@ zval *dom_write_property(zend_object *object, zend_string *name, zval *value, vo
 	const dom_prop_handler *hnd = dom_get_prop_handler(obj, name, cache_slot);

 	if (hnd) {
-		if (UNEXPECTED(!hnd->write_func)) {
-			zend_readonly_property_modification_error_ex(ZSTR_VAL(object->ce->name), ZSTR_VAL(name));
-			return &EG(error_zval);
-		}
-
 		zend_property_info *prop = NULL;
 		if (cache_slot) {
 			ZEND_ASSERT(*cache_slot == obj->prop_handler);
@@ -429,6 +424,16 @@ zval *dom_write_property(zend_object *object, zend_string *name, zval *value, vo
 			}
 		}

+		if (UNEXPECTED(!hnd->write_func)) {
+			if (prop && (prop->flags & ZEND_ACC_PPP_SET_MASK) &&
+			    !zend_asymmetric_property_has_set_access(prop)) {
+				zend_asymmetric_visibility_property_modification_error(prop, "modify");
+			} else {
+				zend_readonly_property_modification_error_ex(ZSTR_VAL(object->ce->name), ZSTR_VAL(name));
+			}
+			return &EG(error_zval);
+		}
+
 		ZEND_ASSERT(prop && ZEND_TYPE_IS_SET(prop->type));
 		zval tmp;
 		ZVAL_COPY(&tmp, value);
diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php
index 71aa5f4ec0f..521e3cd99c2 100644
--- a/ext/dom/php_dom.stub.php
+++ b/ext/dom/php_dom.stub.php
@@ -237,41 +237,23 @@

     class DOMDocumentType extends DOMNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $name;
+        /** @virtual */
+        public private(set) string $name;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public DOMNamedNodeMap $entities;
+        /** @virtual */
+        public private(set) DOMNamedNodeMap $entities;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public DOMNamedNodeMap $notations;
+        /** @virtual */
+        public private(set) DOMNamedNodeMap $notations;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $publicId;
+        /** @virtual */
+        public private(set) string $publicId;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $systemId;
+        /** @virtual */
+        public private(set) string $systemId;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $internalSubset;
+        /** @virtual */
+        public private(set) ?string $internalSubset;
     }

     class DOMCdataSection extends DOMText
@@ -319,101 +301,56 @@ class DOMNode
         public const int DOCUMENT_POSITION_CONTAINED_BY = 0x10;
         public const int DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $nodeName;
+        /** @virtual */
+        public private(set) string $nodeName;

         /** @virtual */
         public ?string $nodeValue;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $nodeType;
+        /** @virtual */
+        public private(set) int $nodeType;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMNode $parentNode;
+        /** @virtual */
+        public private(set) ?DOMNode $parentNode;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $parentElement;
+        /** @virtual */
+        public private(set) ?DOMElement $parentElement;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public DOMNodeList $childNodes;
+        /** @virtual */
+        public private(set) DOMNodeList $childNodes;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMNode $firstChild;
+        /** @virtual */
+        public private(set) ?DOMNode $firstChild;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMNode $lastChild;
+        /** @virtual */
+        public private(set) ?DOMNode $lastChild;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMNode $previousSibling;
+        /** @virtual */
+        public private(set) ?DOMNode $previousSibling;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMNode $nextSibling;
+        /** @virtual */
+        public private(set) ?DOMNode $nextSibling;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMNamedNodeMap $attributes;
+        /** @virtual */
+        public private(set) ?DOMNamedNodeMap $attributes;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public bool $isConnected;
+        /** @virtual */
+        public private(set) bool $isConnected;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMDocument $ownerDocument;
+        /** @virtual */
+        public private(set) ?DOMDocument $ownerDocument;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $namespaceURI;
+        /** @virtual */
+        public private(set) ?string $namespaceURI;

         /** @virtual */
         public string $prefix;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $localName;
+        /** @virtual */
+        public private(set) ?string $localName;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $baseURI;
+        /** @virtual */
+        public private(set) ?string $baseURI;

         /** @virtual */
         public string $textContent;
@@ -484,65 +421,35 @@ public function __wakeup(): void {}

     class DOMNameSpaceNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $nodeName;
+        /** @virtual */
+        public private(set) string $nodeName;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $nodeValue;
+        /** @virtual */
+        public private(set) ?string $nodeValue;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $nodeType;
+        /** @virtual */
+        public private(set) int $nodeType;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $prefix;
+        /** @virtual */
+        public private(set) string $prefix;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $localName;
+        /** @virtual */
+        public private(set) ?string $localName;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $namespaceURI;
+        /** @virtual */
+        public private(set) ?string $namespaceURI;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public bool $isConnected;
+        /** @virtual */
+        public private(set) bool $isConnected;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMDocument $ownerDocument;
+        /** @virtual */
+        public private(set) ?DOMDocument $ownerDocument;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMNode $parentNode;
+        /** @virtual */
+        public private(set) ?DOMNode $parentNode;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $parentElement;
+        /** @virtual */
+        public private(set) ?DOMElement $parentElement;

         /** @implementation-alias DOMNode::__sleep */
         public function __sleep(): array {}
@@ -565,23 +472,14 @@ public function createDocument(?string $namespace = null, string $qualifiedName

     class DOMDocumentFragment extends DOMNode implements DOMParentNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $firstElementChild;
+        /** @virtual */
+        public private(set) ?DOMElement $firstElementChild;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $lastElementChild;
+        /** @virtual */
+        public private(set) ?DOMElement $lastElementChild;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $childElementCount;
+        /** @virtual */
+        public private(set) int $childElementCount;

         public function __construct() {}

@@ -609,11 +507,8 @@ public function replaceChildren(...$nodes): void {}

     class DOMNodeList implements IteratorAggregate, Countable
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $length;
+        /** @virtual */
+        public private(set) int $length;

         /** @tentative-return-type */
         public function count(): int {}
@@ -629,23 +524,14 @@ class DOMCharacterData extends DOMNode implements DOMChildNode
         /** @virtual */
         public string $data;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $length;
+        /** @virtual */
+        public private(set) int $length;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $previousElementSibling;
+        /** @virtual */
+        public private(set) ?DOMElement $previousElementSibling;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $nextElementSibling;
+        /** @virtual */
+        public private(set) ?DOMElement $nextElementSibling;

         /** @tentative-return-type */
         public function appendData(string $data): true {}
@@ -686,32 +572,20 @@ public function after(...$nodes): void {}

     class DOMAttr extends DOMNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $name;
+        /** @virtual */
+        public private(set) string $name;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public bool $specified;
+        /** @virtual */
+        public private(set) bool $specified;

         /** @virtual */
         public string $value;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $ownerElement;
+        /** @virtual */
+        public private(set) ?DOMElement $ownerElement;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public mixed $schemaTypeInfo;
+        /** @virtual */
+        public private(set) mixed $schemaTypeInfo;

         public function __construct(string $name, string $value = "") {}

@@ -721,11 +595,8 @@ public function isId(): bool {}

     class DOMElement extends DOMNode implements \DOMParentNode, \DOMChildNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $tagName;
+        /** @virtual */
+        public private(set) string $tagName;

         /** @virtual */
         public string $className;
@@ -733,41 +604,23 @@ class DOMElement extends DOMNode implements \DOMParentNode, \DOMChildNode
         /** @virtual */
         public string $id;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public mixed $schemaTypeInfo;
+        /** @virtual */
+        public private(set) mixed $schemaTypeInfo;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $firstElementChild;
+        /** @virtual */
+        public private(set) ?DOMElement $firstElementChild;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $lastElementChild;
+        /** @virtual */
+        public private(set) ?DOMElement $lastElementChild;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $childElementCount;
+        /** @virtual */
+        public private(set) int $childElementCount;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $previousElementSibling;
+        /** @virtual */
+        public private(set) ?DOMElement $previousElementSibling;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $nextElementSibling;
+        /** @virtual */
+        public private(set) ?DOMElement $nextElementSibling;

         public function __construct(string $qualifiedName, ?string $value = null, string $namespace = "") {}

@@ -856,39 +709,26 @@ public function insertAdjacentText(string $where, string $data): void {}

     class DOMDocument extends DOMNode implements DOMParentNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMDocumentType $doctype;
+        /** @virtual */
+        public private(set) ?DOMDocumentType $doctype;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public DOMImplementation $implementation;
+        /** @virtual */
+        public private(set) DOMImplementation $implementation;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $documentElement;
+        /** @virtual */
+        public private(set) ?DOMElement $documentElement;

         /**
-         * @readonly
          * @deprecated
          * @virtual
          */
-        public ?string $actualEncoding;
+        public private(set) ?string $actualEncoding;

         /** @virtual */
         public ?string $encoding;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $xmlEncoding;
+        /** @virtual */
+        public private(set) ?string $xmlEncoding;

         /** @virtual */
         public bool $standalone;
@@ -909,11 +749,10 @@ class DOMDocument extends DOMNode implements DOMParentNode
         public ?string $documentURI;

         /**
-         * @readonly
          * @deprecated
          * @virtual
          */
-        public mixed $config;
+        public private(set) mixed $config;

         /** @virtual */
         public bool $formatOutput;
@@ -933,23 +772,14 @@ class DOMDocument extends DOMNode implements DOMParentNode
         /** @virtual */
         public bool $substituteEntities;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $firstElementChild;
+        /** @virtual */
+        public private(set) ?DOMElement $firstElementChild;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DOMElement $lastElementChild;
+        /** @virtual */
+        public private(set) ?DOMElement $lastElementChild;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $childElementCount;
+        /** @virtual */
+        public private(set) int $childElementCount;

         public function __construct(string $version = "1.0", string $encoding = "") {}

@@ -1082,11 +912,8 @@ final class DOMException extends Exception

     class DOMText extends DOMCharacterData
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $wholeText;
+        /** @virtual */
+        public private(set) string $wholeText;

         public function __construct(string $data = "") {}

@@ -1105,11 +932,8 @@ public function splitText(int $offset) {}

     class DOMNamedNodeMap implements IteratorAggregate, Countable
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $length;
+        /** @virtual */
+        public private(set) int $length;

         /** @tentative-return-type */
         public function getNamedItem(string $qualifiedName): ?DOMNode {}
@@ -1128,44 +952,32 @@ public function getIterator(): Iterator {}

     class DOMEntity extends DOMNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $publicId;
+        /** @virtual */
+        public private(set) ?string $publicId;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $systemId;
+        /** @virtual */
+        public private(set) ?string $systemId;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $notationName;
+        /** @virtual */
+        public private(set) ?string $notationName;

         /**
-         * @readonly
          * @deprecated
          * @virtual
          */
-        public ?string $actualEncoding;
+        public private(set) ?string $actualEncoding;

         /**
-         * @readonly
          * @deprecated
          * @virtual
          */
-        public ?string $encoding;
+        public private(set) ?string $encoding;

         /**
-         * @readonly
          * @deprecated
          * @virtual
          */
-        public ?string $version;
+        public private(set) ?string $version;
     }

     class DOMEntityReference extends DOMNode
@@ -1175,26 +987,17 @@ public function __construct(string $name) {}

     class DOMNotation extends DOMNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $publicId;
+        /** @virtual */
+        public private(set) string $publicId;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $systemId;
+        /** @virtual */
+        public private(set) string $systemId;
     }

     class DOMProcessingInstruction extends DOMNode
-    {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $target;
+    {
+        /** @virtual */
+        public private(set) string $target;

         /** @virtual */
         public string $data;
@@ -1206,11 +1009,8 @@ public function __construct(string $name, string $value = "") {}
     /** @not-serializable */
     class DOMXPath
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public DOMDocument $document;
+        /** @virtual */
+        public private(set) DOMDocument $document;

         /** @virtual */
         public bool $registerNodeNamespaces;
@@ -1358,73 +1158,37 @@ class Node
     {
         private final function __construct() {}

-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $nodeType;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $nodeName;
+        /** @virtual */
+        public private(set) int $nodeType;
+        /** @virtual */
+        public private(set) string $nodeName;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $baseURI;
+        /** @virtual */
+        public private(set) string $baseURI;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public bool $isConnected;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Document $ownerDocument;
+        /** @virtual */
+        public private(set) bool $isConnected;
+        /** @virtual */
+        public private(set) ?Document $ownerDocument;

         /** @implementation-alias DOMNode::getRootNode */
         public function getRootNode(array $options = []): Node {}
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Node $parentNode;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $parentElement;
+        /** @virtual */
+        public private(set) ?Node $parentNode;
+        /** @virtual */
+        public private(set) ?Element $parentElement;
         /** @implementation-alias DOMNode::hasChildNodes */
         public function hasChildNodes(): bool {}
-        /**
-         * @readonly
-         * @virtual
-         */
-        public NodeList $childNodes;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Node $firstChild;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Node $lastChild;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Node $previousSibling;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Node $nextSibling;
+        /** @virtual */
+        public private(set) NodeList $childNodes;
+        /** @virtual */
+        public private(set) ?Node $firstChild;
+        /** @virtual */
+        public private(set) ?Node $lastChild;
+        /** @virtual */
+        public private(set) ?Node $previousSibling;
+        /** @virtual */
+        public private(set) ?Node $nextSibling;

         /** @virtual */
         public ?string $nodeValue;
@@ -1474,11 +1238,8 @@ public function __wakeup(): void {}

     class NodeList implements \IteratorAggregate, \Countable
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $length;
+        /** @virtual */
+        public private(set) int $length;

         /** @implementation-alias DOMNodeList::count */
         public function count(): int {}
@@ -1492,11 +1253,8 @@ public function item(int $index): ?Node {}

     class NamedNodeMap implements \IteratorAggregate, \Countable
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $length;
+        /** @virtual */
+        public private(set) int $length;

         /** @implementation-alias DOMNamedNodeMap::item */
         public function item(int $index): ?Attr {}
@@ -1514,11 +1272,8 @@ public function getIterator(): \Iterator {}

     class DtdNamedNodeMap implements \IteratorAggregate, \Countable
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $length;
+        /** @virtual */
+        public private(set) int $length;

         /** @implementation-alias DOMNamedNodeMap::item */
         public function item(int $index): Entity|Notation|null {}
@@ -1536,11 +1291,8 @@ public function getIterator(): \Iterator {}

     class HTMLCollection implements \IteratorAggregate, \Countable
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $length;
+        /** @virtual */
+        public private(set) int $length;

         /** @implementation-alias DOMNodeList::item */
         public function item(int $index): ?Element {}
@@ -1564,71 +1316,37 @@ enum AdjacentPosition : string

     class Element extends Node implements ParentNode, ChildNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $namespaceURI;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $prefix;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $localName;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $tagName;
+        /** @virtual */
+        public private(set) ?string $namespaceURI;
+        /** @virtual */
+        public private(set) ?string $prefix;
+        /** @virtual */
+        public private(set) string $localName;
+        /** @virtual */
+        public private(set) string $tagName;

-        /**
-         * @readonly
-         */
-        public HTMLCollection $children;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $firstElementChild;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $lastElementChild;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $childElementCount;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $previousElementSibling;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $nextElementSibling;
+        public private(set) HTMLCollection $children;
+        /** @virtual */
+        public private(set) ?Element $firstElementChild;
+        /** @virtual */
+        public private(set) ?Element $lastElementChild;
+        /** @virtual */
+        public private(set) int $childElementCount;
+        /** @virtual */
+        public private(set) ?Element $previousElementSibling;
+        /** @virtual */
+        public private(set) ?Element $nextElementSibling;

         /** @virtual */
         public string $id;
         /** @virtual */
         public string $className;
-        /** @readonly */
-        public TokenList $classList;
+        public private(set) TokenList $classList;

         /** @implementation-alias DOMNode::hasAttributes */
         public function hasAttributes(): bool {}
-        /**
-         * @readonly
-         * @virtual
-         */
-        public NamedNodeMap $attributes;
+        /** @virtual */
+        public private(set) NamedNodeMap $attributes;
         /** @implementation-alias DOMElement::getAttributeNames */
         public function getAttributeNames(): array {}
         /** @implementation-alias DOMElement::getAttribute */
@@ -1716,40 +1434,22 @@ class HTMLElement extends Element

     class Attr extends Node
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $namespaceURI;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $prefix;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $localName;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $name;
+        /** @virtual */
+        public private(set) ?string $namespaceURI;
+        /** @virtual */
+        public private(set) ?string $prefix;
+        /** @virtual */
+        public private(set) string $localName;
+        /** @virtual */
+        public private(set) string $name;
         /** @virtual */
         public string $value;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $ownerElement;
+        /** @virtual */
+        public private(set) ?Element $ownerElement;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public bool $specified;
+        /** @virtual */
+        public private(set) bool $specified;

         /** @implementation-alias DOMAttr::isId */
         public function isId(): bool {}
@@ -1760,24 +1460,15 @@ public function rename(?string $namespaceURI, string $qualifiedName): void {}

     class CharacterData extends Node implements ChildNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $previousElementSibling;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $nextElementSibling;
+        /** @virtual */
+        public private(set) ?Element $previousElementSibling;
+        /** @virtual */
+        public private(set) ?Element $nextElementSibling;

         /** @virtual */
         public string $data;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $length;
+        /** @virtual */
+        public private(set) int $length;
         /** @implementation-alias DOMCharacterData::substringData */
         public function substringData(int $offset, int $count): string {}
         public function appendData(string $data): void {}
@@ -1801,22 +1492,16 @@ class Text extends CharacterData

         /** @implementation-alias DOMText::splitText */
         public function splitText(int $offset): Text {}
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $wholeText;
+        /** @virtual */
+        public private(set) string $wholeText;
     }

     class CDATASection extends Text {}

     class ProcessingInstruction extends CharacterData
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $target;
+        /** @virtual */
+        public private(set) string $target;
     }

     class Comment extends CharacterData
@@ -1826,36 +1511,18 @@ class Comment extends CharacterData

     class DocumentType extends Node implements ChildNode
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $name;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public DtdNamedNodeMap $entities;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public DtdNamedNodeMap $notations;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $publicId;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $systemId;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $internalSubset;
+        /** @virtual */
+        public private(set) string $name;
+        /** @virtual */
+        public private(set) DtdNamedNodeMap $entities;
+        /** @virtual */
+        public private(set) DtdNamedNodeMap $notations;
+        /** @virtual */
+        public private(set) string $publicId;
+        /** @virtual */
+        public private(set) string $systemId;
+        /** @virtual */
+        public private(set) ?string $internalSubset;

         /** @implementation-alias DOMElement::remove */
         public function remove(): void {}
@@ -1869,25 +1536,13 @@ public function replaceWith(Node|string ...$nodes): void {}

     class DocumentFragment extends Node implements ParentNode
     {
-        /**
-         * @readonly
-         */
-        public HTMLCollection $children;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $firstElementChild;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $lastElementChild;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $childElementCount;
+        public private(set) HTMLCollection $children;
+        /** @virtual */
+        public private(set) ?Element $firstElementChild;
+        /** @virtual */
+        public private(set) ?Element $lastElementChild;
+        /** @virtual */
+        public private(set) int $childElementCount;

         /** @implementation-alias DOMDocumentFragment::appendXML */
         public function appendXml(string $data): bool {}
@@ -1906,63 +1561,35 @@ public function querySelectorAll(string $selectors): NodeList {}

     class Entity extends Node
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $publicId;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $systemId;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?string $notationName;
+        /** @virtual */
+        public private(set) ?string $publicId;
+        /** @virtual */
+        public private(set) ?string $systemId;
+        /** @virtual */
+        public private(set) ?string $notationName;
     }

     class EntityReference extends Node {}

     class Notation extends Node
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $publicId;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $systemId;
+        /** @virtual */
+        public private(set) string $publicId;
+        /** @virtual */
+        public private(set) string $systemId;
     }

     abstract class Document extends Node implements ParentNode
     {
-        /**
-         * @readonly
-         */
-        public HTMLCollection $children;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $firstElementChild;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $lastElementChild;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $childElementCount;
+        public private(set) HTMLCollection $children;
+        /** @virtual */
+        public private(set) ?Element $firstElementChild;
+        /** @virtual */
+        public private(set) ?Element $lastElementChild;
+        /** @virtual */
+        public private(set) int $childElementCount;

-        /** @readonly */
-        public Implementation $implementation;
+        public private(set) Implementation $implementation;
         /** @virtual */
         public string $URL;
         /** @virtual */
@@ -1974,16 +1601,10 @@ abstract class Document extends Node implements ParentNode
         /** @virtual */
         public string $inputEncoding;

-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?DocumentType $doctype;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?Element $documentElement;
+        /** @virtual */
+        public private(set) ?DocumentType $doctype;
+        /** @virtual */
+        public private(set) ?Element $documentElement;
         /** @implementation-alias Dom\Element::getElementsByTagName */
         public function getElementsByTagName(string $qualifiedName): HTMLCollection {}
         /** @implementation-alias Dom\Element::getElementsByTagNameNS */
@@ -2043,11 +1664,8 @@ public function querySelectorAll(string $selectors): NodeList {}

         /** @virtual */
         public ?HTMLElement $body;
-        /**
-         * @readonly
-         * @virtual
-         */
-        public ?HTMLElement $head;
+        /** @virtual */
+        public private(set) ?HTMLElement $head;
         /** @virtual */
         public string $title;
     }
@@ -2083,11 +1701,8 @@ public static function createFromFile(string $path, int $options = 0, ?string $o

         public static function createFromString(string $source, int $options = 0, ?string $overrideEncoding = null): XMLDocument {}

-        /**
-         * @readonly
-         * @virtual
-         */
-        public string $xmlEncoding;
+        /** @virtual */
+        public private(set) string $xmlEncoding;

         /** @virtual */
         public bool $xmlStandalone;
@@ -2121,11 +1736,8 @@ final class TokenList implements \IteratorAggregate, \Countable
         /** @implementation-alias Dom\Node::__construct */
         private function __construct() {}

-        /**
-         * @readonly
-         * @virtual
-         */
-        public int $length;
+        /** @virtual */
+        public private(set) int $length;
         public function item(int $index): ?string {}
         public function contains(string $token): bool {}
         public function add(string ...$tokens): void {}
@@ -2159,11 +1771,8 @@ private function __construct() {}
     /** @not-serializable */
     final class XPath
     {
-        /**
-         * @readonly
-         * @virtual
-         */
-        public Document $document;
+        /** @virtual */
+        public private(set) Document $document;

         /** @virtual */
         public bool $registerNodeNamespaces;
diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h
index 1c90f920cdd..0274186380d 100644
Binary files a/ext/dom/php_dom_arginfo.h and b/ext/dom/php_dom_arginfo.h differ
diff --git a/ext/dom/php_dom_decl.h b/ext/dom/php_dom_decl.h
index f918637ab52..e42fe596959 100644
Binary files a/ext/dom/php_dom_decl.h and b/ext/dom/php_dom_decl.h differ
diff --git a/ext/dom/tests/modern/spec/Element_prefix_readonly.phpt b/ext/dom/tests/modern/spec/Element_prefix_readonly.phpt
index 78625fcb6f7..cb623f70298 100644
--- a/ext/dom/tests/modern/spec/Element_prefix_readonly.phpt
+++ b/ext/dom/tests/modern/spec/Element_prefix_readonly.phpt
@@ -14,5 +14,5 @@
 echo $dom->saveXml();
 ?>
 --EXPECT--
-Cannot modify readonly property Dom\HTMLElement::$prefix
+Cannot modify private(set) property Dom\Element::$prefix from global scope
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
diff --git a/ext/dom/tests/property_write_errors.phpt b/ext/dom/tests/property_write_errors.phpt
index 58cf800728a..7d2af3b1055 100644
--- a/ext/dom/tests/property_write_errors.phpt
+++ b/ext/dom/tests/property_write_errors.phpt
@@ -49,8 +49,8 @@
 ?>
 --EXPECT--
 Cannot assign array to property DOMNode::$nodeValue of type ?string
-Cannot modify readonly property DOMDocument::$nodeType
-Cannot modify readonly property DOMDocument::$xmlEncoding
-Cannot modify readonly property DOMEntity::$actualEncoding
-Cannot modify readonly property DOMEntity::$encoding
-Cannot modify readonly property DOMEntity::$version
+Cannot modify private(set) property DOMNode::$nodeType from global scope
+Cannot modify private(set) property DOMDocument::$xmlEncoding from global scope
+Cannot modify private(set) property DOMEntity::$actualEncoding from global scope
+Cannot modify private(set) property DOMEntity::$encoding from global scope
+Cannot modify private(set) property DOMEntity::$version from global scope