PHP 与 Javascript API 交互时,何时需要对 JSON 编码或解码?
结论
在当前调用配置下,移除 JSON.stringify() 后就能正常得到数组,因此应直接返回 JavaScript 对象,让 RESTlet 的响应处理层完成序列化。PHP 收到响应后,调用一次 json_decode($response, true) 即可。
题述现象通常是响应被重复 JSON 编码造成的:JSON.stringify() 先把 JavaScript 对象转成字符串,RESTlet 的响应处理层又把这个字符串编码成 JSON。这样,PHP 调用一次 json_decode() 后,得到的仍是字符串。
是否需要编解码,取决于当前数据是对象还是 JSON 文本,以及框架是否已经完成了转换。
为什么解码后仍然是字符串?
下面两段都是合法 JSON,但顶层类型不同。
第一段表示对象:
{"result":"success","data":{"test":"test"}}
第二段表示字符串,字符串的内容又是一段 JSON:
"{\"result\":\"success\",\"data\":{\"test\":\"test\"}}"
PHP 对它们解码后,得到的类型也不同:
$objectJson = '{"result":"success","data":{"test":"test"}}';
$stringJson = '"{\"result\":\"success\",\"data\":{\"test\":\"test\"}}"';
var_dump(json_decode($objectJson, true)); // array
var_dump(json_decode($stringJson, true)); // string
json_decode() 只解析一层 JSON,不会继续解析结果字符串中的 JSON 文本。第二个参数 true 表示将 JSON 对象转换为关联数组,不会把所有类型的结果都强制转换为数组。
在 JavaScript 中,连续编码两次的结果如下:
var value = {
result: 'success',
data: { test: 'test' }
};
var once = JSON.stringify(value);
// {"result":"success","data":{"test":"test"}}
var twice = JSON.stringify(once);
// "{\"result\":\"success\",\"data\":{\"test\":\"test\"}}"
如果 RESTlet 又对返回的字符串做了一次序列化,就会出现题述结果。要确认具体是哪一层进行了重复编码,需要检查原始 HTTP 响应正文,仅看 print_r() 不够直观。
先检查实际传输的内容
排查时,先查看 PHP 从 cURL 获取、尚未解码的响应正文,同时检查 HTTP 状态码和响应的 Content-Type。
设置 CURLOPT_RETURNTRANSFER => true 后,可以这样检查:
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException(curl_error($ch));
}
// 仅用于调试;生产环境应避免记录敏感响应。
var_dump($response);
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException(json_last_error_msg());
}
var_dump($result);
如果原始正文带有前面示例中的外层双引号和转义符,就能确认传输的是 JSON 字符串值。
检查时要区分正文中实际存在的转义符,以及日志工具为了显示字符串而添加的转义符。必要时查看原始响应内容。
PHP 请求和 JavaScript 响应应该怎么写?
PHP:将数组编码一次,再作为请求正文发送
$payload = [
'result' => 'success',
'data' => ['test' => 'test'],
];
$body = json_encode($payload);
if ($body === false) {
throw new RuntimeException(json_last_error_msg());
}
// 假定 $ch 已配置目标地址、鉴权及请求头。
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
]);
请求头应包含 Content-Type: application/json,并保留接口要求的鉴权头。也可以通过 Accept: application/json 表明希望接收 JSON 响应。
传给 CURLOPT_POSTFIELDS 的应是编码后的 $body 字符串。直接传入 PHP 数组会触发表单提交处理,不能替代 JSON 请求正文。
JavaScript:根据入口参数类型决定是否解析
RESTlet 的入口参数可能已经由平台解析,应根据接口约定和参数的实际类型处理:
function post(requestBody) {
var payload;
if (typeof requestBody === 'string') {
payload = JSON.parse(requestBody);
} else {
payload = requestBody;
}
// 适用于题述中直接返回对象即可得到正确响应的配置。
return payload;
}
将这段入口处理逻辑放入现有的 RESTlet 脚本中。
参数已经是对象时,直接使用即可。JSON.parse() 用来解析 JSON 文本,无需对对象再次调用。请求格式有误时,还应按接口约定处理解析异常,并校验字段。
响应的处理方式也取决于平台:题述配置下直接返回对象即可;如果接口要求开发者自行写入原始响应正文,则需要调用 JSON.stringify(),再将生成的文本写入正文。
PHP:解码一次,并检查返回结构
以下代码适用于 PHP 7.2,假定 $response 已通过 cURL 错误检查,HTTP 状态也符合接口预期:
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException(
'响应不是有效 JSON:' . json_last_error_msg()
);
}
if (!is_array($result)) {
throw new UnexpectedValueException('响应顶层类型不符合约定');
}
if (
!isset($result['result'], $result['data']) ||
!is_array($result['data'])
) {
throw new UnexpectedValueException('响应字段不符合约定');
}
PHP 7.2 可以用 json_last_error() 和 json_last_error_msg() 检查错误。JSON_THROW_ON_ERROR 从 PHP 7.3 才开始提供,不能直接用于题述版本。
什么时候需要编码或解码?
| 当前情况 | 应采取的操作 |
|---|---|
| PHP 数组需要成为 JSON 请求正文 | 调用一次 json_encode() |
| JavaScript 收到原始 JSON 请求文本 | 调用一次 JSON.parse() |
| JavaScript 收到平台已解析的对象 | 直接使用 |
| 框架负责把返回对象序列化为 JSON | 直接返回对象 |
| 需要自行生成原始 JSON 响应正文 | 调用一次 JSON.stringify() |
| PHP cURL 收到原始 JSON 响应正文 | 调用一次 json_decode() |
| 客户端库已经把响应解析为对象或数组 | 直接使用 |
同一次请求中,请求正文和响应正文是两份独立的数据,需要分别完成序列化与解析。业务代码是否需要显式调用这些函数,要看框架已经完成了哪些步骤。
文档示例、Python 和签名需要注意什么?
文档中出现 JSON.stringify() 时,要结合完整示例判断返回值要求的是字符串还是对象,并检查示例是否直接写入响应正文、是否有额外的响应封装。题目没有给出具体文档页面和配置,因此无法判断该示例是否适用于当前端点。
Python 的 JSON 编解码遵循同样的类型规则。如果同类代码的表现不同,应比较实际发送和接收的正文,并检查 HTTP 客户端或服务端框架是否自动处理了 JSON。
HMAC-SHA256 不会直接导致解码结果变成字符串。如果签名协议包含请求正文,应按协议用最终发送的正文计算签名,避免在签名后重新编码。空格、转义方式或字段顺序发生变化,都可能改变参与签名的字节。
题目中的写法:
{'result': 'success', 'data': {'test': 'test'}}
是 JavaScript 对象字面量,但不是合法的 JSON 文本。JSON 的属性名和字符串值必须使用双引号。
只有接口明确约定返回“包含 JSON 文本的字符串”时,才应进行第二次解码,不要将连续解码两次作为默认修复方式。如果接口两端都由自己维护,应约定响应顶层为对象,并修正多余的序列化操作。
备注:内容仅供参考。
