陣列與 XML 互轉是 API 串接、資料匯出、設定檔處理的常見需求。本文提供一個輕量級 Array2xml 類別,支援多維陣列遞迴轉換、屬性設定、編碼指定,並說明進階擴充方向。
<?php
class Array2xml {
private $xml = '';
private $encoding = 'utf-8';
private $rootElement = 'root';
private $numericKeyPrefix = 'item';
private $attrSeparator = ' '; // 鍵值中用空格分隔屬性,如 "id name='value'"
public function __construct($encoding = 'utf-8', $rootElement = 'root') {
$this->encoding = $encoding;
$this->rootElement = $rootElement;
}
/**
* 主進入點
* @param array $array 要轉換的陣列
* @param string $root 根元素名稱 (可覆蓋建構子設定)
* @return string XML 字串
*/
public function array2xml($array, $root = null) {
$root = $root ?? $this->rootElement;
$this->xml = '<?xml version="1.0" encoding="' . $this->encoding . '"?>';
$this->xml .= $this->_array2xml($array, $root);
return $this->xml;
}
/**
* 遞迴轉換核心
* @param mixed $data 陣列或標量值
* @param string $parentKey 父層鍵名 (用於建立元素名稱)
* @return string XML 片段
*/
private function _array2xml($data, $parentKey = '') {
$xml = '';
if (is_array($data)) {
foreach ($data as $key => $value) {
// 處理數字索引:轉為 - ...
if (is_numeric($key)) {
$elementName = $this->numericKeyPrefix;
$attributes = ' id="' . $key . '"';
} else {
// 字串鍵名:支援 "tag attr='val'" 格式設定屬性
$parts = explode($this->attrSeparator, $key, 2);
$elementName = $parts[0];
$attributes = isset($parts[1]) ? ' ' . $parts[1] : '';
}
$xml .= "<{$elementName}{$attributes}>";
$xml .= $this->_array2xml($value, $elementName);
$xml .= "</{$elementName}>";
}
} elseif (is_bool($data)) {
$xml .= $data ? 'true' : 'false';
} elseif (is_null($data)) {
$xml .= ''; // 或 <nil/>
} else {
// 標量值:需 CDATA 區塊保護特殊字元
$xml .= '<![CDATA[' . $data . ']]>';
}
return $xml;
}
/** 輸出並送出 HTTP 標頭 */
public function output($array, $root = null) {
header('Content-Type: application/xml; charset=' . $this->encoding);
echo $this->array2xml($array, $root);
}
/** 儲存至檔案 */
public function save($array, $filename, $root = null) {
$xml = $this->array2xml($array, $root);
return file_put_contents($filename, $xml);
}
}
?>
<?php
$converter = new Array2xml('utf-8', 'response');
$data = [
'status' => 'success',
'code' => 200,
'user' => [
'id' => 1001,
'username' => 'john_doe',
'email' => 'john@example.com',
'roles' => ['admin', 'editor'],
'profile' => [
'nickname' => 'John',
'avatar' => 'https://example.com/avatar.jpg'
]
],
'items' => [
['id'=>1, 'name'=>'Item A', 'price'=>100],
['id'=>2, 'name'=>'Item B', 'price'=>200],
]
];
echo $converter->array2xml($data);
<?xml version="1.0" encoding="utf-8"?>
<response>
<status><![CDATA[success]]></status>
<code><![CDATA[200]]></code>
<user>
<id><![CDATA[1001]]></id>
<username><![CDATA[john_doe]]></username>
<email><![CDATA[john@example.com]]></email>
<roles>
<item id="0"><![CDATA[admin]]></item>
<item id="1"><![CDATA[editor]]></item>
</roles>
<profile>
<nickname><![CDATA[John]]></nickname>
<avatar><![CDATA[https://example.com/avatar.jpg]]></avatar>
</profile>
</user>
<items>
<item id="0">
<id><![CDATA[1]]></id>
<name><![CDATA[Item A]]></name>
<price><![CDATA[100]]></price>
</item>
<item id="1">
<id><![CDATA[2]]></id>
<name><![CDATA[Item B]]></name>
<price><![CDATA[200]]></price>
</item>
</items>
</response>
$data = [
'product id="P1001" category="electronics"' => [
'name' => 'Smartphone',
'price' => 29999,
'specs' => [
'spec name="screen"' => '6.5 inch OLED',
'spec name="cpu"' => 'Snapdragon 8 Gen 2',
]
]
];
// 輸出:
//
//
//
//
//
//
//
<?php
class Xml2Array {
/**
* XML 字串轉陣列
* @param string $xml XML 字串
* @param bool $attributesAsPrefix 屬性是否加前綴區分
* @return array
*/
public static function parse($xml, $attributesAsPrefix = true) {
$parser = xml_parser_create('UTF-8');
xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1);
xml_parse_into_struct($parser, $xml, $values, $index);
xml_parser_free($parser);
if (empty($values)) return [];
$stack = [];
$result = [];
$current = &$result;
foreach ($values as $val) {
$tag = $val['tag'];
$type = $val['type']; // open, close, complete, cdata
$attrs = $val['attributes'] ?? [];
$value = $val['value'] ?? '';
if ($type === 'open') {
$newNode = [];
if ($attributesAsPrefix && $attrs) {
foreach ($attrs as $ak => $av) {
$newNode['@' . $ak] = $av;
}
}
$current[$tag][] = &$newNode;
$stack[] = &$current;
$current = &$current[$tag][count($current[$tag]) - 1];
} elseif ($type === 'complete' || $type === 'cdata') {
$node = $value;
if ($attributesAsPrefix && $attrs) {
$node = ['@value' => $value];
foreach ($attrs as $ak => $av) {
$node['@' . $ak] = $av;
}
}
$current[$tag][] = $node;
} elseif ($type === 'close') {
$current = &$stack[count($stack) - 1];
array_pop($stack);
}
}
// 簡化單一元素陣列
return self::simplify($result);
}
private static function simplify($data) {
if (is_array($data)) {
foreach ($data as $k => &$v) {
if (is_array($v) && count($v) === 1 && isset($v[0]) && !isset($v['@value'])) {
$v = $v[0];
}
if (is_array($v)) {
$v = self::simplify($v);
}
}
}
return $data;
}
}
// 使用
$xml = '<user id="1001"><name>John</name></user>';
$array = Xml2Array::parse($xml);
// 結果:['user' => [['@id'=>'1001', 'name'=>'John']]]
| 場景 | 說明 | 關鍵點 |
|---|---|---|
| API 回應 | RESTful API 支援 XML 格式 | Content-Type: application/xml;CDATA 保護 |
| 資料匯出 | 報表、備份匯出 XML 檔案 | 大資料量用 XMLWriter 串流寫入 |
| 設定檔 | 系統設定以 XML 儲存 | 結構固定,易驗證 |
| 第三方整合 | 支付、物流、簡訊商 API | 需符合對方 XSD Schema |
| Sitemap 產生 | SEO 用 sitemap.xml | 定期重建,ping 搜尋引擎 |
<?php
function exportLargeDatasetToXml($dataIterator, $filename) {
$writer = new XMLWriter();
$writer->openUri($filename);
$writer->setIndent(true);
$writer->setIndentString(' ');
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('dataset');
foreach ($dataIterator as $row) {
$writer->startElement('row');
foreach ($row as $key => $value) {
$writer->writeElement($key, $value);
}
$writer->endElement(); // row
}
$writer->endElement(); // dataset
$writer->endDocument();
$writer->flush();
// 記憶體用量恆定,不隨資料量增長
}
?>
htmlspecialchars() 轉義。本類別採用 CDATA 方案。startDocument 後加入 $writer->writeDtdElement(...) 或手動拼接 DOCTYPE 行。XMLWriter::startElementNs() 支援命名空間;陣列轉換需擴充鍵名格式如 {ns}tag。preg_replace('/[^a-zA-Z0-9_-]/', '_', $key))。