You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 21:05:02