diff --git a/src/Xml2Array.php b/src/Xml2Array.php index 0cae476..5663867 100644 --- a/src/Xml2Array.php +++ b/src/Xml2Array.php @@ -191,7 +191,11 @@ protected function loadXml($inputXml) $this->xml = new DOMDocument($this->config['version'], $this->config['encoding']); if (is_string($inputXml)) { - $this->xml->loadXML($inputXml); + $parse = @$this->xml->loadXML($inputXml); + + if ($parse === false) { + throw new DOMException('Error parsing XML string, input is not a well-formed XML string.'); + } } elseif ($inputXml instanceof SimpleXMLElement) { $this->xml->loadXML($inputXml->asXML()); } elseif ($inputXml instanceof DOMDocument) { @@ -215,21 +219,16 @@ protected function parseNode(DOMNode $node) switch ($node->nodeType) { case XML_CDATA_SECTION_NODE: - $output[$this->config['cdataKey']] = trim($node->textContent); + $output[$this->config['cdataKey']] = $this->normalizeTextContent($node->textContent); break; case XML_TEXT_NODE: - $output = trim(preg_replace([ - '/\n+\s+/', - '/\r+\s+/', - '/\n+\t+/', - '/\r+\t+/' - ], ' ', $node->textContent)); + $output = $this->normalizeTextContent($node->textContent); break; case XML_ELEMENT_NODE: $output = $this->parseChildNodes($node, $output); - $output = $this->normalizeValues($output); + $output = $this->normalizeNodeValues($output); $output = $this->collectAttributes($node, $output); break; } @@ -247,32 +246,36 @@ protected function parseNode(DOMNode $node) */ protected function parseChildNodes(DOMNode $node, $output) { - if ($node->childNodes->length == 1) { - if (!empty($output)) { - $output[$this->config['valueKey']] = $this->parseNode($node->firstChild); + foreach ($node->childNodes as $child) { + if ($child->nodeType === XML_CDATA_SECTION_NODE) { + if (!is_array($output)) { + if (!empty($output)) { + $output = [$this->config['valueKey'] => $output]; + } else { + $output = []; + } + } + + $output[$this->config['cdataKey']] = $this->normalizeTextContent($child->textContent); } else { - $output = $this->parseNode($node->firstChild); - } - } else { - foreach ($node->childNodes as $child) { - if ($child->nodeType === XML_CDATA_SECTION_NODE) { - $output[$this->config['cdataKey']] = trim($child->textContent); - } else { - $value = $this->parseNode($child); + $value = $this->parseNode($child); - if ($child->nodeType == XML_TEXT_NODE) { - if ($value != '') { + if ($child->nodeType == XML_TEXT_NODE) { + if ($value != '') { + if (!empty($output)) { $output[$this->config['valueKey']] = $value; + } else { + $output = $value; } - } else { - $nodeName = $child->nodeName; - - if (!isset($output[$nodeName])) { - $output[$nodeName] = []; - } + } + } else { + $nodeName = $child->nodeName; - $output[$nodeName][] = $value; + if (!isset($output[$nodeName])) { + $output[$nodeName] = []; } + + $output[$nodeName][] = $value; } } } @@ -281,32 +284,49 @@ protected function parseChildNodes(DOMNode $node, $output) } /** - * Normalize values + * Clean text content of text node + * + * @param string $textContent + * + * @return string + */ + protected function normalizeTextContent($textContent) + { + return trim(preg_replace([ + '/\n+\s+/', + '/\r+\s+/', + '/\n+\t+/', + '/\r+\t+/' + ], ' ', $textContent)); + } + + /** + * Normalize values of node * - * @param mixed $output + * @param mixed $values * * @return mixed */ - protected function normalizeValues($output) + protected function normalizeNodeValues($values) { - if (is_array($output)) { + if (is_array($values)) { // if only one node of its kind, assign it directly instead if array($value); - foreach ($output as $key => $value) { + foreach ($values as $key => $value) { if (is_array($value) && count($value) === 1) { $keyName = array_keys($value)[0]; if (is_numeric($keyName)) { - $output[$key] = $value[$keyName]; + $values[$key] = $value[$keyName]; } } } - if (empty($output)) { - $output = ''; + if (empty($values)) { + $values = ''; } } - return $output; + return $values; } /** diff --git a/tests/Array2XmlTest.php b/tests/Array2XmlTest.php index 5a72542..eb487b0 100644 --- a/tests/Array2XmlTest.php +++ b/tests/Array2XmlTest.php @@ -59,29 +59,75 @@ class Array2XmlTest extends TestCase ]; /** - * Expected xml string + * Throw DOMException when there are more than one root node * - * @var string + * @test + * + * @return void */ - protected $expected_xml_string = 'Example tagAnother tag with attributesSub tag 1Sub tag 2Hello
Section number 1
Section number 2
Section number 3
Content
'; + public function throw_dom_exception_when_there_are_more_than_one_root_node() + { + $this->expectException(DOMException::class); + $this->expectExceptionMessage('XML documents are allowed only one root element. Wrap your elements in a key or set the `rootElement` parameter in the configuration.'); + + $process = Array2Xml::convert([ + 'root' => 'content', + 'another_root' => 'Another content' + ]); + } /** - * Convert array to xml string + * Throw DOMException when node name is invalid * * @test * * @return void */ - public function convert_array_to_xml_string() + public function throw_dom_exception_when_node_name_is_invalid() { - $dom = new DOMDocument; - $dom->loadXML($this->expected_xml_string); + $this->expectException(DOMException::class); + $this->expectExceptionMessage('Invalid character in the tag name being generated: 0'); + + $process = Array2Xml::convert(['content']); + } + + /** + * Throw DOMException when attribute name is invalid + * + * @test + * + * @return void + */ + public function throw_dom_exception_when_attaribute_name_is_invalid() + { + $this->expectException(DOMException::class); + $this->expectExceptionMessage('Invalid character in the attribute name being generated: invalid attribute'); + + $process = Array2Xml::convert([ + 'root' => [ + 'sub' => [ + '@attributes' => [ + 'invalid attribute' => 'Attribute value' + ] + ] + ] + ]); + } - $this->assertSame($dom->saveXML(), Array2Xml::convert($this->input_array)->toXml()); + /** + * Convert array to XML string + * + * @test + * + * @return void + */ + public function convert_array_to_xml_string() + { + $this->assertXmlStringEqualsXmlFile(__DIR__ . '/resources/example.xml', Array2Xml::convert($this->input_array)->toXml()); } /** - * Convert array to dom + * Convert array to DOM * * @test * @@ -90,8 +136,8 @@ public function convert_array_to_xml_string() public function convert_array_to_dom() { $dom = new DOMDocument; - $dom->loadXML($this->expected_xml_string); + $dom->loadXML(file_get_contents(__DIR__ . '/resources/example.xml')); - $this->assertSame($dom->saveXML(), Array2Xml::convert($this->input_array)->toDom()->saveXML()); + $this->assertEquals($dom, Array2Xml::convert($this->input_array)->toDom()); } } diff --git a/tests/Xml2ArrayTest.php b/tests/Xml2ArrayTest.php index d49d46b..ff62907 100644 --- a/tests/Xml2ArrayTest.php +++ b/tests/Xml2ArrayTest.php @@ -59,7 +59,7 @@ class Xml2ArrayTest extends TestCase ]; /** - * Convert to array from an xml string containing only one empty node + * Convert to array from an XML string containing only one empty node * * @test * @@ -69,13 +69,13 @@ public function convert_to_array_from_an_xml_string_containing_only_one_empty_no { $string = ''; - $this->assertSame([ + $this->assertEquals([ 'root' => '' ], Xml2Array::convert($string)->toArray()); } /** - * Convert to array from an xml string containing one node with empty value + * Convert to array from an XML string containing one node with empty value * * @test * @@ -85,13 +85,13 @@ public function convert_to_array_from_an_xml_string_containing_one_node_with_emp { $string = ''; - $this->assertSame([ + $this->assertEquals([ 'root' => '' ], Xml2Array::convert($string)->toArray()); } /** - * Convert to array from xml string containing one node that whose value is on one line + * Convert to array from XML string containing one node that whose value is on one line * * @test * @@ -101,13 +101,13 @@ public function convert_to_array_from_xml_string_containing_one_node_that_whose_ { $string = 'Welcome to Xml2Array Converter'; - $this->assertSame([ + $this->assertEquals([ 'root' => 'Welcome to Xml2Array Converter' ], Xml2Array::convert($string)->toArray()); } /** - * Convert to array from xml string containing one node that whose value is on multilines + * Convert to array from XML string containing one node that whose value is on multilines * * @test * @@ -122,13 +122,13 @@ public function convert_to_array_from_xml_string_containing_one_node_that_whose_ Xml2Array Converter '; - $this->assertSame([ + $this->assertEquals([ 'root' => "Welcome to Xml2Array Converter" ], Xml2Array::convert($string)->toArray()); } /** - * Convert to array from xml string containing one node that whose value is cdata section + * Convert to array from XML string containing one node that whose value is Cdata Section * * @test * @@ -138,7 +138,7 @@ public function convert_to_array_from_xml_string_containing_one_node_that_whose_ { $string = ''; - $this->assertSame([ + $this->assertEquals([ 'root' => [ '@cdata' => 'This is CDATA section' ] @@ -146,7 +146,7 @@ public function convert_to_array_from_xml_string_containing_one_node_that_whose_ } /** - * Convert to array from xml string that root node has sub node + * Convert to array from XML string that root node has sub node * * @test * @@ -159,7 +159,7 @@ public function convert_to_array_from_xml_string_that_root_node_has_sub_node() Node value '; - $this->assertSame([ + $this->assertEquals([ 'root' => [ 'subnode_1' => 'Node value', 'subnode_2' => 'Node value' @@ -168,7 +168,7 @@ public function convert_to_array_from_xml_string_that_root_node_has_sub_node() } /** - * Convert to array from xml string that node only has attributes + * Convert to array from XML string that node only has attributes * * @test * @@ -181,7 +181,7 @@ public function convert_to_array_from_xml_string_that_node_only_has_attributes() '; - $this->assertSame([ + $this->assertEquals([ 'root' => [ 'subnode_1' => [ '@attributes' => [ @@ -198,7 +198,7 @@ public function convert_to_array_from_xml_string_that_node_only_has_attributes() } /** - * Convert to array from xml string that node has value and attributes + * Convert to array from XML string that node has value and attributes * * @test * @@ -211,7 +211,7 @@ public function convert_to_array_from_xml_string_that_node_has_value_and_attribu Node value '; - $this->assertSame([ + $this->assertEquals([ 'root' => [ 'subnode_1' => [ '@value' => 'Node value', @@ -230,7 +230,7 @@ public function convert_to_array_from_xml_string_that_node_has_value_and_attribu } /** - * Convert to array from xml string containing has namespaces + * Convert to array from XML string containing has namespaces * * @test * @@ -245,7 +245,7 @@ public function convert_to_array_from_xml_string_containing_has_namespaces() '; - $this->assertSame([ + $this->assertEquals([ 'root_node' => [ 'example:node_with_namespace' => [ 'example:sub' => [ @@ -263,7 +263,7 @@ public function convert_to_array_from_xml_string_containing_has_namespaces() } /** - * Convert to array from xml string with special config + * Convert to array from XML string with special config * * @test * @@ -276,7 +276,7 @@ public function convert_to_array_from_xml_string_with_special_config() Content '; - $this->assertSame([ + $this->assertEquals([ 'root_node' => [ 'sub_node' => [ '#text' => 'Content', @@ -292,7 +292,7 @@ public function convert_to_array_from_xml_string_with_special_config() } /** - * Convert from xml string to array with all cases + * Convert from XML string to array with all cases * * @test */ @@ -304,11 +304,11 @@ public function convert_from_xml_string_to_array_with_all_cases() 'namespacesOnRoot' => false ])->toArray(); - $this->assertSame($this->fulltest_expected_result, $result); + $this->assertEquals($this->fulltest_expected_result, $result); } /** - * Convert from xml object to array with all cases + * Convert from XML object to array with all cases * * @test */ @@ -320,11 +320,11 @@ public function convert_from_xml_object_to_array() 'namespacesOnRoot' => false ])->toArray(); - $this->assertSame($this->fulltest_expected_result, $result); + $this->assertEquals($this->fulltest_expected_result, $result); } /** - * Convert from dom object to array with all cases + * Convert from DOM object to array with all cases * * @test */ @@ -337,11 +337,26 @@ public function convert_from_dom_object_to_array() 'namespacesOnRoot' => false ])->toArray(); - $this->assertSame($this->fulltest_expected_result, $result); + $this->assertEquals($this->fulltest_expected_result, $result); } /** - * Throw dom exception when invalid input + * Throw DOMException when input XML string is not well-formed XML + * + * @test + * + * @return void + */ + public function throw_dom_exception_when_input_xml_string_not_well_formed() + { + $this->expectException(DOMException::class); + $this->expectExceptionMessage('Error parsing XML string, input is not a well-formed XML string.'); + + Xml2Array::convert('123'); + } + + /** + * Throw DOMException when invalid input * * @test * @@ -350,23 +365,21 @@ public function convert_from_dom_object_to_array() public function throw_dom_exception_when_invalid_input() { $this->expectException(DOMException::class); + $this->expectExceptionMessage('The input XML must be one of types DOMDocument, SimpleXMLElement or well-formed XML string.'); Xml2Array::convert([]); } /** - * Convert from xml string to json + * Convert from XML string to Json * * @test */ public function convert_from_xml_string_to_json() { $string = file_get_contents(__DIR__ . '/resources/example.xml'); - $result = Xml2Array::convert($string)->toJson(); - $expected = '{"root_node":{"tag":"Example tag","attribute_tag":{"@value":"Another tag with attributes","@attributes":{"description":"This is a tag with attribute"}},"cdata_section":{"@cdata":"This is CDATA section"},"tag_with_subtag":{"sub_tag":["Sub tag 1","Sub tag 2"]},"mixed_section":{"@value":"Hello","@cdata":"This is another CDATA section","section":[{"@value":"Section number 1","@attributes":{"id":"sec_1"}},{"@value":"Section number 2","@attributes":{"id":"sec_2"}},{"@value":"Section number 3","@attributes":{"id":"sec_3"}}]},"example:with_namespace":{"example:sub":"Content"},"@attributes":{"xmlns:example":"http:\/\/example.com"}}}'; - - $this->assertSame($expected, $result); + $this->assertJsonStringEqualsJsonFile(__DIR__ . '/resources/example.json', $result); } } diff --git a/tests/resources/example.json b/tests/resources/example.json new file mode 100644 index 0000000..41c844d --- /dev/null +++ b/tests/resources/example.json @@ -0,0 +1,57 @@ +{ + "root_node": + { + "tag": "Example tag", + "attribute_tag": + { + "@value": "Another tag with attributes", + "@attributes": + { + "description": "This is a tag with attribute" + } + }, + "cdata_section": + { + "@cdata": "This is CDATA section" + }, + "tag_with_subtag": + { + "sub_tag": ["Sub tag 1", "Sub tag 2"] + }, + "mixed_section": + { + "@value": "Hello", + "@cdata": "This is another CDATA section", + "section": [ + { + "@value": "Section number 1", + "@attributes": + { + "id": "sec_1" + } + }, + { + "@value": "Section number 2", + "@attributes": + { + "id": "sec_2" + } + }, + { + "@value": "Section number 3", + "@attributes": + { + "id": "sec_3" + } + }] + }, + "example:with_namespace": + { + "example:sub": "Content" + }, + "@attributes": + { + "xmlns:example": "http:\/\/example.com" + } + } +} \ No newline at end of file diff --git a/tests/resources/example.xml b/tests/resources/example.xml index a03cfe5..d0a9623 100644 --- a/tests/resources/example.xml +++ b/tests/resources/example.xml @@ -1,25 +1,2 @@ - - Example tag - - Another tag with attributes - - - - - Sub tag 1 - Sub tag 2 - - - - Hello - -
Section number 1
-
Section number 2
-
Section number 3
-
- - - Content - -
\ No newline at end of file +Example tagAnother tag with attributesSub tag 1Sub tag 2Hello
Section number 1
Section number 2
Section number 3
Content