Skip to content

Commit 0b360c6

Browse files
committed
Improved XML support
1 parent 4c7ad98 commit 0b360c6

File tree

5 files changed

+147
-95
lines changed

5 files changed

+147
-95
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ You can tune the middleware behavior using middleware specific configuration par
668668
- "joinLimits.records": The maximum number of records returned for a joined entity ("1000")
669669
- "customization.beforeHandler": Handler to implement request customization ("")
670670
- "customization.afterHandler": Handler to implement response customization ("")
671-
- "xml.objectElement": The XML element name to use for objects ("object")
671+
- "xml.types": JSON types that should be added to the XML type attribute ("null,array")
672672

673673
If you don't specify these parameters in the configuration, then the default values (between brackets) are used.
674674

@@ -1054,7 +1054,7 @@ While (note the "format" query parameter):
10541054

10551055
Outputs:
10561056

1057-
<object><id>1</id><user_id>1</user_id><category_id>1</category_id><content>blog started</content></object>
1057+
<root><id>1</id><user_id>1</user_id><category_id>1</category_id><content>blog started</content></root>
10581058

10591059
This functionality is disabled by default and must be enabled using the "middlewares" configuration setting.
10601060

src/Tqdev/PhpCrudApi/Middleware/XmlMiddleware.php

Lines changed: 102 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -22,91 +22,131 @@ public function __construct(Router $router, Responder $responder, array $propert
2222
$this->reflection = $reflection;
2323
}
2424

