Skip to content

Commit a9c5ce1

Browse files
committed
Add new methods and fix attribute handling issues.
1 parent fd989ef commit a9c5ce1

5 files changed

Lines changed: 518 additions & 96 deletions

File tree

src/FinnaXml/XmlDoc.php

Lines changed: 135 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)