在我們當年剛剛上班的那個年代,還全是 XML 的天下,但現在 JSON 資料格式已經是各種應用傳輸的事實标準了。最近幾年開始學習程式設計開發的同學可能都完全沒有接觸過使用 XML 來進行資料傳輸。當然,時代是一直在進步的,JSON 相比 XML 來說,更加地友善快捷,可讀性更高。但其實從語義的角度來說,XML 的表現形式更強。
話不多說,在 PHP 中操作 JSON 其實非常簡單,大家最常用的無非也就是 json_encode() 和 json_decode() 這兩個函數。它們有一些需要注意的地方,也有一些好玩的地方。今天,我們就來深入地再學習一下。
JSON 編碼
首先,我們準備一個數組,用于我們後面編碼的操作。
$data = [
'id' => 1,
'name' => '測試情況',
'cat' => [
'學生 & "在職"',
],
'number' => "123123123",
'edu' => [
[
'name' => '<b>中學</b>',
'date' => '2015-2018',
],
[
'name' => '<b>大學</b>',
'date' => '2018-2022',
],
],
];
非常簡單地數組,其實也沒有什麼特别的東西,隻是有資料的嵌套,有一些中文和特殊符号而已。對于普通的 JSON 編碼來說,直接使用 json_encode() 就可以了。
$json1 = json_encode($data);
var_dump($json1);
// string(215) "{"id":1,"name":"\u6d4b\u8bd5\u60c5\u51b5","cat":["\u5b66\u751f & \"\u5728\u804c\""],"number":"123123123","edu":[{"name":"<b>\u4e2d\u5b66<\/b>","date":"2015-2018"},{"name":"<b>\u5927\u5b66<\/b>","date":"2018-2022"}]}"
中文處理
上面編碼後的 JSON 資料發現了什麼問題沒?沒錯,相信不少人一眼就會看出,中文字元全被轉換成了 \uxxxx 這種格式。這其實是在預設情況下,json_encode() 函數都會将這些多位元組字元轉換成 Unicode 格式的内容。我們直接在 json_encode() 後面增加一個常量參數就可以解決這個問題,讓中文字元正常地顯示出來。
$json1 = json_encode($data, JSON_UNESCAPED_UNICODE);
var_dump($json1);
// string(179) "{"id":1,"name":"測試情況","cat":["學生 & \"在職\""],"number":"123123123","edu":[{"name":"<b>中學<\/b>","date":"2015-2018"},{"name":"<b>大學<\/b>","date":"2018-2022"}]}"
當然,隻是這樣就太沒意思了。因為我曾經在面試的時候就有一位面試官問過我,如果解決這種問題,而且不用這個常量參數。大家可以先不看下面的代碼,思考一下自己有什麼解決方案嗎?
function t($data)
{
foreach ($data as $k => $d) {
if (is_object($d)) {
$d = (array) $d;
}
if (is_array($d)) {
$data[$k] = t($d);
} else {
$data[$k] = urlencode($d);
}
}
return $data;
}
$newData = t($data);
$json1 = json_encode($newData);
var_dump(urldecode($json1));
// string(177) "{"id":"1","name":"測試情況","cat":["學生 & "在職""],"number":"123123123","edu":[{"name":"<b>中學</b>","date":"2015-2018"},{"name":"<b>大學</b>","date":"2018-2022"}]}"
其實就是一個很簡單地解決方案,遞歸地将資料中所有字段内容轉換成 urlencode() 編碼,然後再使用 json_encode() 編碼,完成之後再使用 urldecode() 反解出來。是不是有點意思?其實這是不少老程式員的一個小技巧,因為 JSON_UNESCAPED_UNICODE 這個常量是在 PHP5.4 之後才有的,之前的話如果想讓編碼後的資料直接顯示中文,就隻能這樣操作了。
當然,現在已經是 PHP8 時代了,早就已經不需要這麼麻煩地操作了,不過也不能排除有些面試館仗着自己是老碼農故意出些這樣的題目。大家了解下,知道有這麼回事就可以了,畢竟在實際的項目開發中,使用 PHP5.4 以下版本的系統可能還真是非常少了(這樣的公司不去也罷,技術更新得太慢了)。
其它參數
除了 JSON_UNESCAPED_UNICODE 之外,我們還有許多的常量參數可以使用,而且這個參數是可以并行操作的,也就是可以多個常量參數共同生效。
$json1 = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_NUMERIC_CHECK | JSON_HEX_QUOT);
var_dump($json1);
// string(230) "{"id":1,"name":"測試情況","cat":["學生 \u0026 \u0022在職\u0022"],"number":123123123,"edu":[{"name":"\u003Cb\u003E中學\u003C\/b\u003E","date":"2015-2018"},{"name":"\u003Cb\u003E大學\u003C\/b\u003E","date":"2018-2022"}]}"
這一堆參數其實是針對的我們資料中的一些特殊符号,比如說 & 符、<> HTML 标簽等。當然,還有一些常量參數沒有全部展示出來,大家可以自己查閱官方手冊中的說明。
另外,json_encode() 還有第三個參數,代表的是疊代的層級。比如我們上面的這個資料是多元數組,它有三層,是以我們至少要給到 3 才能正常地解析。下面代碼我們隻是給了一個 1 ,是以傳回的内容就是 false 。也就是無法編碼成功。預設情況下,這個參數的值是 512 。
var_dump(json_encode($data, JSON_UNESCAPED_UNICODE, 1)); // bool(false)
對象及格式處理
預設情況下,json_encode() 會根據資料的類型進行編碼,是以如果是數組的話,那麼它編碼之後的内容就是 JSON 的數組格式,這時我們也可以添加一個 JSON_FORCE_OBJECT ,讓它将一個數組以對象的形式進行編碼。
$data = [];
var_dump(json_encode($data)); // string(2) "[]"
var_dump(json_encode($data, JSON_FORCE_OBJECT)); // string(2) "{}"
之前在講數學相關函數的時候我們學習過,如果資料中有 NAN 這種資料的話,json_encode() 是無法編碼的,其實我們可以添加一個 JSON_PARTIAL_OUTPUT_ON_ERROR ,對一些不可編碼的值進行替換。下面的代碼中,我們就可以使用它讓 NAN 替換成 0 。
$data = NAN;
var_dump(json_encode($data)); // bool(false)
var_dump(json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR)); // 0
對象編碼的屬性問題
對于對象來說,JSON 編碼後的内容就和序列化一樣,隻會有對象的屬性而不會有方法。畢竟 JSON 最大的用處就是用于資料傳輸的,方法對于資料傳輸來說沒有什麼實際的作用。而屬性也會根據它的封裝情況有所不同,隻會編碼公共的,也就是 public 的屬性。
$data = new class
{
private $a = 1;
protected $b = 2;
public $c = 3;
public function x(){
}
};
var_dump(json_encode($data)); // string(7) "{"c":3}"
從這段測試代碼中可以看出,protected 、 private 屬性以及那個方法都不會被編碼。
JSON 解碼
對于 JSON 解碼來說,其實更簡單一些,因為 json_decode() 的常量參數沒有那麼多。
var_dump(json_decode($json1));
// object(stdClass)#1 (5) {
// ["id"]=>
// int(1)
// ["name"]=>
// string(12) "測試情況"
// ["cat"]=>
// ……
// ……
var_dump(json_decode($json1, true));
// array(5) {
// ["id"]=>
// int(1)
// ["name"]=>
// string(12) "測試情況"
// ["cat"]=>
// ……
// ……
首先還是看下它的第二個參數。這個參數的作用其實從代碼中就可以看出來,如果不填這個參數,也就是預設情況下它的值是 false ,那麼解碼出來的資料是對象格式的。而我們将這具參數設定為 true 的話,那麼解碼後的結果就會是數組格式的。這個也是大家非常常用的功能,就不多做解釋了。
var_dump(json_decode('{"a":1321231231231231231231231231231231231231231231231231231231231231231231233}', true));
// array(1) {
// ["a"]=>
// float(1.3212312312312E+72)
// }
var_dump(json_decode('{"a":1321231231231231231231231231231231231231231231231231231231231231231231233}', true, 512, JSON_BIGINT_AS_STRING));
// array(1) {
// ["a"]=>
// string(73) "1321231231231231231231231231231231231231231231231231231231231231231231233"
// }
對于這種非常長的數字格式的資料來說,如果直接 json_decode() 解碼的話,它會直接轉換成 科學計數法 。我們可以直接使用一個 JSON_BIGINT_AS_STRING 常量參數,将這種資料在解碼的時候直接轉換成字元串,其實也就是保留了資料的原始樣貌。注意,這裡 json_decode() 函數的參數因為有那個轉換對象為數組的參數存在,是以它有四個參數,第三個參數是疊代深度,第四個就是定義這些格式化常量值的。而且它和 json_encode() 是反過來的,疊代深度參數在前,格式常量參數在後面,這裡一定要注意哦!
如果資料是錯誤的,那麼 json_decode() 會傳回 NULL 。
var_dump(json_decode("", true)); // NULL
var_dump(json_decode("{a:1}", true)); // NULL
錯誤處理
上面兩段代碼中我們都示範了如果編碼或解碼的資料有問題會出現什麼情況,比如 json_encode() 會傳回 false ,json_decode() 會傳回 NULL 。但是具體的原因呢?
$data = NAN;
var_dump(json_encode($data)); // bool(false)
var_dump(json_last_error()); // int(7)
var_dump(json_last_error_msg()); // string(34) "Inf and NaN cannot be JSON encoded"
沒錯,json_last_error() 和 json_last_error_msg() 就是傳回 JSON 操作時的錯誤資訊的。也就是說,json_encode() 和 json_decode() 在正常情況下是不會報錯的,我們如果要獲得錯誤資訊,就得使用這兩個函數來擷取。這一點也是不少新手小同學沒有注意過的地方,沒錯誤資訊,不抛出異常問題對我們的開發調試其實是非常不友好的。因為很可能找了半天都不知道問題出在哪裡。
在 PHP7.3 之後,新增加了一個常量參數,可以讓我們的 json_encode() 和 json_decode() 在編解碼錯誤的時候抛出異常,這樣我們就可以快速地定位問題了,現在如果大家的系統運作環境是 PHP7.3 以上的話,非常推薦使用這個常量參數讓系統來抛出異常。
// php7.3
var_dump(json_encode($data, JSON_THROW_ON_ERROR));
// Fatal error: Uncaught JsonException: Inf and NaN cannot be JSON encoded
var_dump(json_decode('', true, 512, JSON_THROW_ON_ERROR));
// PHP Fatal error: Uncaught JsonException: Syntax error
JSON_THROW_ON_ERROR 是對 json_encode() 和 json_decode() 都起效的。同樣,隻要設定了這個常量參數,我們就可以使用 try...catch 來進行捕獲了。
try {
var_dump(json_encode($data, JSON_THROW_ON_ERROR));
} catch (JsonException $e) {
var_dump($e->getMessage()); // string(34) "Inf and NaN cannot be JSON encoded"
}
JSON 序列化接口
在之前的文章中,我們學習過 使用Serializable接口來自定義PHP中類的序列化 。也就是說,通過 Serializable 接口我們可以自定義序列化的格式内容。而對于 JSON 來說,同樣也提供了一個 JsonSerializable 接口來實作我自定義 JSON 編碼時的對象格式内容。
class jsontest implements JsonSerializable
{
public function __construct($value)
{$this->value = $value;}
public function jsonSerialize()
{return $this->value;}
}
print "Null -> " . json_encode(new jsontest(null)) . "\n";
print "Array -> " . json_encode(new jsontest(array(1, 2, 3))) . "\n";
print "Assoc. -> " . json_encode(new jsontest(array('a' => 1, 'b' => 3, 'c' => 4))) . "\n";
print "Int -> " . json_encode(new jsontest(5)) . "\n";
print "String -> " . json_encode(new jsontest('Hello, World!')) . "\n";
print "Object -> " . json_encode(new jsontest((object) array('a' => 1, 'b' => 3, 'c' => 4))) . "\n";
// Null -> null
// Array -> [1,2,3]
// Assoc. -> {"a":1,"b":3,"c":4}
// Int -> 5
// String -> "Hello, World!"
// Object -> {"a":1,"b":3,"c":4}
這是一個小的示例,隻需要實作 JsonSerializable 接口中的 jsonSerialize() 方法并傳回内容就可以實作這個 jsontest 對象的 JSON 編碼格式的指定。這裡我們隻是簡單地傳回了資料的内容,其實和普通的 json_encode() 沒什麼太大的差別。下面我們通過一個複雜的例子看一下。
class Student implements JsonSerializable
{
private $id;
private $name;
private $cat;
private $number;
private $edu;
public function __construct($id, $name, $cat = null, $number = null, $edu = null)
{
$this->id = $id;
$this->name = $name;
$this->cat = $cat;
$this->number = $number;
$this->edu = $edu;
}
public function jsonSerialize()
{
if (!$cat) {
$this->cat = ['學生'];
}
if (!$edu) {
$this->edu = new stdClass;
}
$this->number = '學号:' . (!$number ? mt_rand() : $number);
if ($this->id == 2) {
return [
$this->id,
$this->name,
$this->cat,
$this->number,
$this->edu,
];
}
return [
'id' => $this->id,
'name' => $this->name,
'cat' => $this->cat,
'number' => $this->number,
'edu' => $this->edu,
];
}
}
var_dump(json_encode(new Student(1, '測試一'), JSON_UNESCAPED_UNICODE));
// string(82) "{"id":1,"name":"測試一","cat":["學生"],"number":"學号:14017495","edu":{}}"
var_dump(json_encode([new Student(1, '測試一'), new Student(2, '測試二')], JSON_UNESCAPED_UNICODE));
// string(137) "[{"id":1,"name":"測試一","cat":["學生"],"number":"學号:1713936069","edu":{}},[2,"測試二",["學生"],"學号:499173036",{}]]"
在這個例子中,我們在 jsonSerialize() 做了一些操作。如果資料沒有傳值,比如為 null 的情況下就給一個預設值。然後在 id 為 2 的情況下傳回一個普通數組。大家可以看到最後一段注釋中的第二條資料的格式。
這個接口是不是很有意思,相信大家可能對上面的 json_encode() 和 json_decode() 非常熟悉了,但這個接口估計不少人真的是沒接觸過,是不是非常有意思。
總結
果然,什麼事情都怕深挖。不學不知道,一學吓一跳,平常天天用得這麼簡單的 JSON 操作的相關函數其實還有很多好用的功能是我們不知道的。當然,最主要的還是看看文檔,弄明白并且記住一些非常好用的常量參數,另外,抛出異常的功能也是這篇文章的重點内容,建議版本達到的朋友最好都能使用 JSON_THROW_ON_ERROR 來讓錯誤及時抛出,及時發現哦!
測試代碼:
https://github.com/zhangyue0503/dev-blog/blob/master/php/202012/source/11.深入學習PHP中的JSON相關函數.php