25-
private function convertFromJsonToXml(string $body): string
25+
private function json2xml($json, $types = 'null,boolean,number,string,object,array')
2626
{
27-
$objectElement = $this->getProperty('objectElement', 'object');
28-
$prolog = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
29-
$xml = new \SimpleXMLElement($prolog . '<root></root>');
30-
$object = json_decode($body);
31-
if (is_scalar($object)) {
32-
$xml = $xml->addChild($objectElement, $object);
33-
} else {
34-
$xml = $xml->addChild($objectElement);
35-
$this->convertFromObjectToXml($object, $xml, $objectElement);
36-
}
37-
return $prolog . $xml->asXML();
38-
}
39-
40-
private function convertFromObjectToXml($object, $xml, string $objectElement): void
41-
{
42-
if (is_array($object)) {
43-
$xml->addAttribute('type', 'list');
44-
}
45-
foreach ($object as $key => $value) {
46-
if (!is_array($value) && !is_object($value)) {
47-
if (is_object($object)) {
48-
$xml->addChild($key, (string) $value);
27+
$a = json_decode($json);
28+
$d = new \DOMDocument();
29+
$c = $d->createElement("root");
30+
$d->appendChild($c);
31+
$t = function ($v) {
32+
$type = gettype($v);
33+
switch ($type) {
34+
case 'integer':
35+
return 'number';
36+
case 'double':
37+
return 'number';
38+
default:
39+
return strtolower($type);
40+
}
41+
};
42+
$ts = explode(',', $types);
43+
$f = function ($f, $c, $a, $s = false) use ($t, $d, $ts) {
44+
if (in_array($t($a), $ts)) {
45+
$c->setAttribute('type', $t($a));
46+
}
47+
if ($t($a) != 'array' && $t($a) != 'object') {
48+
if ($t($a) == 'boolean') {
49+
$c->appendChild($d->createTextNode($a ? 'true' : 'false'));
4950
} else {
50-
$xml->addChild($objectElement, (string) $value);
51+
$c->appendChild($d->createTextNode($a));
52+
}
53+
} else {
54+
foreach ($a as $k => $v) {
55+
if ($k == '__type' && $t($a) == 'object') {
56+
$c->setAttribute('__type', $v);
57+
} else {
58+
if ($t($v) == 'object') {
59+
$ch = $c->appendChild($d->createElementNS(null, $s ? 'item' : $k));
60+
$f($f, $ch, $v);
61+
} else if ($t($v) == 'array') {
62+
$ch = $c->appendChild($d->createElementNS(null, $s ? 'item' : $k));
63+
$f($f, $ch, $v, true);
64+
} else {
65+
$va = $d->createElementNS(null, $s ? 'item' : $k);
66+
if ($t($v) == 'boolean') {
67+
$va->appendChild($d->createTextNode($v ? 'true' : 'false'));
68+
} else {
69+
$va->appendChild($d->createTextNode($v));
70+
}
71+
$ch = $c->appendChild($va);
72+
if (in_array($t($v), $ts)) {
73+
$ch->setAttribute('type', $t($v));
74+
}
75+
}
76+
}
5177
}
52-
continue;
53-
}
54-
$node = $xml;
55-
if (is_object($object)) {
56-
$node = $node->addChild($key);
57-
} elseif (is_object($value)) {
58-
$node = $node->addChild($objectElement);
5978
}
60-
$this->convertFromObjectToXml($value, $node, $objectElement);
61-
}
79+
};
80+
$f($f, $c, $a, $t($a) == 'array');
81+
return $d->saveXML($d->documentElement);
6282
}
6383

64-
private function convertFromXmlToJson(string $body)/*: object */
84+
private function xml2json($xml)
6585
{
66-
$objectElement = $this->getProperty('objectElement', 'object');
67-
$prolog = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
68-
$xml = new \SimpleXMLElement($prolog . $body);
69-
$object = $this->convertFromXmlToObject($xml, $objectElement);
70-
return json_decode(json_encode($object));
71-
}
72-
73-
private function convertFromXmlToObject($xml): array
74-
{
75-
$result = [];
76-
foreach ($xml->children() as $nodeName => $nodeValue) {
77-
if (count($nodeValue->children()) == 0) {
78-
$object = strVal($nodeValue);
79-
} else {
80-
$object = $this->convertFromXmlToObject($nodeValue);
81-
}
82-
$attributes = $xml->attributes();
83-
if ($attributes['type'] == 'list') {
84-
$result[] = $object;
85-
} else {
86-
$result[$nodeName] = $object;
87-
}
86+
$a = @dom_import_simplexml(simplexml_load_string($xml));
87+
if (!$a) {
88+
return null;
8889
}
89-
return $result;
90+
$t = function ($v) {
91+
$t = $v->getAttribute('type');
92+
$txt = $v->firstChild->nodeType == XML_TEXT_NODE;
93+
return $t ?: ($txt ? 'string' : 'object');
94+
};
95+
$f = function ($f, $a) use ($t) {
96+
$c = null;
97+
if ($t($a) == 'null') {
98+
$c = null;
99+
} else if ($t($a) == 'boolean') {
100+
$b = substr(strtolower($a->textContent), 0, 1);
101+
$c = in_array($b, array('1', 't'));
102+
} else if ($t($a) == 'number') {
103+
$c = $a->textContent + 0;
104+
} else if ($t($a) == 'string') {
105+
$c = $a->textContent;
106+
} else if ($t($a) == 'object') {
107+
$c = array();
108+
if ($a->getAttribute('__type')) {
109+
$c['__type'] = $a->getAttribute('__type');
110+
}
111+
for ($i = 0; $i < $a->childNodes->length; $i++) {
112+
$v = $a->childNodes[$i];
113+
$c[$v->nodeName] = $f($f, $v);
114+
}
115+
$c = (object) $c;
116+
} else if ($t($a) == 'array') {
117+
$c = array();
118+
for ($i = 0; $i < $a->childNodes->length; $i++) {
119+
$v = $a->childNodes[$i];
120+
$c[$i] = $f($f, $v);
121+
}
122+
}
123+
return $c;
124+
};
125+
$c = $f($f, $a);
126+
return json_encode($c);
90127
}
91128

92129
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
93130
{
94-
$operation = RequestUtils::getOperation($request);
95-
96131
parse_str($request->getUri()->getQuery(), $params);
97132
$isXml = isset($params['format']) && $params['format'] == 'xml';
98133
if ($isXml) {
99134
$body = $request->getBody()->getContents();
100135
if ($body) {
101-
$json = $this->convertFromXmlToJson($body);
102-
$request = $request->withParsedBody($json);
136+
$json = $this->xml2json($body);
137+
$request = $request->withParsedBody(json_decode($json));
103138
}
104139
}
105140
$response = $next->handle($request);
106141
if ($isXml) {
107142
$body = $response->getBody()->getContents();
108143
if ($body) {
109-
$xml = $this->convertFromJsonToXml($body);
144+
$types = implode(',', $this->getArrayProperty('types', 'null,array'));
145+
if ($types == '' || $types == 'all') {
146+
$xml = $this->json2xml($body);
147+
} else {
148+
$xml = $this->json2xml($body, $types);
149+
}
110150
$response = ResponseFactory::fromXml(ResponseFactory::OK, $xml);
111151
}
112152
}

tests/functional/001_records/087_read_and_write_posts_as_xml.log

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ GET /records/posts/1?format=xml
1111
===
1212
200
1313
Content-Type: text/xml
14-
Content-Length: 145
14+
Content-Length: 102
1515

16-
<?xml version="1.0" encoding="UTF-8"?>
17-
<object><id>1</id><user_id>1</user_id><category_id>1</category_id><content>blog started</content></object>
16+
<root><id>1</id><user_id>1</user_id><category_id>1</category_id><content>blog started</content></root>
1817
===
1918
GET /records/posts?limit=2
2019
===
@@ -28,19 +27,17 @@ GET /records/posts?limit=2&format=xml
2827
===
2928
200
3029
Content-Type: text/xml
31-
Content-Length: 1204
30+
Content-Length: 1122
3231

33-
<?xml version="1.0" encoding="UTF-8"?>
34-
<object><records type="list"><object><id>1</id><user_id>1</user_id><category_id>1</category_id><content>blog started</content></object><object><id>2</id><user_id>1</user_id><category_id>2</category_id><content>🦀€ Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте</content></object><object><id>5</id><user_id>1</user_id><category_id>1</category_id><content>#1</content></object><object><id>6</id><user_id>1</user_id><category_id>1</category_id><content>#2</content></object><object><id>7</id><user_id>1</user_id><category_id>1</category_id><content>#3</content></object><object><id>8</id><user_id>1</user_id><category_id>1</category_id><content>#4</content></object><object><id>9</id><user_id>1</user_id><category_id>1</category_id><content>#5</content></object><object><id>10</id><user_id>1</user_id><category_id>1</category_id><content>#6</content></object><object><id>11</id><user_id>1</user_id><category_id>1</category_id><content>#7</content></object><object><id>12</id><user_id>1</user_id><category_id>1</category_id><content>#8</content></object></records></object>
32+
<root><records type="array"><item><id>1</id><user_id>1</user_id><category_id>1</category_id><content>blog started</content></item><item><id>2</id><user_id>1</user_id><category_id>2</category_id><content>🦀€ Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте</content></item><item><id>5</id><user_id>1</user_id><category_id>1</category_id><content>#1</content></item><item><id>6</id><user_id>1</user_id><category_id>1</category_id><content>#2</content></item><item><id>7</id><user_id>1</user_id><category_id>1</category_id><content>#3</content></item><item><id>8</id><user_id>1</user_id><category_id>1</category_id><content>#4</content></item><item><id>9</id><user_id>1</user_id><category_id>1</category_id><content>#5</content></item><item><id>10</id><user_id>1</user_id><category_id>1</category_id><content>#6</content></item><item><id>11</id><user_id>1</user_id><category_id>1</category_id><content>#7</content></item><item><id>12</id><user_id>1</user_id><category_id>1</category_id><content>#8</content></item></records></root>
3533
===
3634
GET /records/posts/1?join=users&format=xml
3735
===
3836
200
3937
Content-Type: text/xml
40-
Content-Length: 243
38+
Content-Length: 200
4139

42-
<?xml version="1.0" encoding="UTF-8"?>
43-
<object><id>1</id><user_id><id>1</id><username>user1</username><password>testtest2</password><location>POINT(30 20)</location></user_id><category_id>1</category_id><content>blog started</content></object>
40+
<root><id>1</id><user_id><id>1</id><username>user1</username><password>testtest2</password><location>POINT(30 20)</location></user_id><category_id>1</category_id><content>blog started</content></root>
4441
===
4542
GET /records/posts/1?join=users&join=comments,categories
4643
===
@@ -54,10 +51,9 @@ GET /records/posts/1?join=users&join=comments,categories&format=xml
5451
===
5552
200
5653
Content-Type: text/xml
57-
Content-Length: 550
54+
Content-Length: 536
5855

59-
<?xml version="1.0" encoding="UTF-8"?>
60-
<object><id>1</id><user_id><id>1</id><username>user1</username><password>testtest2</password><location>POINT(30 20)</location></user_id><category_id>1</category_id><content>blog started</content><comments type="list"><object><id>1</id><post_id>1</post_id><message>great</message><category_id><id>3</id><name>comment</name><icon/></category_id></object><object><id>2</id><post_id>1</post_id><message>fantastic</message><category_id><id>3</id><name>comment</name><icon/></category_id></object></comments></object>
56+
<root><id>1</id><user_id><id>1</id><username>user1</username><password>testtest2</password><location>POINT(30 20)</location></user_id><category_id>1</category_id><content>blog started</content><comments type="array"><item><id>1</id><post_id>1</post_id><message>great</message><category_id><id>3</id><name>comment</name><icon type="null"></icon></category_id></item><item><id>2</id><post_id>1</post_id><message>fantastic</message><category_id><id>3</id><name>comment</name><icon type="null"></icon></category_id></item></comments></root>
6157
===
6258
GET /records/posts?page=2,1
6359
===
@@ -71,10 +67,9 @@ GET /records/posts?page=2,1&format=xml
7167
===
7268
200
7369
Content-Type: text/xml
74-
Content-Length: 348
70+
Content-Length: 302
7571

76-
<?xml version="1.0" encoding="UTF-8"?>
77-
<object><records type="list"><object><id>2</id><user_id>1</user_id><category_id>2</category_id><content>🦀€ Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте</content></object></records><results>12</results></object>
72+
<root><records type="array"><item><id>2</id><user_id>1</user_id><category_id>2</category_id><content>🦀€ Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте</content></item></records><results>12</results></root>
7873
===
7974
POST /records/posts?format=xml
8075
Content-Type: application/xml
@@ -83,10 +78,9 @@ Content-Type: application/xml
8378
===
8479
200
8580
Content-Type: text/xml
86-
Content-Length: 115
81+
Content-Length: 72
8782

88-
<?xml version="1.0" encoding="UTF-8"?>
89-
<object><code>1009</code><message>Duplicate key exception</message></object>
83+
<root><code>1009</code><message>Duplicate key exception</message></root>
9084
===
9185
PUT /records/posts/1?format=xml
9286
Content-Type: application/xml
@@ -95,7 +89,28 @@ Content-Type: application/xml
9589
===
9690
200
9791
Content-Type: text/xml
98-
Content-Length: 57
92+
Content-Length: 14
93+
94+
<root>1</root>
95+
===
96+
PUT /records/posts/1?format=xml
97+
Content-Type: application/xml
98+
99+
<root><user_id>a</user_id></root>
100+
===
101+
200
102+
Content-Type: text/xml
103+
Content-Length: 137
104+
105+
<root><code>1013</code><message>Input validation failed for 'posts'</message><details><user_id>invalid integer</user_id></details></root>
106+
===
107+
PUT /records/posts/1?format=xml
108+
Content-Type: application/xml
109+
110+
<root><user_id>a</wrong_tag></root>
111+
===
112+
200
113+
Content-Type: text/xml
114+
Content-Length: 73
99115

100-
<?xml version="1.0" encoding="UTF-8"?>
101-
<object>1</object>
116+
<root><code>1008</code><message>Cannot read HTTP message</message></root>

tests/functional/001_records/088_read_and_write_multiple_posts_as_xml.log

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,16 @@ GET /records/posts/1,2?format=xml
1111
===
1212
200
1313
Content-Type: text/xml
14-
Content-Length: 414
14+
Content-Length: 364
1515

16-
<?xml version="1.0" encoding="UTF-8"?>
17-
<object type="list"><object><id>1</id><user_id>1</user_id><category_id>1</category_id><content>blog started</content></object><object><id>2</id><user_id>1</user_id><category_id>2</category_id><content>🦀€ Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте</content></object></object>
16+
<root type="array"><item><id>1</id><user_id>1</user_id><category_id>1</category_id><content>blog started</content></item><item><id>2</id><user_id>1</user_id><category_id>2</category_id><content>🦀€ Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте</content></item></root>
1817
===
1918
PUT /records/posts/1,2?format=xml
2019

21-
<object type="list"><object><user_id>1</user_id></object><object><user_id>2</user_id></object></object>
20+
<object type="array"><object><user_id>1</user_id></object><object><user_id>2</user_id></object></object>
2221
===
2322
200
2423
Content-Type: text/xml
25-
Content-Length: 104
24+
Content-Length: 54
2625

27-
<?xml version="1.0" encoding="UTF-8"?>
28-
<object type="list"><object>1</object><object>1</object></object>
26+
<root type="array"><item>1</item><item>1</item></root>

tests/functional/003_columns/017_get_barcodes_table_as_xml.log

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ GET /columns/barcodes?format=xml
33
===
44
200
55
Content-Type: text/xml
6-
Content-Length: 489
6+
Content-Length: 433
77

8-
<?xml version="1.0" encoding="UTF-8"?>
9-
<object><name>barcodes</name><type>table</type><columns type="list"><object><name>id</name><type>integer</type><pk>1</pk></object><object><name>product_id</name><type>integer</type><fk>products</fk></object><object><name>hex</name><type>varchar</type><length>255</length></object><object><name>bin</name><type>blob</type></object><object><name>ip_address</name><type>varchar</type><length>15</length><nullable>1</nullable></object></columns></object>
8+
<root><name>barcodes</name><type>table</type><columns type="array"><item><name>id</name><type>integer</type><pk>true</pk></item><item><name>product_id</name><type>integer</type><fk>products</fk></item><item><name>hex</name><type>varchar</type><length>255</length></item><item><name>bin</name><type>blob</type></item><item><name>ip_address</name><type>varchar</type><length>15</length><nullable>true</nullable></item></columns></root>

0 commit comments

Comments
 (0)