Passed
Pull Request — master (#1078)
by Pauli
05:42 queued 02:37
created

XmlResponse::addChildElement()   D

Complexity

Conditions 18
Paths 70

Size

Total Lines 44
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 33
c 1
b 0
f 0
nc 70
nop 4
dl 0
loc 44
rs 4.8666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2019 - 2023
11
 */
12
13
namespace OCA\Music\Http;
14
15
use OCP\AppFramework\Http\Response;
16
17
/**
18
 * This class creates an XML response out of a passed in associative array,
19
 * similarly how the class JSONResponse works. The content is described with
20
 * a recursive array structure, where arrays may have string or integer keys.
21
 * One array should not mix string and integer keys, that will lead to undefined
22
 * outcome. Furthermore, array with integer keys is supported only as payload of
23
 * an array with string keys.
24
 *
25
 * Note that this response type has been created to fulfill the needs of the
26
 * SubsonicController and AmpacheController and may not be suitable for all other
27
 * purposes.
28
 */
29
class XmlResponse extends Response {
30
	private $content;
31
	private $doc;
32
	private $attributeKeys;
33
	private $boolAsInt;
34
	private $nullAsEmpty;
35
36
	/**
37
	 * @param array $content
38
	 * @param bool|string[] $attributes If true, then key-value pair is made into attribute if possible.
39
	 *                                  If false, then key-value pairs are never made into attributes.
40
	 *                                  If an array, then keys found from the array are made into attributes if possible.
41
	 * @param bool $boolAsInt If true, any boolean values are yielded as int 0/1.
42
	 *                        If false, any boolean values are yielded as string "false"/"true".
43
	 * @param bool $nullAsEmpty If true, any null values are converted to empty strings, and the result has an empty element or attribute.
44
	 *                          If false, any null-valued keys are are left out from the result.
45
	 */
46
	public function __construct(array $content, /*mixed*/ $attributes=true, bool $boolAsInt=false, bool $nullAsEmpty=false) {
47
		$this->addHeader('Content-Type', 'application/xml');
48
49
		// The content must have exactly one root element, add one if necessary
50
		if (\count($content) != 1) {
51
			$content = ['root' => $content];
52
		}
53
		$this->content = $content;
54
		$this->doc = new \DOMDocument('1.0', 'UTF-8');
55
		$this->doc->formatOutput = true;
56
		$this->attributeKeys = $attributes;
57
		$this->boolAsInt = $boolAsInt;
58
		$this->nullAsEmpty = $nullAsEmpty;
59
	}
60
61
	public function render() {
62
		$rootName = \array_keys($this->content)[0];
63
		$this->addChildElement($this->doc, $rootName, $this->content[$rootName]);
64
		return $this->doc->saveXML();
65
	}
66
67
	private function addChildElement($parentNode, $key, $value, $allowAttribute=true) {
68
		if (\is_bool($value)) {
69
			if ($this->boolAsInt) {
70
				$value = $value ? '1' : '0';
71
			} else {
72
				$value = $value ? 'true' : 'false';
73
			}
74
		} elseif (\is_numeric($value)) {
75
			$value = (string)$value;
76
		} elseif ($value === null && $this->nullAsEmpty) {
77
			$value = '';
78
		}
79
80
		if (\is_string($value)) {
81
			// key 'value' has a special meaning
82
			if ($key == 'value') {
83
				$parentNode->appendChild($this->doc->createTextNode($value));
84
			} elseif ($allowAttribute && $this->keyMayDefineAttribute($key)) {
85
				$parentNode->setAttribute($key, $value);
86
			} else {
87
				$child = $this->doc->createElement($key);
88
				$child->appendChild($this->doc->createTextNode($value));
89
				$parentNode->appendChild($child);
90
			}
91
		} elseif (\is_array($value)) {
92
			if (self::arrayIsIndexed($value)) {
93
				foreach ($value as $child) {
94
					$this->addChildElement($parentNode, $key, $child, /*allowAttribute=*/false);
95
				}
96
			} else { // associative array
97
				$element = $this->doc->createElement($key);
98
				$parentNode->appendChild($element);
99
				foreach ($value as $childKey => $childValue) {
100
					$this->addChildElement($element, $childKey, $childValue);
101
				}
102
			}
103
		} elseif ($value instanceof \stdClass) {
104
			// empty element
105
			$element = $this->doc->createElement($key);
106
			$parentNode->appendChild($element);
107
		} elseif ($value === null) {
108
			// skip
109
		} else {
110
			throw new \Exception("Unexpected value type for key $key");
111
		}
112
	}
113
114
	private function keyMayDefineAttribute($key) {
115
		if (\is_array($this->attributeKeys)) {
116
			return \in_array($key, $this->attributeKeys);
117
		} else {
118
			return \boolval($this->attributeKeys);
119
		}
120
	}
121
122
	/**
123
	 * Array is considered to be "indexed" if its first element has numerical key.
124
	 * Empty array is considered to be "indexed".
125
	 * @param array $array
126
	 */
127
	private static function arrayIsIndexed(array $array) {
128
		\reset($array);
129
		return empty($array) || \is_int(\key($array));
130
	}
131
}
132