diff --git a/docs/dom.md b/docs/dom.md index 5ba90815..53c07d9f 100644 --- a/docs/dom.md +++ b/docs/dom.md @@ -1160,6 +1160,30 @@ $xml = $mapper($someNode); $xml = $doc->stringifyNode($someNode); ``` +#### to_unsafe_legacy_document + +Converts a `Dom\XMLDocument` (PHP 8.4+) into a legacy `DOMDocument` via an XML round-trip. +This is useful when interoperating with libraries that still expect the legacy `DOMDocument` type. + +The `documentURI` is preserved on the resulting `DOMDocument`. + +**Caveat:** Line numbers in the resulting `DOMDocument` may differ from the original because +the new DOM's `saveXML()` can reformat the output (e.g., collapsing multi-line opening tags +into single lines). + +```php +use VeeWee\Xml\Dom\Document; +use function VeeWee\Xml\Dom\Mapper\to_unsafe_legacy_document; + +$doc = Document::fromXmlFile('some.xml'); + +// Using the convenience method on Document: +$legacyDoc = $doc->toUnsafeLegacyDocument(); + +// Or using the mapper function directly: +$legacyDoc = $doc->map(to_unsafe_legacy_document()); +``` + #### xslt_template Allows you to map an XML document based on an [XSLT template](xslt.md). diff --git a/src/Xml/Dom/Document.php b/src/Xml/Dom/Document.php index 38c35b1b..e6e88bc7 100644 --- a/src/Xml/Dom/Document.php +++ b/src/Xml/Dom/Document.php @@ -9,12 +9,14 @@ use Dom\Node; use Dom\XMLDocument; use Dom\XPath as DOMXPath; +use DOMDocument; use VeeWee\Xml\Dom\Traverser\Traverser; use VeeWee\Xml\Dom\Traverser\Visitor; use VeeWee\Xml\ErrorHandling\Issue\IssueCollection; use VeeWee\Xml\Exception\RuntimeException; use function Psl\Vec\map; use function VeeWee\Xml\Dom\Locator\document_element; +use function VeeWee\Xml\Dom\Mapper\to_unsafe_legacy_document; use function VeeWee\Xml\Dom\Mapper\xml_string; use function VeeWee\Xml\Internal\configure; @@ -101,6 +103,18 @@ public function toUnsafeDocument(): XMLDocument return $this->document; } + /** + * Converts this document into a legacy DOMDocument via an XML round-trip. + * + * The documentURI is preserved. Note that line numbers may differ from the original + * because the new DOM's saveXML() can reformat the output (e.g., collapsing + * multi-line opening tags into single lines). + */ + public function toUnsafeLegacyDocument(): DOMDocument + { + return $this->map(to_unsafe_legacy_document()); + } + /** * @template T * @param callable(XMLDocument): T $locator diff --git a/src/Xml/Dom/Mapper/to_unsafe_legacy_document.php b/src/Xml/Dom/Mapper/to_unsafe_legacy_document.php new file mode 100644 index 00000000..fb01b203 --- /dev/null +++ b/src/Xml/Dom/Mapper/to_unsafe_legacy_document.php @@ -0,0 +1,46 @@ + disallow_issues( + static function () use ($document): DOMDocument { + $xml = xml_string()($document); + + $legacy = new DOMDocument(); + disallow_libxml_false_returns( + $legacy->loadXML($xml), + 'Unable to load XML into legacy DOMDocument' + ); + + // documentURI must be set AFTER loadXML() because loadXML() resets it. + $documentUri = $document->documentURI; + if ($documentUri !== '') { + $legacy->documentURI = $documentUri; + } + + return $legacy; + } + ); +} diff --git a/src/bootstrap.php b/src/bootstrap.php index 63f962f4..099050a4 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -71,6 +71,7 @@ 'Xml\Dom\Manipulator\Xmlns\rename' => __DIR__.'/Xml/Dom/Manipulator/Xmlns/rename.php', 'Xml\Dom\Manipulator\Xmlns\rename_element_namespace' => __DIR__.'/Xml/Dom/Manipulator/Xmlns/rename_element_namespace.php', 'Xml\Dom\Manipulator\append' => __DIR__.'/Xml/Dom/Manipulator/append.php', + 'Xml\Dom\Mapper\to_unsafe_legacy_document' => __DIR__.'/Xml/Dom/Mapper/to_unsafe_legacy_document.php', 'Xml\Dom\Mapper\xml_string' => __DIR__.'/Xml/Dom/Mapper/xml_string.php', 'Xml\Dom\Mapper\xslt_template' => __DIR__.'/Xml/Dom/Mapper/xslt_template.php', 'Xml\Dom\Predicate\is_attribute' => __DIR__.'/Xml/Dom/Predicate/is_attribute.php', diff --git a/tests/Xml/Dom/Mapper/ToLegacyDocumentTest.php b/tests/Xml/Dom/Mapper/ToLegacyDocumentTest.php new file mode 100644 index 00000000..bd801add --- /dev/null +++ b/tests/Xml/Dom/Mapper/ToLegacyDocumentTest.php @@ -0,0 +1,63 @@ +hello'); + $legacy = $doc->toUnsafeLegacyDocument(); + + static::assertInstanceOf(DOMDocument::class, $legacy); + static::assertSame('hello', $legacy->getElementsByTagName('item')->item(0)->textContent); + } + + public function test_it_preserves_document_uri(): void + { + $doc = Document::fromXmlString( + '', + \VeeWee\Xml\Dom\Configurator\document_uri('/some/path/file.xml') + ); + $legacy = $doc->toUnsafeLegacyDocument(); + + static::assertSame('/some/path/file.xml', $legacy->documentURI); + } + + public function test_it_preserves_namespaces(): void + { + $xml = 'value'; + $doc = Document::fromXmlString($xml); + $legacy = $doc->toUnsafeLegacyDocument(); + + $items = $legacy->getElementsByTagNameNS('http://example.com', 'item'); + static::assertSame(1, $items->length); + static::assertSame('value', $items->item(0)->textContent); + } + + public function test_it_can_be_used_as_mapper(): void + { + $doc = Document::fromXmlString(''); + $legacy = $doc->map(to_unsafe_legacy_document()); + + static::assertInstanceOf(DOMDocument::class, $legacy); + static::assertSame('root', $legacy->documentElement->localName); + } + + public function test_it_preserves_xml_declaration(): void + { + $xml = '' . PHP_EOL . ''; + $doc = Document::fromXmlString($xml); + $legacy = $doc->toUnsafeLegacyDocument(); + + static::assertSame('1.0', $legacy->xmlVersion); + static::assertSame('UTF-8', $legacy->encoding); + } +}