Symfony Serializer处理含复杂XSD序列类型的XML问题
问题描述
我有如下XSD定义:
<xsd:complexType name="typeADDRESS"> <xsd:sequence> <xsd:element ref="NAME" minOccurs="0" maxOccurs="unbounded"/> <!-- # ... just to show that these aren't the only ones --> <xsd:sequence minOccurs="0" maxOccurs="unbounded"> <xsd:element ref="EMAIL"/> <xsd:element ref="PUBLIC_KEY" minOccurs="0" maxOccurs="unbounded"/> </xsd:sequence> <!-- # ... and also some attributes later --> </xsd:sequence> </xsd:complexType>
对应的XML数据示例如下:
<EMAIL>A</EMAIL> <PUBLIC_KEY>A1</PUBLIC_KEY> <PUBLIC_KEY>A2</PUBLIC_KEY> <EMAIL>B</EMAIL> <PUBLIC_KEY>B1</PUBLIC_KEY> <PUBLIC_KEY>B2</PUBLIC_KEY>
请问:
- 如何将其转换为合理的PHP结构?
- 反序列化时如何保持顺序?
- 是否需要使用
NormalizableInterface、DenormalizableInterface? - 其余属性是否也需要手动进行(反)序列化处理?
解决方案
1. 合理的PHP结构设计
从XSD的嵌套序列定义出发,最贴合的结构是用索引数组包裹关联数组,每个关联数组对应一组EMAIL及其绑定的PUBLIC_KEY集合,同时保留NAME等其他字段的顺序。示例代码如下:
class Address { /** @var string[] 存储多个NAME节点值 */ public array $names = []; /** * @var array<int, array{email: string, publicKeys: string[]}> * 每个元素对应一组EMAIL+PUBLIC_KEY的嵌套序列 */ public array $emailGroups = []; // 对应XSD中后续定义的其他属性 public ?string $extraAttr = null; }
这里emailGroups的每个元素都严格对应XSD里的嵌套序列:必填的EMAIL作为email字段,可选重复的PUBLIC_KEY作为publicKeys数组,结构清晰且完全匹配XSD约束。
2. 反序列化时保持顺序
PHP的索引数组本身是有序的,只要在反序列化时按XML节点的出现顺序依次处理,就能天然保留原序列。核心逻辑是:遍历XML节点,遇到EMAIL就新建一个分组,后续的PUBLIC_KEY都归到当前分组,直到下一个EMAIL出现。
用SimpleXML实现的示例代码:
$xmlContent = '<ADDRESS>' . $yourXmlSample . '</ADDRESS>'; $xml = simplexml_load_string($xmlContent); $address = new Address(); $currentEmailGroup = null; foreach ($xml->children() as $node) { switch ($node->getName()) { case 'NAME': $address->names[] = (string)$node; break; case 'EMAIL': // 先把上一个分组存入数组 if ($currentEmailGroup !== null) { $address->emailGroups[] = $currentEmailGroup; } // 初始化新分组 $currentEmailGroup = [ 'email' => (string)$node, 'publicKeys' => [] ]; break; case 'PUBLIC_KEY': if ($currentEmailGroup !== null) { $currentEmailGroup['publicKeys'][] = (string)$node; } break; // 处理其他节点或属性 default: if ($node->attributes()) { // 示例处理属性 $address->extraAttr = (string)$node->attributes()->someAttr; } } } // 存入最后一个未完成的分组 if ($currentEmailGroup !== null) { $address->emailGroups[] = $currentEmailGroup; }
3. 是否需要NormalizableInterface/DenormalizableInterface?
如果使用Symfony Serializer这类序列化库,必须使用。因为默认的序列化逻辑会把所有EMAIL和PUBLIC_KEY拆成独立数组,破坏XSD定义的分组关联。
实现这两个接口后,你可以手动控制分组逻辑,确保序列化/反序列化时严格匹配XSD的序列约束。示例实现:
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizableInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; class Address implements NormalizerInterface, DenormalizableInterface, DenormalizerAwareInterface { use DenormalizerAwareTrait; // ... 属性定义 public function denormalize($data, string $type, string $format = null, array $context = []) { $currentGroup = null; foreach ($data as $key => $value) { match ($key) { 'NAME' => $this->names = is_array($value) ? $value : [$value], 'EMAIL' => function() use (&$currentGroup, $value) { if ($currentGroup) $this->emailGroups[] = $currentGroup; $currentGroup = ['email' => $value, 'publicKeys' => []]; }, 'PUBLIC_KEY' => function() use (&$currentGroup, $value) { if (!$currentGroup) return; $currentGroup['publicKeys'] = is_array($value) ? array_merge($currentGroup['publicKeys'], $value) : [...$currentGroup['publicKeys'], $value]; }, // 处理属性(XML中属性会带@前缀) '@extraAttr' => $this->extraAttr = $value, default => null }; } if ($currentGroup) $this->emailGroups[] = $currentGroup; return $this; } public function normalize($object, string $format = null, array $context = []) { $data = []; // 序列化NAME节点 foreach ($this->names as $name) $data['NAME'][] = $name; // 序列化EMAIL+PUBLIC_KEY序列 foreach ($this->emailGroups as $group) { $data['EMAIL'][] = $group['email']; foreach ($group['publicKeys'] as $key) $data['PUBLIC_KEY'][] = $key; } // 序列化属性 if ($this->extraAttr) $data['@extraAttr'] = $this->extraAttr; return $data; } // 接口要求的支持判断方法 public function supportsNormalization($data, string $format = null, array $context = []): bool { return $data instanceof self; } public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool { return $type === self::class; } }
4. 其余属性的(反)序列化处理
普通的单值节点、简单属性不需要手动处理,序列化库的默认逻辑就能完成映射。但对于可重复节点(比如NAME)或复杂结构的属性,需要在接口方法中统一处理,或者通过注解配置(比如Symfony的@SerializedName)告诉序列化库如何映射,确保结构统一。
内容的提问来源于stack exchange,提问作者Hans Mackowiak
相关产品推荐
相关产品推荐

