Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ext/dom/DocumentAppendChild.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function execute(Frame $frame): void
if (\count($frame->calledArgs) < 2) {
throw new \LogicException('DOMDocument::appendChild() expects exactly 1 argument');
}
$child = $this->elementArg($frame->calledArgs[1], 'DOMDocument::appendChild()', 0);
$child = $this->nodeChildArg($frame->calledArgs[1], 'DOMDocument::appendChild()', 0);
VmDom::ensureDocument($receiver);
if (null === $frame->vmContext) {
throw new \LogicException('DOMDocument::appendChild() requires VM context in this compiler build');
Expand All @@ -32,7 +32,7 @@ public function execute(Frame $frame): void
}
}

private function elementArg(Variable $var, string $label, int $index): \PHPCompiler\VM\ObjectEntry
private function nodeChildArg(Variable $var, string $label, int $index): \PHPCompiler\VM\ObjectEntry
{
$var = $var->resolveIndirect();
if (Variable::TYPE_OBJECT !== $var->type) {
Expand All @@ -44,7 +44,7 @@ private function elementArg(Variable $var, string $label, int $index): \PHPCompi
));
}
$object = $var->toObject();
if (!VmDom::isElement($object)) {
if (!VmDom::isAppendableNode($object)) {
throw new \TypeError(sprintf(
'%s expects argument #%d to be of type DOMNode, %s given',
$label,
Expand Down
28 changes: 28 additions & 0 deletions ext/dom/DocumentCreateDocumentFragment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\dom;

use PHPCompiler\Frame;

/** DOMDocument::createDocumentFragment() — VM (#6317, php-src ext/dom/document.c). */
final class DocumentCreateDocumentFragment extends DomClassMethod
{
public function __construct()
{
parent::__construct('createDocumentFragment');
}

public function execute(Frame $frame): void
{
$this->receiver($frame, VmDom::CLASS_DOCUMENT, 'DOMDocument::createDocumentFragment()');
if (null === $frame->vmContext) {
throw new \LogicException('DOMDocument::createDocumentFragment() requires VM context in this compiler build');
}
$fragment = VmDom::createDocumentFragment($frame->vmContext);
if (null !== $frame->returnVar) {
$frame->returnVar->copyFrom($fragment);
}
}
}
2 changes: 2 additions & 0 deletions ext/dom/DomConstants.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ final class DomConstants

public const XML_DOCUMENT_TYPE_NODE = 10;

public const XML_DOCUMENT_FRAG_NODE = 11;

/** Internal marker for {@see VmDom::createNodeList()} handles. */
public const XML_NODELIST = -1;
}
6 changes: 3 additions & 3 deletions ext/dom/ElementAppendChild.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function execute(Frame $frame): void
if (\count($frame->calledArgs) < 2) {
throw new \LogicException('DOMElement::appendChild() expects exactly 1 argument');
}
$child = $this->elementArg($frame->calledArgs[1], 'DOMElement::appendChild()', 0);
$child = $this->nodeChildArg($frame->calledArgs[1], 'DOMElement::appendChild()', 0);
if (null === $frame->vmContext) {
throw new \LogicException('DOMElement::appendChild() requires VM context in this compiler build');
}
Expand All @@ -31,7 +31,7 @@ public function execute(Frame $frame): void
}
}

private function elementArg(Variable $var, string $label, int $index): \PHPCompiler\VM\ObjectEntry
private function nodeChildArg(Variable $var, string $label, int $index): \PHPCompiler\VM\ObjectEntry
{
$var = $var->resolveIndirect();
if (Variable::TYPE_OBJECT !== $var->type) {
Expand All @@ -43,7 +43,7 @@ private function elementArg(Variable $var, string $label, int $index): \PHPCompi
));
}
$object = $var->toObject();
if (!VmDom::isElement($object)) {
if (!VmDom::isAppendableNode($object)) {
throw new \TypeError(sprintf(
'%s expects argument #%d to be of type DOMNode, %s given',
$label,
Expand Down
57 changes: 57 additions & 0 deletions ext/dom/FragmentAppendChild.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\dom;

use PHPCompiler\Frame;
use PHPCompiler\VM\Variable;

/** DOMDocumentFragment::appendChild() — VM (#6317, php-src ext/dom/node.c). */
final class FragmentAppendChild extends DomClassMethod
{
public function __construct()
{
parent::__construct('appendChild');
}

public function execute(Frame $frame): void
{
$receiver = $this->receiver($frame, VmDom::CLASS_DOCUMENT_FRAGMENT, 'DOMDocumentFragment::appendChild()');
if (\count($frame->calledArgs) < 2) {
throw new \LogicException('DOMDocumentFragment::appendChild() expects exactly 1 argument');
}
$child = $this->nodeChildArg($frame->calledArgs[1], 'DOMDocumentFragment::appendChild()', 0);
if (null === $frame->vmContext) {
throw new \LogicException('DOMDocumentFragment::appendChild() requires VM context in this compiler build');
}
$appended = VmDom::appendChild($frame->vmContext, $receiver, $child);
if (null !== $frame->returnVar) {
$frame->returnVar->object($appended);
}
}

private function nodeChildArg(Variable $var, string $label, int $index): \PHPCompiler\VM\ObjectEntry
{
$var = $var->resolveIndirect();
if (Variable::TYPE_OBJECT !== $var->type) {
throw new \TypeError(sprintf(
'%s expects argument #%d to be of type DOMNode, %s given',
$label,
$index + 1,
VmDom::typeLabel($var)
));
}
$object = $var->toObject();
if (!VmDom::isElement($object)) {
throw new \TypeError(sprintf(
'%s expects argument #%d to be of type DOMNode, %s given',
$label,
$index + 1,
$object->class->name
));
}

return $object;
}
}
103 changes: 94 additions & 9 deletions ext/dom/VmDom.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ final class VmDom

public const CLASS_ELEMENT = 'domelement';

public const CLASS_DOCUMENT_FRAGMENT = 'domdocumentfragment';

public const CLASS_NODE = 'domnode';

public const CLASS_NODE_LIST = 'domnodelist';
Expand Down Expand Up @@ -114,6 +116,8 @@ public static function registerClasses(Context $ctx): void
$document->methodVisibility['loadxml'] = $pub;
$document->methods['createelement'] = new DocumentCreateElement();
$document->methodVisibility['createelement'] = $pub;
$document->methods['createdocumentfragment'] = new DocumentCreateDocumentFragment();
$document->methodVisibility['createdocumentfragment'] = $pub;
$document->methods['appendchild'] = new DocumentAppendChild();
$document->methodVisibility['appendchild'] = $pub;
$document->methods['savexml'] = new DocumentSaveXML();
Expand All @@ -129,6 +133,14 @@ public static function registerClasses(Context $ctx): void
$element->methods['appendchild'] = new ElementAppendChild();
$element->methodVisibility['appendchild'] = $pub;
$ctx->classes[self::CLASS_ELEMENT] = $element;

$fragment = new ClassEntry('DOMDocumentFragment');
$fragment->isInternal = true;
$fragment->parentLc = self::CLASS_NODE;
$fragment->properties[] = new ClassProperty(self::PROP_NODE_NAME, null, $strProto);
$fragment->methods['appendchild'] = new FragmentAppendChild();
$fragment->methodVisibility['appendchild'] = $pub;
$ctx->classes[self::CLASS_DOCUMENT_FRAGMENT] = $fragment;
}

public static function createDocumentType(
Expand Down Expand Up @@ -250,6 +262,29 @@ public static function createElement(Context $ctx, string $name): Variable
return $var;
}

public static function createDocumentFragment(Context $ctx): Variable
{
$class = $ctx->classes[self::CLASS_DOCUMENT_FRAGMENT] ?? null;
if (null === $class) {
throw new \LogicException('DOMDocumentFragment is not registered in this compiler build');
}

$entry = new ObjectEntry($class);
$entry->constructed = true;
$entry->getProperty(self::PROP_NODE_NAME)->string('#document-fragment');
self::initNodePropertySlots($entry);

$state = new DomNodeState();
$state->nodeType = DomConstants::XML_DOCUMENT_FRAG_NODE;
$state->nodeName = '#document-fragment';
DomRegistry::attach($entry, $state);

$var = new Variable(Variable::TYPE_OBJECT);
$var->object($entry);

return $var;
}

public static function loadXML(Context $ctx, ObjectEntry $document, string $xml): bool
{
self::ensureDocument($document);
Expand All @@ -275,6 +310,10 @@ public static function loadXML(Context $ctx, ObjectEntry $document, string $xml)

public static function appendChild(Context $ctx, ObjectEntry $parent, ObjectEntry $child): ObjectEntry
{
if (self::isDocumentFragment($child)) {
return self::appendFragmentChildren($ctx, $parent, $child);
}

if (!self::isElement($child)) {
throw new \DOMException('Hierarchy request error');
}
Expand All @@ -283,19 +322,23 @@ public static function appendChild(Context $ctx, ObjectEntry $parent, ObjectEntr
if (DomConstants::XML_DOCUMENT_NODE === $parentState->nodeType) {
$existing = $parent->getProperty(self::PROP_DOCUMENT_ELEMENT)->resolveIndirect();
if (Variable::TYPE_NULL !== $existing->type) {
$parent = $existing->toObject();
$parentState = DomRegistry::state($parent);
} else {
$parentState->documentElementName = DomRegistry::state($child)->nodeName;
$parent->getProperty(self::PROP_DOCUMENT_ELEMENT)->object($child);
self::linkChildToParent($child, null);
self::syncSubtree($ctx, $child);
$parentState->childIds[] = $child->id;
self::linkChildToParent($child, $parent);
self::syncSubtree($ctx, $parent);

return $child;
}
$parentState->documentElementName = DomRegistry::state($child)->nodeName;
$parent->getProperty(self::PROP_DOCUMENT_ELEMENT)->object($child);
self::linkChildToParent($child, null);
self::syncSubtree($ctx, $child);

return $child;
}

if (DomConstants::XML_ELEMENT_NODE !== $parentState->nodeType) {
if (DomConstants::XML_ELEMENT_NODE !== $parentState->nodeType
&& DomConstants::XML_DOCUMENT_FRAG_NODE !== $parentState->nodeType
) {
throw new \DOMException('Hierarchy request error');
}

Expand All @@ -306,6 +349,31 @@ public static function appendChild(Context $ctx, ObjectEntry $parent, ObjectEntr
return $child;
}

private static function appendFragmentChildren(
Context $ctx,
ObjectEntry $parent,
ObjectEntry $fragment
): ObjectEntry {
if (!self::isDocumentFragment($fragment)) {
throw new \LogicException('appendFragmentChildren() expects a DOMDocumentFragment');
}

$fragState = DomRegistry::state($fragment);
$childIds = $fragState->childIds;
$fragState->childIds = [];
foreach ($childIds as $childId) {
$child = DomRegistry::entry($childId);
if (null === $child) {
continue;
}
self::linkChildToParent($child, null);
self::appendChild($ctx, $parent, $child);
}
self::syncSubtree($ctx, $fragment);

return $fragment;
}

public static function saveXML(ObjectEntry $document): string
{
$state = self::ensureDocument($document);
Expand Down Expand Up @@ -638,7 +706,11 @@ private static function syncNodeLinks(Context $ctx, ObjectEntry $node): void
return;
}
}
if (null === $node->class->parentLc && !self::isElement($node) && !self::isDocument($node)) {
if (null === $node->class->parentLc
&& !self::isElement($node)
&& !self::isDocument($node)
&& !self::isDocumentFragment($node)
) {
return;
}
$listVar = self::createNodeList($ctx, $state->childIds);
Expand Down Expand Up @@ -717,6 +789,18 @@ public static function isDocument(ObjectEntry $entry): bool
&& DomConstants::XML_DOCUMENT_NODE === DomRegistry::state($entry)->nodeType;
}

public static function isDocumentFragment(ObjectEntry $entry): bool
{
return self::CLASS_DOCUMENT_FRAGMENT === strtolower($entry->class->name)
&& DomRegistry::has($entry)
&& DomConstants::XML_DOCUMENT_FRAG_NODE === DomRegistry::state($entry)->nodeType;
}

public static function isAppendableNode(ObjectEntry $entry): bool
{
return self::isElement($entry) || self::isDocumentFragment($entry);
}

private static function serializeDoctype(string $name, string $publicId, string $systemId): string
{
if ('' !== $publicId || '' !== $systemId) {
Expand Down Expand Up @@ -791,6 +875,7 @@ private static function classNameFromLc(string $lc): string
self::CLASS_DOCUMENT => 'DOMDocument',
self::CLASS_DOCUMENT_TYPE => 'DOMDocumentType',
self::CLASS_ELEMENT => 'DOMElement',
self::CLASS_DOCUMENT_FRAGMENT => 'DOMDocumentFragment',
default => $lc,
};
}
Expand Down
24 changes: 24 additions & 0 deletions test/compliance/cases/stdlib/dom_document_fragment.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
stdlib DOMDocumentFragment createDocumentFragment/appendChild (#6317, ext/dom/php_dom.c)
--FILE--
<?php
$doc = new DOMDocument();
$root = $doc->createElement('root');
$doc->appendChild($root);
$frag = $doc->createDocumentFragment();
echo get_class($frag), "\n";
$child = $doc->createElement('item');
$frag->appendChild($child);
$root->appendChild($frag);
echo $frag->childNodes->length, "\n";
echo $root->childNodes->length, "\n";
echo $root->firstChild->nodeName, "\n";
echo trim($doc->saveXML()), "\n";
?>
--EXPECT--
DOMDocumentFragment
0
1
item
<?xml version="1.0"?>
<root><item/></root>
14 changes: 14 additions & 0 deletions test/repro/dom_document_fragment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
echo class_exists('DOMDocumentFragment') ? "class ok\n" : "class missing\n";
$doc = new DOMDocument();
$root = $doc->createElement('root');
$doc->appendChild($root);
$frag = $doc->createDocumentFragment();
echo $frag instanceof DOMDocumentFragment ? "instance ok\n" : "instance fail\n";
$child = $doc->createElement('item');
$frag->appendChild($child);
$root->appendChild($frag);
echo $frag->childNodes->length, "\n";
echo $root->childNodes->length, "\n";
echo $root->firstChild->nodeName, "\n";
echo trim($doc->saveXML()), "\n";