Anyon 9c3fd5b59d feat(wechat): 完善微信协议层通用能力
- 新增 InteractsProtocol,复用 access_token 注入、JSON 请求、raw/download/upload 与消息加解密伪路径处理。

- 微信公众平台、小程序、服务平台统一按官方 path + params/options 调用,服务平台保留授权链路能力。

- 微信支付 APIv3 支持 raw/download 原始响应,并确保签名 query 与实际请求一致。

- 增强 XML 重复节点/嵌套节点编解码和消息安全模式密文校验。
2026-05-08 00:29:10 +08:00

129 lines
3.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
/**
* 微信 XML 编解码工具。
*/
namespace We\Support;
use SimpleXMLElement;
use We\Exception\WechatException;
/**
* 微信 XML 编解码工具。
*
* 用于处理微信消息 XML解码时读取 CDATA 文本,编码时将字段值写入 CDATA。
*/
final class Xml
{
/**
* 将微信消息 XML 解析为数组。
*
* @return array<string,mixed>
*/
public static function decode(string $xml): array
{
$xml = trim($xml);
if ($xml === '') {
return [];
}
$previous = libxml_use_internal_errors(true);
$element = simplexml_load_string($xml, SimpleXMLElement::class, LIBXML_NOCDATA);
libxml_use_internal_errors($previous);
if (!$element instanceof SimpleXMLElement) {
throw new WechatException('微信 XML 格式无效');
}
return self::normalize($element);
}
/**
* 将数组编码为微信消息 XML。
*
* @param array<string,mixed> $data
*/
public static function encode(array $data): string
{
$content = '<xml>';
foreach ($data as $key => $value) {
$content .= self::encodeNode((string)$key, $value);
}
return $content . '</xml>';
}
/**
* 递归编码单个 XML 节点;列表数组会重复输出同名节点。
*/
private static function encodeNode(string $key, mixed $value): string
{
self::assertNodeName($key);
if ($value === []) {
return sprintf('<%1$s></%1$s>', $key);
}
if (is_array($value) && array_is_list($value)) {
$content = '';
foreach ($value as $item) {
$content .= self::encodeNode($key, $item);
}
return $content;
}
if (is_array($value)) {
$content = '';
foreach ($value as $childKey => $childValue) {
$content .= self::encodeNode((string)$childKey, $childValue);
}
return sprintf('<%1$s>%2$s</%1$s>', $key, $content);
}
return sprintf('<%1$s><![CDATA[%2$s]]></%1$s>', $key, self::cdata((string)$value));
}
/**
* 递归转换 SimpleXML 节点为普通数组。
*
* @return array<string,mixed>
*/
private static function normalize(SimpleXMLElement $element): array
{
$result = [];
foreach ($element->children() as $key => $value) {
$children = $value->children();
$node = $children->count() > 0 ? self::normalize($value) : (string)$value;
if (array_key_exists($key, $result)) {
if (!is_array($result[$key]) || !array_is_list($result[$key])) {
$result[$key] = [$result[$key]];
}
$result[$key][] = $node;
continue;
}
$result[$key] = $node;
}
return $result;
}
/**
* 转义 CDATA 结束标记,避免字段值破坏 XML 结构。
*/
private static function cdata(string $value): string
{
return str_replace(']]>', ']]]]><![CDATA[>', $value);
}
/**
* 校验 XML 节点名,避免生成非法 XML。
*/
private static function assertNodeName(string $key): void
{
if (preg_match('/^[A-Za-z_][A-Za-z0-9_.:-]*$/', $key) !== 1) {
throw new WechatException('微信 XML 节点名无效: ' . $key);
}
}
}