From e826bf80363f307f7e461f06edc335000cfec3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= Date: Sun, 5 Apr 2026 07:31:58 +0200 Subject: [PATCH 1/3] Add support for generating enum pages for the manual (#21469) --- build/gen_stub.php | 351 +++++++++++++++++++++++++++++++-------------- 1 file changed, 243 insertions(+), 108 deletions(-) diff --git a/build/gen_stub.php b/build/gen_stub.php index 396541272c32..67be5c2f7b94 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -945,6 +945,22 @@ public function getDeclarationName(): string } } +class EnumCaseName { + public /* readonly */ Name $enum; + public /* readonly */ string $case; + + public function __construct(Name $enum, string $case) + { + $this->enum = $enum; + $this->case = $case; + } + + public function __toString(): string + { + return "$this->enum::$this->case"; + } +} + interface FunctionOrMethodName { public function getDeclaration(): string; public function getArgInfoName(): string; @@ -3286,17 +3302,19 @@ protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fie } class EnumCaseInfo { - private /* readonly */ string $name; + private /* readonly */ EnumCaseName $name; private /* readonly */ ?Expr $value; + private /* readonly */ ?string $valueString; - public function __construct(string $name, ?Expr $value) { + public function __construct(EnumCaseName $name, ?Expr $value, ?string $valueString) { $this->name = $name; $this->value = $value; + $this->valueString = $valueString; } /** @param array $allConstInfos */ public function getDeclaration(array $allConstInfos): string { - $escapedName = addslashes($this->name); + $escapedName = addslashes($this->name->case); if ($this->value === null) { $code = "\n\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", NULL);\n"; } else { @@ -3309,6 +3327,55 @@ public function getDeclaration(array $allConstInfos): string { return $code; } + + /** @param array $allConstInfos */ + public function getEnumSynopsisItemElement(DOMDocument $doc, array $allConstInfos, int $indentationLevel): DOMElement + { + $indentation = str_repeat(" ", $indentationLevel); + + $itemElement = $doc->createElement("enumitem"); + + $identifierElement = $doc->createElement("enumidentifier", $this->name->case); + + $itemElement->appendChild(new DOMText("\n$indentation ")); + $itemElement->appendChild($identifierElement); + + $valueString = $this->getEnumSynopsisValueString($allConstInfos); + if ($valueString) { + $itemElement->appendChild(new DOMText("\n$indentation ")); + $valueElement = $doc->createElement("enumvalue", $valueString); + $itemElement->appendChild($valueElement); + } + + $descriptionElement = $doc->createElement("enumitemdescription", "Description."); + $itemElement->appendChild(new DOMText("\n$indentation ")); + $itemElement->appendChild($descriptionElement); + + $itemElement->appendChild(new DOMText("\n$indentation")); + + return $itemElement; + } + + /** @param array $allConstInfos */ + public function getEnumSynopsisValueString(array $allConstInfos): ?string + { + if ($this->value === null) { + return null; + } + + $value = EvaluatedValue::createFromExpression($this->value, null, null, $allConstInfos); + if ($value->isUnknownConstValue) { + return null; + } + + if ($value->originatingConsts) { + return implode("\n", array_map(function (ConstInfo $const) use ($allConstInfos) { + return $const->getFieldSynopsisValueString($allConstInfos); + }, $value->originatingConsts)); + } + + return $this->valueString; + } } // Instances of AttributeInfo are immutable and do not need to be cloned @@ -3748,8 +3815,12 @@ public function getClassSynopsisDocument(array $classMap, array $allConstInfos): * @param array $allConstInfos */ public function getClassSynopsisElement(DOMDocument $doc, array $classMap, array $allConstInfos): ?DOMElement { - $classSynopsis = $doc->createElement("classsynopsis"); - $classSynopsis->setAttribute("class", $this->type === "interface" ? "interface" : "class"); + if ($this->type === "enum") { + $classSynopsis = $doc->createElement("enumsynopsis"); + } else { + $classSynopsis = $doc->createElement("classsynopsis"); + $classSynopsis->setAttribute("class", $this->type === "interface" ? "interface" : "class"); + } $namespace = $this->getNamespace(); if ($namespace) { @@ -3769,108 +3840,120 @@ public function getClassSynopsisElement(DOMDocument $doc, array $classMap, array $classSynopsisIndentation = str_repeat(" ", $classSynopsisIndentationLevel); } - $exceptionOverride = $this->type === "class" && $this->isException($classMap) ? "exception" : null; - $ooElement = self::createOoElement($doc, $this, $exceptionOverride, true, null, $classSynopsisIndentationLevel + 1); - if (!$ooElement) { - return null; - } $classSynopsis->appendChild(new DOMText("\n$classSynopsisIndentation ")); - $classSynopsis->appendChild($ooElement); - foreach ($this->extends as $k => $parent) { - $parentInfo = $classMap[$parent->toString()] ?? null; - if ($parentInfo === null) { - throw new Exception("Missing parent class " . $parent->toString()); - } + if ($this->type === "enum") { + $enumName = $doc->createElement("enumname", $this->getClassName()); + $classSynopsis->appendChild($enumName); - $ooElement = self::createOoElement( - $doc, - $parentInfo, - null, - false, - $k === 0 ? "extends" : null, - $classSynopsisIndentationLevel + 1 - ); + foreach ($this->enumCaseInfos as $enumCaseInfo) { + $classSynopsis->appendChild(new DOMText("\n\n$classSynopsisIndentation ")); + $enumItemElement = $enumCaseInfo->getEnumSynopsisItemElement($doc, $allConstInfos, $classSynopsisIndentationLevel + 1); + $classSynopsis->appendChild($enumItemElement); + } + } else { + $exceptionOverride = $this->type === "class" && $this->isException($classMap) ? "exception" : null; + $ooElement = self::createOoElement($doc, $this, $exceptionOverride, true, null, $classSynopsisIndentationLevel + 1); if (!$ooElement) { return null; } - - $classSynopsis->appendChild(new DOMText("\n\n$classSynopsisIndentation ")); $classSynopsis->appendChild($ooElement); - } - foreach ($this->implements as $k => $interface) { - $interfaceInfo = $classMap[$interface->toString()] ?? null; - if (!$interfaceInfo) { - throw new Exception("Missing implemented interface " . $interface->toString()); + foreach ($this->extends as $k => $parent) { + $parentInfo = $classMap[$parent->toString()] ?? null; + if ($parentInfo === null) { + throw new Exception("Missing parent class " . $parent->toString()); + } + + $ooElement = self::createOoElement( + $doc, + $parentInfo, + null, + false, + $k === 0 ? "extends" : null, + $classSynopsisIndentationLevel + 1 + ); + if (!$ooElement) { + return null; + } + + $classSynopsis->appendChild(new DOMText("\n\n$classSynopsisIndentation ")); + $classSynopsis->appendChild($ooElement); } - $ooElement = self::createOoElement($doc, $interfaceInfo, null, false, $k === 0 ? "implements" : null, $classSynopsisIndentationLevel + 1); - if (!$ooElement) { - return null; + foreach ($this->implements as $k => $interface) { + $interfaceInfo = $classMap[$interface->toString()] ?? null; + if (!$interfaceInfo) { + throw new Exception("Missing implemented interface " . $interface->toString()); + } + + $ooElement = self::createOoElement($doc, $interfaceInfo, null, false, $k === 0 ? "implements" : null, $classSynopsisIndentationLevel + 1); + if (!$ooElement) { + return null; + } + $classSynopsis->appendChild(new DOMText("\n\n$classSynopsisIndentation ")); + $classSynopsis->appendChild($ooElement); } - $classSynopsis->appendChild(new DOMText("\n\n$classSynopsisIndentation ")); - $classSynopsis->appendChild($ooElement); - } - /** @var array $parentsWithInheritedConstants */ - $parentsWithInheritedConstants = []; - /** @var array $parentsWithInheritedProperties */ - $parentsWithInheritedProperties = []; - /** @var array $parentsWithInheritedMethods */ - $parentsWithInheritedMethods = []; + /** @var array $parentsWithInheritedConstants */ + $parentsWithInheritedConstants = []; + /** @var array $parentsWithInheritedProperties */ + $parentsWithInheritedProperties = []; + /** @var array $parentsWithInheritedMethods */ + $parentsWithInheritedMethods = []; - $this->collectInheritedMembers( - $parentsWithInheritedConstants, - $parentsWithInheritedProperties, - $parentsWithInheritedMethods, - $this->hasConstructor(), - $classMap - ); + $this->collectInheritedMembers( + $parentsWithInheritedConstants, + $parentsWithInheritedProperties, + $parentsWithInheritedMethods, + $this->hasConstructor(), + $classMap + ); - $this->appendInheritedMemberSectionToClassSynopsis( - $doc, - $classSynopsis, - $parentsWithInheritedConstants, - "&Constants;", - "&InheritedConstants;", - $classSynopsisIndentationLevel + 1 - ); + $this->appendInheritedMemberSectionToClassSynopsis( + $doc, + $classSynopsis, + $parentsWithInheritedConstants, + "&Constants;", + "&InheritedConstants;", + $classSynopsisIndentationLevel + 1 + ); - if (!empty($this->constInfos)) { - $classSynopsis->appendChild(new DOMText("\n\n$classSynopsisIndentation ")); - $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Constants;"); - $classSynopsisInfo->setAttribute("role", "comment"); - $classSynopsis->appendChild($classSynopsisInfo); + if (!empty($this->constInfos)) { + $classSynopsis->appendChild(new DOMText("\n\n$classSynopsisIndentation ")); + $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Constants;"); + $classSynopsisInfo->setAttribute("role", "comment"); + $classSynopsis->appendChild($classSynopsisInfo); - foreach ($this->constInfos as $constInfo) { - $classSynopsis->appendChild(new DOMText("\n$classSynopsisIndentation ")); - $fieldSynopsisElement = $constInfo->getFieldSynopsisElement($doc, $allConstInfos, $classSynopsisIndentationLevel + 1); - $classSynopsis->appendChild($fieldSynopsisElement); + foreach ($this->constInfos as $constInfo) { + $classSynopsis->appendChild(new DOMText("\n$classSynopsisIndentation ")); + $fieldSynopsisElement = $constInfo->getFieldSynopsisElement($doc, $allConstInfos, $classSynopsisIndentationLevel + 1); + $classSynopsis->appendChild($fieldSynopsisElement); + } } - } - if (!empty($this->propertyInfos)) { - $classSynopsis->appendChild(new DOMText("\n\n$classSynopsisIndentation ")); - $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Properties;"); - $classSynopsisInfo->setAttribute("role", "comment"); - $classSynopsis->appendChild($classSynopsisInfo); + if (!empty($this->propertyInfos)) { + $classSynopsis->appendChild(new DOMText("\n\n$classSynopsisIndentation ")); + $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Properties;"); + $classSynopsisInfo->setAttribute("role", "comment"); + $classSynopsis->appendChild($classSynopsisInfo); - foreach ($this->propertyInfos as $propertyInfo) { - $classSynopsis->appendChild(new DOMText("\n$classSynopsisIndentation ")); - $fieldSynopsisElement = $propertyInfo->getFieldSynopsisElement($doc, $allConstInfos, $classSynopsisIndentationLevel + 1); - $classSynopsis->appendChild($fieldSynopsisElement); + foreach ($this->propertyInfos as $propertyInfo) { + $classSynopsis->appendChild(new DOMText("\n$classSynopsisIndentation ")); + $fieldSynopsisElement = $propertyInfo->getFieldSynopsisElement($doc, $allConstInfos, $classSynopsisIndentationLevel + 1); + $classSynopsis->appendChild($fieldSynopsisElement); + } } - } - $this->appendInheritedMemberSectionToClassSynopsis( - $doc, - $classSynopsis, - $parentsWithInheritedProperties, - "&Properties;", - "&InheritedProperties;", - $classSynopsisIndentationLevel + 1 - ); + $this->appendInheritedMemberSectionToClassSynopsis( + $doc, + $classSynopsis, + $parentsWithInheritedProperties, + "&Properties;", + "&InheritedProperties;", + $classSynopsisIndentationLevel + 1 + ); + } if (!empty($this->funcInfos)) { $classSynopsis->appendChild(new DOMText("\n\n$classSynopsisIndentation ")); @@ -3954,7 +4037,7 @@ private static function createOoElement( $indentation = str_repeat(" ", $indentationLevel); if ($classInfo->type !== "class" && $classInfo->type !== "interface") { - echo "Class synopsis generation is not implemented for " . $classInfo->type . "\n"; + echo "Warning: Class synopsis generation is not implemented for " . $classInfo->type . " of type " . $classInfo->name . "\n"; return null; } @@ -4491,7 +4574,10 @@ private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPri ); } else if ($classStmt instanceof Stmt\EnumCase) { $enumCaseInfos[] = new EnumCaseInfo( - $classStmt->name->toString(), $classStmt->expr); + new EnumCaseName($className, $classStmt->name->toString()), + $classStmt->expr, + $classStmt->expr ? $prettyPrinter->prettyPrintExpr($classStmt->expr) : null, + ); } else { throw new Exception("Not implemented {$classStmt->getType()}"); } @@ -5741,7 +5827,7 @@ function replaceClassSynopses( continue; } - if (stripos($xml, "getElementsByTagName("classsynopsis") as $element) { - $classSynopsisElements[] = $element; + $synopsisElements[] = $element; } - foreach ($classSynopsisElements as $classSynopsis) { - if (!$classSynopsis instanceof DOMElement) { - continue; - } + foreach ($doc->getElementsByTagName("enumsynopsis") as $element) { + $synopsisElements[] = $element; + } - $child = $classSynopsis->firstElementChild; - if ($child === null) { + foreach ($synopsisElements as $synopsis) { + if (!$synopsis instanceof DOMElement) { continue; } - $child = $child->lastElementChild; - if ($child === null) { - continue; + + if ($synopsis->nodeName === "classsynopsis") { + $child = $synopsis->firstElementChild; + if ($child === null) { + continue; + } + $child = $child->lastElementChild; + if ($child === null) { + continue; + } + } elseif ($synopsis->nodeName === "enumsynopsis") { + $child = $synopsis->firstElementChild; + if ($child === null) { + continue; + } } + $className = $child->textContent; - if ($classSynopsis->parentElement->nodeName === "packagesynopsis" && - $classSynopsis->parentElement->firstElementChild->nodeName === "package" + if ($synopsis->parentElement->nodeName === "packagesynopsis" && + $synopsis->parentElement->firstElementChild->nodeName === "package" ) { - $package = $classSynopsis->parentElement->firstElementChild; + $package = $synopsis->parentElement->firstElementChild; $namespace = $package->textContent; $className = $namespace . "\\" . $className; - $elementToReplace = $classSynopsis->parentElement; + $elementToReplace = $synopsis->parentElement; } else { - $elementToReplace = $classSynopsis; + $elementToReplace = $synopsis; } if (!isset($classMap[$className])) { @@ -5797,17 +5895,32 @@ function replaceClassSynopses( $classInfo = $classMap[$className]; - $newClassSynopsis = $classInfo->getClassSynopsisElement($doc, $classMap, $allConstInfos); - if ($newClassSynopsis === null) { + $newSynopsis = $classInfo->getClassSynopsisElement($doc, $classMap, $allConstInfos); + if ($newSynopsis === null) { continue; } // Check if there is any change - short circuit if there is not any. - if (replaceAndCompareXmls($doc, $elementToReplace, $newClassSynopsis)) { + if (replaceAndCompareXmls($doc, $elementToReplace, $newSynopsis)) { continue; } + if ($synopsis->nodeName === "enumsynopsis") { + $oldEnumCaseDescriptionElements = collectEnumSynopsisItemDescriptions($className, $elementToReplace); + $newEnumCaseDescriptionElements = collectEnumSynopsisItemDescriptions($className, $newSynopsis); + + foreach ($newEnumCaseDescriptionElements as $key => $newEnumCaseDescriptionElement) { + if (isset($oldEnumCaseDescriptionElements[$key]) === false) { + continue; + } + + $oldEnumCaseDescriptionElement = $oldEnumCaseDescriptionElements[$key]; + + $newEnumCaseDescriptionElement->parentElement->replaceChild($oldEnumCaseDescriptionElement, $newEnumCaseDescriptionElement); + } + } + // Return the updated XML $replacedXml = $doc->saveXML(); @@ -5843,6 +5956,28 @@ function replaceClassSynopses( return $classSynopses; } +/** + * @return array + */ +function collectEnumSynopsisItemDescriptions(string $className, DOMElement $synopsis): array +{ + $enumCaseDescriptionElements = []; + + $enumCaseDescriptions = $synopsis->getElementsByTagName("enumitemdescription"); + foreach ($enumCaseDescriptions as $enumItemDescription) { + $enumCaseNames = $enumItemDescription->parentElement->getElementsByTagName("enumidentifier"); + if (empty($enumCaseNames)) { + continue; + } + + $enumCaseName = $enumCaseNames[0]->textContent; + + $enumCaseDescriptionElements["$className::$enumCaseName"] = $enumItemDescription; + } + + return $enumCaseDescriptionElements; +} + function getReplacedSynopsisXml(string $xml): string { return preg_replace( From 27ebf47ff1ba5a5145a57176a5655e0ce11bfdb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= Date: Sun, 5 Apr 2026 07:40:29 +0200 Subject: [PATCH 2/3] Use readonly properties + CPP for EnumCaseName in gen_stub.php --- build/gen_stub.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/build/gen_stub.php b/build/gen_stub.php index 70265c40cb97..9c1f797485c1 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -902,13 +902,10 @@ public function getDeclarationName(): string } class EnumCaseName { - public /* readonly */ Name $enum; - public /* readonly */ string $case; - - public function __construct(Name $enum, string $case) - { - $this->enum = $enum; - $this->case = $case; + public function __construct( + public readonly Name $enum, + public readonly string $case + ) { } public function __toString(): string From 1a9a2c7052e3e22b21c43b31dda9355b6d975f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= Date: Sun, 5 Apr 2026 10:10:15 +0200 Subject: [PATCH 3/3] Fix enum case registration An EnumCaseName::__toString() was added to the PHP 8.5 branch returning the FQCN of the enum case, which worked fine, but in the meanwhile, support for enum registration was added to master which implicitly relied on the EnumCaseName::__toString() method to return only the name of the case. That's why the "Verify the generated files are up to date" step was failing until this fix. --- build/gen_stub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/gen_stub.php b/build/gen_stub.php index 9c1f797485c1..ce1a23866610 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -3529,7 +3529,7 @@ public function getCDeclarations(): string $i = 1; foreach ($this->enumCaseInfos as $case) { - $cName = 'ZEND_ENUM_' . str_replace('\\', '_', $this->name->toString()) . '_' . $case->name; + $cName = 'ZEND_ENUM_' . str_replace('\\', '_', $this->name->toString()) . '_' . $case->name->case; $code .= "\t{$cName} = {$i},\n"; $i++; }