diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61220af --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor +composer.phar +composer.lock +.php_cs.cache +.DS_Store diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cfbf61b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: php + +php: + - 5.6 + - hhvm + +before_script: + - travis_retry composer self-update + - travis_retry composer install --prefer-source --no-interaction --dev + +script: phpunit diff --git a/LICENSE b/LICENSE index bfd071d..3e53655 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 9fc06ef..8ccfd01 100644 --- a/README.md +++ b/README.md @@ -1 +1,256 @@ -# Xml-Array +# Xml Array +[![Total Downloads](https://poser.pugx.org/jackiedo/xml-array/downloads)](https://packagist.org/packages/jackiedo/xml-array) +[![Latest Stable Version](https://poser.pugx.org/jackiedo/xml-array/v/stable)](https://packagist.org/packages/jackiedo/xml-array) +[![Latest Unstable Version](https://poser.pugx.org/jackiedo/xml-array/v/unstable)](https://packagist.org/packages/jackiedo/xml-array) +[![License](https://poser.pugx.org/jackiedo/xml-array/license)](https://packagist.org/packages/jackiedo/xml-array) + +The conversion between xml and array becomes easier than ever. This package provides some very simple classes to convert XML to array and back. + +# Features of this package +* Convert an XML object (DOMDocument, SimpleXMLElement) or well-formed XML string to an associative array or Json string. +* Convert an associative array to well-formed XML string or DOMDocument. +* Support parsing and building attributes, Cdata sections and namespaces of XML in conversion process. + +# Overview +Look at one of the following sessions to learn more about this package. + +* [Installation](#installation) +* [Basic usage](#basic-usage) + - [Convert XML to array](#convert-xml-to-array) + - [Convert XML to Json](#convert-xml-to-json) + - [Convert array to XML](#convert-array-to-xml) + - [Convert array to DOM](#convert-array-to-dom) +* [Advanced usage](#advanced-usage) + - [Set configuration](#set-configuration) + - [Get configuration](#get-configuration) + - [Default configuration](#default-configuration) +* [License](#license) + +## Installation +You can install this package through [Composer](https://getcomposer.org). + +```shell +composer require jackiedo/xml-array +``` + +## Basic usage + +### Convert XML to array + +###### Syntax: + +``` +array Xml2Array::convert(DOMDocument|SimpleXMLElement|string $inputXML)->toArray(); +``` + +> **Note:** The input XML can be one of types DOMDocument object, SimpleXMLElement object or well-formed XML string. + +###### Example: + +```php +use Jackiedo\XmlArray\Xml2Array; +... + +$xmlString = ' + + Example tag + + Another tag with attributes + + + + + Sub tag 1 + Sub tag 2 + + + + Hello + + + +
Section number 1
+
Section number 2
+
Section number 3
+
+ + + Content + +
'; + +$array = Xml2Array::convert($xmlString)->toArray(); +``` + +After running this piece of code `$array` will contain: + +```php +$array = [ + "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" + ] + ] +] +``` + +### Convert XML to Json + +###### Syntax: + +``` +string Xml2Array::convert(DOMDocument|SimpleXMLElement|string $inputXML)->toJson([int $options = 0]); +``` + +###### Example: + +```php +$jsonString = Xml2Array::convert($xmlString)->toJson(JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); +``` + +### Convert array to XML + +###### Syntax: + +``` +string Array2Xml::convert(array $array)->toXml([bool $prettyOutput = false]); +``` + +###### Example: + +```php +use Jackiedo\XmlArray\Array2Xml; +... + +// We will use array from the result of above example as input for this example +$xmlString = Array2Xml::convert($array)->toXml(true); +``` + +### Convert array to DOM + +###### Syntax: + +``` +DOMDocument Array2Xml::convert(array $array)->toDom(); +``` + +###### Example: + +```php +$domObject = Array2Xml::convert($array)->toDom(); +``` + +## Advanced usage + +### Set configuration +You can set configuration for conversion process with one of following methods: + +###### Method 1: + +```php +... +$config = [ + 'valueKey' => '@text', + 'cdataKey' => '@cdata-section' +]; + +$array = Xml2Array::convert($inputXml, $config)->toArray(); +... + +// And for backward processing +$xml = Array2Xml::convert($inputArray, $config)->toXml(); +``` + +> **Note**: Configuration is an array of parameters. For more details, see section [Default configuration](#default-configuration). + +###### Method 2: + +```php +$converter = new Xml2Array($config); +$array = $converter->convertFrom($inputXml)->toArray(); +``` + +###### Method 3: + +```php +$converter = new Xml2Array; +$array = $converter->setConfig($config)->convertFrom($inputXml)->toArray(); +``` + +### Get configuration +If you implemented the conversion process using methods 2 and 3, you can get configuration of the conversion with method: + +```php +$config = $converter->getConfig(); +``` + +### Default configuration + +###### For Xml2Array + +```php +$defaultConfig = [ + 'version' => '1.0', // Version of XML document + 'encoding' => 'UTF-8', // Encoding of XML document + 'attributesKey' => '@attributes', // The key name use for storing attributes of node + 'cdataKey' => '@cdata', // The key name use for storing value of Cdata Section in node + 'valueKey' => '@value', // The key name use for storing text content of node + 'namespacesOnRoot' => true // Collapse all the namespaces on the root node, otherwise it will put in the nodes for which the namespace first appeared. +]; +``` + +###### For Array2Xml + +```php +$defaultConfig = [ + 'version' => '1.0', // Version of XML document + 'encoding' => 'UTF-8', // Encoding of XML document + 'attributesKey' => '@attributes', // The key name use for storing attributes of node + 'cdataKey' => '@cdata', // The key name use for storing value of Cdata Section in node + 'valueKey' => '@value', // The key name use for storing text content of node + 'rootElement' => null, // The name of root node will be create automatically in process of conversion +]; +``` + +## License +[MIT](LICENSE) © Jackie Do diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c4e9f32 --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "jackiedo/xml-array", + "description": "Simple tools to work with the conversion between xml and array", + "keywords": [ + "xml", + "array", + "tools", + "convert", + "conversion" + ], + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Jackie Do", + "email": "anhvudo@gmail.com" + } + ], + "require": { + "php": ">=5.4.0", + "ext-dom": "*" + }, + "autoload": { + "psr-4": { + "Jackiedo\\XmlArray\\": "src" + } + }, + "minimum-stability": "stable" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..3347b75 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + diff --git a/src/Array2Xml.php b/src/Array2Xml.php new file mode 100644 index 0000000..5d31a32 --- /dev/null +++ b/src/Array2Xml.php @@ -0,0 +1,321 @@ +toXml(); + * $xml = Array2Xml::convert($array, ['rootElement' => 'root_node'])->toXml(); + * $dom = Array2Xml::convert($array)->toDom(); + * + * @package xml-array + * @author Jackie Do + * @copyright 2018 + * @version $Id$ + * @access public + */ +class Array2Xml +{ + /** + * The configuration of the conversion + * + * @var array + */ + protected $config = []; + + /** + * The working XML document + * + * @var DOMDocument + */ + protected $xml = null; + + /** + * Constructor + * + * @param array $config The configuration to use for this instance + */ + public function __construct(array $config = []) + { + $this->setConfig($config); + + $this->xml = new DOMDocument($this->config['version'], $this->config['encoding']); + } + + /** + * Set configuration for converter + * + * @param array $config The configuration to use for conversion + * + * @return $this + */ + public function setConfig(array $config = []) + { + $defaultConfig = [ + 'version' => '1.0', + 'encoding' => 'UTF-8', + 'attributesKey' => '@attributes', + 'cdataKey' => '@cdata', + 'valueKey' => '@value', + 'rootElement' => null, + ]; + + $this->config = array_merge($defaultConfig, $config); + + return $this; + } + + /** + * Return configuration of converter + * + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * Convert an array to an XML document + * A static facade for ease of use and backwards compatibility + * + * @param array $array The input array + * @param array $config The configuration to use for the conversion + * + * @return DOMDocument The XML representation of the input array + */ + public static function convert(array $array = [], array $config = []) + { + $instance = new static($config); + + return $instance->convertFrom($array); + } + + /** + * Convert an array to an XML document + * + * @param array $array The input array + * + * @return $this + */ + public function convertFrom(array $array = []) + { + if (isset($this->config['rootElement'])) { + $rootElement = $this->config['rootElement']; + } elseif (count($array) == 1) { + $rootElement = array_keys($array)[0]; + $array = $array[$rootElement]; + } else { + throw new DOMException('XML documents are allowed only one root element. Wrap your elements in a key or set the `rootElement` parameter in the configuration.'); + } + + $this->xml->appendChild($this->buildNode($rootElement, $array)); + + return $this; + } + + /** + * Return as XML. + * + * @param $prettyOutput Format output for DOMDocument + * + * @return string + */ + public function toXml($prettyOutput = false) + { + $this->xml->formatOutput = (bool) $prettyOutput; + + return $this->xml->saveXML(); + } + + /** + * Return as DOM object. + * + * @return DOMDocument + */ + public function toDom() + { + return $this->xml; + } + + /** + * Build XML node + * + * @param string $nodeName The name of the node that the data will be stored under + * @param mixed $data The value to be build + * + * @throws DOMException + * + * @return DOMElement The XML representation of the input data + */ + protected function buildNode($nodeName, $data) + { + if (!$this->isValidTagName($nodeName)) { + throw new DOMException('Invalid character in the tag name being generated: ' . $nodeName); + } + + $node = $this->xml->createElement($nodeName); + + if (is_array($data)) { + $this->parseArray($node, $nodeName, $data); + } else { + $node->appendChild($this->xml->createTextNode($this->normalizeValues($data))); + } + + return $node; + } + + /** + * Parse array to build node + * + * @param DOMElement $node + * @param string $nodeName + * @param array $array + * + * @throws DOMException + * + * @return void + */ + protected function parseArray(DOMElement $node, $nodeName, array $array) + { + // get the attributes first.; + $array = $this->parseAttributes($node, $nodeName, $array); + + // get value stored in @value + $array = $this->parseValue($node, $array); + + // get value stored in @cdata + $array = $this->parseCdata($node, $array); + + // recurse to build child nodes for this node + foreach ($array as $key => $value) { + if (!$this->isValidTagName($key)) { + throw new DOMException('Invalid character in the tag name being generated: ' . $key); + } + + if (is_array($value) && is_numeric(key($value))) { + // MORE THAN ONE NODE OF ITS KIND; + // if the new array is numeric index, means it is array of nodes of the same kind + // it should follow the parent key name + foreach ($value as $v) { + $node->appendChild($this->buildNode($key, $v)); + } + } else { + // ONLY ONE NODE OF ITS KIND + $node->appendChild($this->buildNode($key, $value)); + } + + unset($array[$key]); + } + } + + /** + * Build attributes of node + * + * @param DOMElement $node + * @param string $nodeName + * @param array $array + * + * @throws DOMException + * + * @return array + */ + protected function parseAttributes(DOMElement $node, $nodeName, array $array) + { + $attributesKey = $this->config['attributesKey']; + + if (array_key_exists($attributesKey, $array) && is_array($array[$attributesKey])) { + foreach ($array[$attributesKey] as $key => $value) { + if (!$this->isValidTagName($key)) { + throw new DOMException('Invalid character in the attribute name being generated: ' . $key); + } + + $node->setAttribute($key, $this->normalizeValues($value)); + } + + unset($array[$attributesKey]); + } + + return $array; + } + + /** + * Build value of node + * + * @param DOMElement $node + * @param array $array + * + * @return DOMElement|array + */ + protected function parseValue(DOMElement $node, array $array) + { + $valueKey = $this->config['valueKey']; + + if (array_key_exists($valueKey, $array)) { + $node->appendChild($this->xml->createTextNode($this->normalizeValues($array[$valueKey]))); + + unset($array[$valueKey]); + } + + return $array; + } + + /** + * Build CDATA of node + * + * @param DOMElement $node + * @param array $array + * + * @return DOMElement|array + */ + protected function parseCdata(DOMElement $node, array $array) + { + $cdataKey = $this->config['cdataKey']; + + if (array_key_exists($cdataKey, $array)) { + $node->appendChild($this->xml->createCDATASection($this->normalizeValues($array[$cdataKey]))); + + unset($array[$cdataKey]); + } + + return $array; + } + + /** + * Get string representation of values + * + * @param mixed $value + * + * @return string + */ + protected function normalizeValues($value) + { + $value = $value === true ? 'true' : $value; + $value = $value === false ? 'false' : $value; + $value = $value === null ? '' : $value; + + return (string) $value; + } + + /** + * Check if the tag name or attribute name contains illegal characters + * @see: http://www.w3.org/TR/xml/#sec-common-syn + * + * @param string + * + * @return boolean + */ + protected function isValidTagName($tag) + { + $pattern = '/^[a-zA-Z_][\w\:\-\.]*$/'; + + return preg_match($pattern, $tag, $matches) && $matches[0] == $tag && substr($tag, -1, 1) != ':'; + } + +} + diff --git a/src/Xml2Array.php b/src/Xml2Array.php new file mode 100644 index 0000000..78944e1 --- /dev/null +++ b/src/Xml2Array.php @@ -0,0 +1,364 @@ +toArray(); + * $array = Xml2Array::convert($xml, ['useNamespaces' => true])->toArray(); + * $json = Xml2Array::convert($xml)->toJson(); + * + * @package xml-array + * @author Jackie Do + * @copyright 2018 + * @version $Id$ + * @access public + */ +class Xml2Array +{ + /** + * The name of the XML attribute that indicates a namespace definition + */ + const ATTRIBUTE_NAMESPACE = 'xmlns'; + + /** + * The string that separates the namespace attribute from the prefix for the namespace + */ + const ATTRIBUTE_NAMESPACE_SEPARATOR = ':'; + + /** + * The configuration of the current instance + * + * @var array + */ + protected $config = []; + + /** + * The working XML document + * + * @var DOMDocument + */ + protected $xml = null; + + /** + * The working list of XML namespaces + * + * @var array + */ + protected $namespaces = []; + + /** + * The result of this conversion + * + * @var array + */ + protected $array = []; + + /** + * Constructor + * + * @param array $config The configuration to use for this instance + */ + public function __construct(array $config = []) + { + $this->setConfig($config); + } + + /** + * Set configuration for converter + * + * @param array $config The configuration to use for conversion + * + * @return $this + */ + public function setConfig(array $config = []) + { + $defaultConfig = [ + 'version' => '1.0', + 'encoding' => 'UTF-8', + 'attributesKey' => '@attributes', + 'cdataKey' => '@cdata', + 'valueKey' => '@value', + 'namespacesOnRoot' => true + ]; + + $this->config = array_merge($defaultConfig, $config); + + return $this; + } + + /** + * Return configuration of converter + * + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * Convert an XML DOMDocument or XML string to an array + * A static facade for ease of use and backwards compatibility + * + * @param DOMDocument|SimpleXMLElement|string $xml The XML to convert to an array + * @param array $config The configuration to use for the conversion + * + * @return array An array representation of the input XML + */ + public static function convert($xml, array $config = []) + { + $instance = new static($config); + + return $instance->convertFrom($xml); + } + + /** + * Convert input XML to an array + * + * @param DOMDocument|SimpleXMLElement|string $inputXml The XML to convert to an array + * + * @return array An array representation of the input XML + */ + public function convertFrom($inputXml) + { + $this->loadXml($inputXml); + + // Convert the XML to an array, starting with the root node + $rootNode = $this->xml->documentElement->nodeName; + $this->array[$rootNode] = $this->parseNode($this->xml->documentElement); + + // Add namespacing information to the root node + if (!empty($this->namespaces) && $this->config['namespacesOnRoot']) { + if (!isset($this->array[$rootNode][$this->config['attributesKey']])) { + $this->array[$rootNode][$this->config['attributesKey']] = []; + } + + foreach ($this->namespaces as $uri => $prefix) { + if ($prefix) { + $prefix = self::ATTRIBUTE_NAMESPACE_SEPARATOR . $prefix; + } + + $this->array[$rootNode][$this->config['attributesKey']][self::ATTRIBUTE_NAMESPACE . $prefix] = $uri; + } + } + + return $this; + } + + /** + * Export result as array + * + * @return array + */ + public function toArray() + { + return $this->array; + } + + /** + * Get result as json string + * + * @param integer $options + * + * @return string + */ + public function toJson($options = 0) + { + return json_encode($this->array, $options); + } + + /** + * Load input into DOMDocument + * + * @param DOMDocument|SimpleXMLElement|string $inputXml The XML to convert to an array + * + * @throws DOMException + * + * @return void + */ + protected function loadXml($inputXml) + { + $this->xml = new DOMDocument($this->config['version'], $this->config['encoding']); + + if (is_string($inputXml)) { + $this->xml->loadXML($inputXml); + } elseif ($inputXml instanceof SimpleXMLElement) { + $this->xml->loadXML($inputXml->asXML()); + } elseif ($inputXml instanceof DOMDocument) { + $this->xml = $inputXml; + } else { + throw new DOMException('The input XML must be one of types DOMDocument, SimpleXMLElement or well-formed XML string.'); + } + } + + /** + * Parse an XML DOMNode + * + * @param DOMNode $node A single XML DOMNode + * + * @return mixed + */ + protected function parseNode(DOMNode $node) + { + $output = []; + $output = $this->collectNamespaces($node, $output); + + switch ($node->nodeType) { + case XML_CDATA_SECTION_NODE: + $output[$this->config['cdataKey']] = trim($node->textContent); + break; + + case XML_TEXT_NODE: + $output = trim($node->textContent); + break; + + case XML_ELEMENT_NODE: + $output = $this->parseChildNodes($node, $output); + $output = $this->normalizeValues($output); + $output = $this->collectAttributes($node, $output); + break; + } + + return $output; + } + + /** + * Parse child nodes of DOMNode + * + * @param DOMNode $node + * @param mixed $output + * + * @return mxied + */ + protected function parseChildNodes(DOMNode $node, $output) + { + if ($node->childNodes->length == 1) { + if (!empty($output)) { + $output[$this->config['valueKey']] = $this->parseNode($node->firstChild); + } 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); + + if ($child->nodeType == XML_TEXT_NODE) { + if ($value != '') { + $output[$this->config['valueKey']] = $value; + } + } else { + $nodeName = $child->nodeName; + + if (!isset($output[$nodeName])) { + $output[$nodeName] = []; + } + + $output[$nodeName][] = $value; + } + } + } + } + + return $output; + } + + /** + * Normalize values + * + * @param mixed $output + * + * @return mixed + */ + protected function normalizeValues($output) + { + if (is_array($output)) { + // if only one node of its kind, assign it directly instead if array($value); + foreach ($output as $key => $value) { + if (is_array($value) && count($value) === 1) { + $keyName = array_keys($value)[0]; + + if (is_numeric($keyName)) { + $output[$key] = $value[$keyName]; + } + } + } + + if (empty($output)) { + $output = ''; + } + } + + return $output; + } + + /** + * Parse DOMNode to get its attributes + * + * @param DOMNode $node + * @param mixed $output + * + * @return mixed + */ + protected function collectAttributes(DOMNode $node, $output) + { + if (!$node->attributes->length) { + return $output; + } + + $attributes = []; + + foreach ($node->attributes as $attributeName => $attributeNode) { + $attributeName = $attributeNode->nodeName; + $attributes[$attributeName] = (string) $attributeNode->value; + } + + // if its a leaf node, store the value in @value instead of directly it. + if (!is_array($output)) { + $output = [$this->config['valueKey'] => $output]; + } + + $output[$this->config['attributesKey']] = $attributes; + + return $output; + } + + /** + * Get the namespace of the supplied node, and add it to the list of known namespaces for this document + * + * @param DOMNode $node + * @param mixed $output + * + * @return mixed + */ + protected function collectNamespaces(DOMNode $node, $output) + { + if ($node->namespaceURI) { + $nsUri = $node->namespaceURI; + $nsPrefix = $node->lookupPrefix($nsUri); + + if (!array_key_exists($nsUri, $this->namespaces)) { + $this->namespaces[$nsUri] = $nsPrefix; + + if (!$this->config['namespacesOnRoot']) { + if ($nsPrefix) { + $nsPrefix = self::ATTRIBUTE_NAMESPACE_SEPARATOR . $nsPrefix; + } + + $output[$this->config['attributesKey']][self::ATTRIBUTE_NAMESPACE . $nsPrefix] = $nsUri; + } + } + } + + return $output; + } +} diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29