alex-kalanis /
restful
| 1 | <?php |
||||
| 2 | |||||
| 3 | namespace kalanis\Restful\Mapping; |
||||
| 4 | |||||
| 5 | |||||
| 6 | use DOMDocument; |
||||
| 7 | use DOMNode; |
||||
| 8 | use kalanis\Restful\Exceptions\InvalidArgumentException; |
||||
| 9 | use kalanis\Restful\Mapping\Exceptions\MappingException; |
||||
| 10 | use Nette\Utils\Arrays; |
||||
| 11 | use Nette\Utils\Json; |
||||
| 12 | use Nette\Utils\JsonException; |
||||
| 13 | use Traversable; |
||||
| 14 | |||||
| 15 | |||||
| 16 | /** |
||||
| 17 | * XmlMapper |
||||
| 18 | * @package kalanis\Restful\Mapping |
||||
| 19 | */ |
||||
| 20 | 1 | class XmlMapper implements IMapper |
|||
| 21 | { |
||||
| 22 | |||||
| 23 | protected const ITEM_ELEMENT = 'item'; |
||||
| 24 | |||||
| 25 | private DOMDocument $xml; |
||||
| 26 | |||||
| 27 | 1 | public function __construct( |
|||
| 28 | private string $rootElement = 'root', |
||||
| 29 | ) |
||||
| 30 | { |
||||
| 31 | 1 | } |
|||
| 32 | |||||
| 33 | /** |
||||
| 34 | * Get XML root element |
||||
| 35 | * @return string |
||||
| 36 | */ |
||||
| 37 | public function getRootElement(): string |
||||
| 38 | { |
||||
| 39 | return $this->rootElement; |
||||
| 40 | } |
||||
| 41 | |||||
| 42 | /** |
||||
| 43 | * Set XML root element |
||||
| 44 | */ |
||||
| 45 | public function setRootElement(string $rootElement): self |
||||
| 46 | { |
||||
| 47 | 1 | $this->rootElement = $rootElement; |
|||
| 48 | 1 | return $this; |
|||
| 49 | } |
||||
| 50 | |||||
| 51 | /** |
||||
| 52 | * Parse traversable or array resource data to XML |
||||
| 53 | * @param string|object|iterable<string|int, mixed> $data |
||||
| 54 | * @param bool $prettyPrint |
||||
| 55 | * @throws InvalidArgumentException |
||||
| 56 | * @return string |
||||
| 57 | */ |
||||
| 58 | public function stringify(iterable|string|object $data, bool $prettyPrint = true): string |
||||
| 59 | { |
||||
| 60 | 1 | if (!is_string($data) && !is_array($data) && !($data instanceof Traversable)) { |
|||
| 61 | throw new InvalidArgumentException('Data must be of type string, array or Traversable'); |
||||
| 62 | } |
||||
| 63 | |||||
| 64 | 1 | if ($data instanceof Traversable) { |
|||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||||
| 65 | $data = iterator_to_array($data); |
||||
| 66 | } |
||||
| 67 | |||||
| 68 | 1 | $this->xml = new DOMDocument('1.0', 'UTF-8'); |
|||
| 69 | 1 | $this->xml->formatOutput = $prettyPrint; |
|||
| 70 | 1 | $this->xml->preserveWhiteSpace = $prettyPrint; |
|||
| 71 | 1 | $root = $this->xml->createElement($this->rootElement); |
|||
| 72 | 1 | $this->xml->appendChild($root); |
|||
| 73 | 1 | $this->toXml($data, $root, self::ITEM_ELEMENT); |
|||
| 74 | 1 | $stored = $this->xml->saveXML(); |
|||
| 75 | 1 | if (false === $stored) { |
|||
| 76 | throw new \RuntimeException('Storing XML failed'); |
||||
| 77 | } |
||||
| 78 | 1 | return $stored; |
|||
| 79 | } |
||||
| 80 | |||||
| 81 | /** |
||||
| 82 | * @param array<string|int, mixed>|string $data |
||||
| 83 | * @param DOMNode $xml |
||||
| 84 | * @param string $previousKey |
||||
| 85 | */ |
||||
| 86 | private function toXml(array|string $data, DOMNode $xml, string $previousKey): void |
||||
| 87 | { |
||||
| 88 | 1 | if (is_iterable($data)) { |
|||
| 89 | 1 | foreach ($data as $key => $value) { |
|||
| 90 | 1 | $node = $xml; |
|||
| 91 | 1 | if (is_int($key)) { |
|||
| 92 | 1 | $node = $this->xml->createElement($previousKey); |
|||
| 93 | 1 | $xml->appendChild($node); |
|||
| 94 | 1 | } elseif (!Arrays::isList($value)) { |
|||
| 95 | 1 | $node = $this->xml->createElement($key); |
|||
| 96 | 1 | $xml->appendChild($node); |
|||
| 97 | } |
||||
| 98 | 1 | $this->toXml($value, $node ?: $xml, is_string($key) ? $key : $previousKey); |
|||
| 99 | } |
||||
| 100 | } else { |
||||
| 101 | 1 | $xml->appendChild($this->xml->createTextNode($data)); |
|||
|
0 ignored issues
–
show
$data of type array<integer|string,mixed> is incompatible with the type string expected by parameter $data of DOMDocument::createTextNode().
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 102 | } |
||||
| 103 | 1 | } |
|||
| 104 | |||||
| 105 | /** |
||||
| 106 | * Parse XML to array |
||||
| 107 | * @param string $data |
||||
| 108 | * @throws MappingException If XML data is not valid |
||||
| 109 | * @return string|object|iterable<string|int, string|int|float|bool|null> |
||||
| 110 | * |
||||
| 111 | */ |
||||
| 112 | public function parse(mixed $data): iterable|string|object |
||||
| 113 | { |
||||
| 114 | 1 | return $this->fromXml(strval($data)); |
|||
|
0 ignored issues
–
show
|
|||||
| 115 | } |
||||
| 116 | |||||
| 117 | /** |
||||
| 118 | * @param string $data |
||||
| 119 | * @throws MappingException If XML data is not valid |
||||
| 120 | * @return iterable<string|int, string|int|float|bool|null> |
||||
| 121 | * |
||||
| 122 | */ |
||||
| 123 | private function fromXml(string $data): iterable |
||||
| 124 | { |
||||
| 125 | try { |
||||
| 126 | 1 | $useErrors = libxml_use_internal_errors(true); |
|||
| 127 | 1 | $xml = simplexml_load_string($data, null, LIBXML_NOCDATA); |
|||
| 128 | 1 | if (false === $xml) { |
|||
| 129 | 1 | $error = libxml_get_last_error(); |
|||
| 130 | 1 | if ($error) { |
|||
|
0 ignored issues
–
show
|
|||||
| 131 | 1 | throw new MappingException('Input is not valid XML document: ' . $error->message . ' on line ' . $error->line); |
|||
| 132 | } else { |
||||
| 133 | throw new MappingException('Total parser failure. Document not valid and cannot get last error.'); |
||||
| 134 | } |
||||
| 135 | } |
||||
| 136 | 1 | libxml_clear_errors(); |
|||
| 137 | 1 | libxml_use_internal_errors($useErrors); |
|||
| 138 | |||||
| 139 | 1 | $data = Json::decode(Json::encode((array) $xml), true); |
|||
| 140 | 1 | return $data ? $this->normalize((array) $data) : []; |
|||
| 141 | 1 | } catch (JsonException $e) { |
|||
| 142 | throw new MappingException('Error in parsing response: ' . $e->getMessage()); |
||||
| 143 | } |
||||
| 144 | } |
||||
| 145 | |||||
| 146 | /** |
||||
| 147 | * Normalize data structure to accepted form |
||||
| 148 | * @param array<string|int, mixed> $value |
||||
| 149 | * @return array<string|int, string|int|float|bool|null> |
||||
| 150 | */ |
||||
| 151 | private function normalize(array $value): array |
||||
| 152 | { |
||||
| 153 | 1 | if (isset($value['@attributes'])) { |
|||
| 154 | 1 | unset($value['@attributes']); |
|||
| 155 | } |
||||
| 156 | 1 | if (0 === count($value)) { |
|||
| 157 | 1 | return []; |
|||
| 158 | } |
||||
| 159 | |||||
| 160 | 1 | foreach ($value as $key => $node) { |
|||
| 161 | 1 | if (!is_array($node)) { |
|||
| 162 | 1 | continue; |
|||
| 163 | } |
||||
| 164 | 1 | $value[$key] = $this->normalize($node); |
|||
| 165 | } |
||||
| 166 | 1 | return $value; |
|||
| 167 | } |
||||
| 168 | } |
||||
| 169 |