src/XmlToArrayConverter.php
<?php declare(strict_types=1);
/*
* Apache-2 License.
* This file is part of susina/config-builder package, release under the APACHE-2 license.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Susina\ConfigBuilder;
use SimpleXMLElement;
use Susina\ConfigBuilder\Exception\ConfigurationBuilderException;
use Susina\ConfigBuilder\Exception\XmlParseBuilderException;
/**
* Class to convert an xml string to array
*/
class XmlToArrayConverter
{
/**
* Create a PHP array from an XML string
*
* @param string $xmlToParse The XML to parse
*
* @return array
*
* @throws ConfigurationBuilderException If invalid content
* @throws XmlParseBuilderException If errors while parsing XML
*/
public function convert(string $xmlToParse): array
{
if ($xmlToParse === '') {
return [];
}
if (!str_starts_with($xmlToParse, '<')) {
throw new ConfigurationBuilderException('Invalid xml content.');
}
$currentInternalErrors = libxml_use_internal_errors(true);
$xml = simplexml_load_string($xmlToParse);
if ($xml instanceof SimpleXMLElement) {
dom_import_simplexml($xml)->ownerDocument?->xinclude();
}
$errors = libxml_get_errors();
libxml_clear_errors();
libxml_use_internal_errors($currentInternalErrors);
if (count($errors) > 0) {
throw new XmlParseBuilderException($errors);
}
return $this->simpleXmlToArray($xml);
}
/**
* Recursive function that converts an SimpleXML object into an array.
*
* @author Christophe VG (based on code form php.net manual comment)
*
* @param \SimpleXMLElement $xml SimpleXML object.
*
* @return array Array representation of SimpleXML object.
*/
private function simpleXmlToArray(SimpleXMLElement $xml): array
{
$ar = [];
/**
* @var int|string $k
* @var mixed $v
*/
foreach ($xml->children() as $k => $v) {
// recurse the child
$child = $this->simpleXmlToArray($v);
// if it's not an array, then it was empty, thus a value/string
$child = $child === [] ? $this->getConvertedXmlValue($v) : $child;
// add the children attributes as if they where children
foreach ($v->attributes() as $ak => $av) {
if ($ak === 'id') {
// special exception: if there is a key named 'id'
// then we will name the current key after that id
$k = (string)$av;
if (ctype_digit($k)) {
$k = (int)$k;
}
} else {
// otherwise, just add the attribute like a child element
if (!is_array($child)) {
$child = [];
}
$child[$ak] = $this->getConvertedXmlValue($av);
}
}
// if the $k is already in our children list, we need to transform
// it into an array, else we add it as a value
if (!array_key_exists($k, $ar)) {
$ar[$k] = $child;
} else {
// (This only applies to nested nodes that do not have an @id attribute)
// if the $ar[$k] element is not already an array, then we need to make it one.
// this is a bit of a hack, but here we check to also make sure that if it is an
// array, that it has numeric keys. this distinguishes it from simply having other
// nested element data.
if (!is_array($ar[$k]) || !isset($ar[$k][0])) {
$ar[$k] = [$ar[$k]];
}
$ar[$k][] = $child;
}
}
return $ar;
}
/**
* Process XML value, handling boolean, if appropriate.
*
* @param \SimpleXMLElement $valueElement The simplexml value object.
*
* @return bool|float|int|string string or boolean value
*/
private function getConvertedXmlValue(SimpleXMLElement $valueElement): float|bool|int|string
{
$value = (string)$valueElement; // convert from simplexml to string
//handle numeric values
if (is_numeric($value)) {
if (ctype_digit($value)) {
return (int)$value;
}
return (float)$value;
}
return match (strtolower($value)) {
'false' => false,
'true' => true,
default => $value
};
}
}