@@ -84,20 +84,25 @@ public function parse(string $xml): static
8484 /**
8585 * Serialize the document as XML.
8686 *
87- * @param int $indent Indent (pretty-print) by $indent spaces
88- * @param bool $trim Trim leading and trailing whitespace from text nodes?
89- * @param ?array $node Node to serialize (omit to serialize the full record)
87+ * @param int $indent Indent (pretty-print) by $indent spaces
88+ * @param bool $trim Trim leading and trailing whitespace from text nodes?
89+ * @param ?array $node Node to serialize (omit to serialize the full record)
90+ * @param bool $omitSinglePrefix Omit namespace prefix if there's only a single namespace?
9091 *
9192 * @return string
9293 */
93- public function toXML (int $ indent = 0 , bool $ trim = false , ?array $ node = null ): string
94- {
94+ public function toXML (
95+ int $ indent = 0 ,
96+ bool $ trim = false ,
97+ ?array $ node = null ,
98+ bool $ omitSinglePrefix = false
99+ ): string {
95100 if (null === $ this ->parsed ) {
96101 throw new RuntimeException ('No parsed document available ' );
97102 }
98103
99104 return (new XmlRenderer ($ this ->parsed , $ this ->defaultNamespace , $ this ->defaultNamespacePrefix ))
100- ->render ($ indent , $ trim , $ node );
105+ ->render ($ indent , $ trim , $ node, $ omitSinglePrefix );
101106 }
102107
103108 /**
@@ -150,6 +155,23 @@ public function setDefaultNamespace(?string $namespace, ?string $prefix = null):
150155 return $ this ;
151156 }
152157
158+ /**
159+ * Add a namespace prefix.
160+ *
161+ * @param string $namespace Namespace URI
162+ * @param string $prefix Prefix to use
163+ *
164+ * @return static
165+ */
166+ public function addNamespacePrefix (string $ namespace , string $ prefix ): static
167+ {
168+ if (null === $ this ->parsed ) {
169+ throw new RuntimeException ('No parsed document available ' );
170+ }
171+ $ this ->parsed ['namespaces ' ][$ prefix ] = $ namespace ;
172+ return $ this ;
173+ }
174+
153175 /**
154176 * Get root node.
155177 *
@@ -241,9 +263,18 @@ public function firstValue(?array $node = null, string|array $path = '', bool $t
241263 public function attr (?array $ node , string $ attr , bool $ trim = true ): ?string
242264 {
243265 // Try to find the attribute first with namespace and fall back to search without namespace:
244- $ result = $ node ['attrs ' ][Notation::ensureValid ($ attr , $ this ->defaultNamespace )]
245- ?? $ node ['attrs ' ][$ attr ]
246- ?? null ;
266+ $ result = null ;
267+ if ($ parsed = Notation::tryParse ($ attr )) {
268+ $ nsAttr = '{ ' . $ parsed [0 ] . '} ' . $ parsed [1 ];
269+ $ result = $ node ['attrs ' ][$ nsAttr ] ?? $ node ['attrs ' ][$ parsed [1 ]] ?? null ;
270+ } else {
271+ // Try with default namespace:
272+ if (null !== $ this ->defaultNamespace ) {
273+ $ nsAttr = '{ ' . $ this ->defaultNamespace . '} ' . $ attr ;
274+ $ result = $ node ['attrs ' ][$ nsAttr ] ?? null ;
275+ }
276+ $ result ??= $ node ['attrs ' ][$ attr ] ?? null ;
277+ }
247278 return ($ trim && null !== $ result ) ? trim ($ result ) : $ result ;
248279 }
249280
@@ -253,17 +284,17 @@ public function attr(?array $node, string $attr, bool $trim = true): ?string
253284 * Note: This method is typically used with modify(); it only updates the node but does not modify the document!
254285 *
255286 * @param array $node Node
256- * @param string $attr Attribute name either in Clark notation, or just name with $this->defaultNamespace defined
287+ * @param string $attr Attribute name either in Clark notation, or just name
257288 * @param ?string $value Attribute value, or null to unset
258289 *
259290 * @return static
260291 */
261292 public function setAttr (array &$ node , string $ attr , ?string $ value ): static
262293 {
263294 if (null === $ value ) {
264- unset($ node ['attrs ' ][Notation:: ensureValid ( $ attr, $ this -> defaultNamespace ) ]);
295+ unset($ node ['attrs ' ][$ attr ]);
265296 } else {
266- $ node ['attrs ' ][Notation:: ensureValid ( $ attr, $ this -> defaultNamespace ) ] = $ value ;
297+ $ node ['attrs ' ][$ attr ] = $ value ;
267298 }
268299 return $ this ;
269300 }
@@ -290,6 +321,19 @@ public function name(array $node, bool $omitDefaultNs = false): string
290321 return $ node ['name ' ];
291322 }
292323
324+ /**
325+ * Get the local name of a node.
326+ *
327+ * @param array $node Node
328+ *
329+ * @return string
330+ */
331+ public function localName (array $ node ): string
332+ {
333+ [, $ localName ] = Notation::parse ($ node ['name ' ]);
334+ return $ localName ;
335+ }
336+
293337 /**
294338 * Set node name.
295339 *
@@ -339,6 +383,7 @@ public function setValue(array &$node, string $value): static
339383 * Add a child node.
340384 *
341385 * Note: This method is typically used with modify(); it only updates the node but does not modify the document!
386+ * Make sure that the namespace is known (use addNamespacePrefix) if you need to serialize the XML.
342387 *
343388 * @param array $node Parent node
344389 * @param string $name Node name
@@ -369,6 +414,64 @@ public function addChild(
369414 return $ this ;
370415 }
371416
417+ /**
418+ * Remove all child nodes.
419+ *
420+ * Note: This method is typically used with modify(); it only updates the node, but does not modify the document!
421+ *
422+ * @param array $node Parent node
423+ *
424+ * @return static
425+ */
426+ public function removeChildren (array &$ node ): static
427+ {
428+ $ node ['sub ' ] = [];
429+ return $ this ;
430+ }
431+
432+ /**
433+ * Replace all child nodes with the nodes from another XmlDoc (exluding the root element).
434+ *
435+ * Note: This method is typically used with modify(); it only updates the node and this instance's namespace
436+ * prefixes, but does not modify the document!
437+ *
438+ * @param array $node Parent node
439+ * @param XmlDoc $otherDoc XmlDoc with the nodes to use
440+ *
441+ * @return static
442+ */
443+ public function replaceChildren (array &$ node , XmlDoc $ otherDoc ): static
444+ {
445+ $ exported = $ otherDoc ->export ();
446+ // Ensure all namespaces have prefixes:
447+ foreach ($ exported ['namespaces ' ] as $ prefix => $ namespace ) {
448+ if (false !== array_search ($ namespace , $ this ->parsed ['namespaces ' ])) {
449+ continue ;
450+ }
451+ if (null !== ($ existing = $ this ->parsed ['namespaces ' ][$ prefix ] ?? null )) {
452+ if ($ existing !== $ namespace ) {
453+ // Collision, find a free prefix:
454+ $ newPrefix = null ;
455+ for ($ i = 2 ; $ i < 100 ; $ i ++) {
456+ if (!isset ($ this ->namespaces [$ prefix . (string )$ i ])) {
457+ $ newPrefix = $ prefix . (string )$ i ;
458+ break ;
459+ }
460+ }
461+ if (null === $ newPrefix ) {
462+ throw new RuntimeException ("Cannot find a free namespace prefix for $ namespace " );
463+ }
464+ $ this ->parsed ['namespaces ' ][$ newPrefix ] = $ namespace ;
465+ }
466+ } else {
467+ $ this ->parsed ['namespaces ' ][$ prefix ] = $ namespace ;
468+ }
469+ }
470+ $ node ['sub ' ] = $ exported ['data ' ]['sub ' ];
471+
472+ return $ this ;
473+ }
474+
372475 /**
373476 * Filter nodes.
374477 *
@@ -380,7 +483,7 @@ public function addChild(
380483 */
381484 public function filter (callable $ callback ): void
382485 {
383- $ this ->parsed ['data ' ]['sub ' ] = $ this ->filterRecursive ($ callback , $ this ->parsed ['data ' ], []);
486+ $ this ->parsed ['data ' ]['sub ' ] = $ this ->filterRecursive ($ callback , [ $ this ->parsed ['data ' ] ], []);
384487 }
385488
386489 /**
@@ -394,27 +497,29 @@ public function filter(callable $callback): void
394497 */
395498 public function modify (callable $ callback ): void
396499 {
397- $ this ->parsed ['data ' ]['sub ' ] = $ this ->modifyRecursive ($ callback , $ this ->parsed ['data ' ], []);
500+ $ this ->parsed ['data ' ]['sub ' ] = $ this ->modifyRecursive ($ callback , [ $ this ->parsed ['data ' ] ], []);
398501 }
399502
400503 /**
401504 * Filter nodes recursively.
402505 *
403506 * Calls the callback for each node and removes the node if the callback returns true.
404507 *
405- * @param callable $callback Callback
406- * @param array $node Parent node
407- * @param array $path Current path
508+ * @param callable $callback Callback
509+ * @param array $nodeStack Parent node stack
510+ * @param array $path Current path
408511 *
409512 * @return array
410513 */
411- protected function filterRecursive (callable $ callback , array $ node , array $ path ): array
514+ protected function filterRecursive (callable $ callback , array $ nodeStack , array $ path ): array
412515 {
413516 $ result = [];
517+ $ node = end ($ nodeStack );
414518 foreach ($ node ['sub ' ] as $ i => $ subNode ) {
415519 $ subPath = [...$ path , $ subNode ['name ' ]];
416- if (!$ callback ($ subNode , implode ('/ ' , $ subPath ), $ i )) {
417- $ subNode ['sub ' ] = $ this ->filterRecursive ($ callback , $ subNode , $ subPath );
520+ $ subStack = [...$ nodeStack , $ subNode ];
521+ if (!$ callback ($ subNode , implode ('/ ' , $ subPath ), $ i , $ subStack )) {
522+ $ subNode ['sub ' ] = $ this ->filterRecursive ($ callback , $ subStack , $ subPath );
418523 $ result [] = $ subNode ;
419524 }
420525 }
@@ -426,19 +531,23 @@ protected function filterRecursive(callable $callback, array $node, array $path)
426531 *
427532 * Calls the callback for each node to allow it to be updated.
428533 *
429- * @param callable $callback Callback
430- * @param array $node Parent node
431- * @param array $path Current path
534+ * @param callable $callback Callback
535+ * @param array $nodeStack Parent node stack
536+ * @param array $path Current path
432537 *
433538 * @return array
434539 */
435- protected function modifyRecursive (callable $ callback , array $ node , array $ path ): array
540+ protected function modifyRecursive (callable $ callback , array $ nodeStack , array $ path ): array
436541 {
437542 $ result = [];
543+ $ node = end ($ nodeStack );
438544 foreach ($ node ['sub ' ] as $ i => $ subNode ) {
439545 $ subPath = [...$ path , $ subNode ['name ' ]];
440- if (false !== $ callback ($ subNode , implode ('/ ' , $ subPath ), $ i )) {
441- $ subNode ['sub ' ] = $ this ->modifyRecursive ($ callback , $ subNode , $ subPath );
546+ $ subStack = [...$ nodeStack , $ subNode ];
547+ if (false !== $ callback ($ subNode , implode ('/ ' , $ subPath ), $ i , $ subStack )) {
548+ // Recreate subStack with any modifications:
549+ $ subStack = [...$ nodeStack , $ subNode ];
550+ $ subNode ['sub ' ] = $ this ->modifyRecursive ($ callback , $ subStack , $ subPath );
442551 $ result [] = $ subNode ;
443552 }
444553 }
0 commit comments