HackでJSONをパースする際にjson_decodeを使うと思うのですが、JSON_FB_HACK_ARRAYSが新しく指定できるようになったみたいなので調べて見ました。
また、JSONを扱う場合の実装をどうするかも検討しました。
json_decodeのオプションによる型の違い
オプションなし
オプションを指定しない場合、パースした結果はarray型になるようです。
function test1(): array<string, mixed> {
$j = file_get_contents("test.json");
$v = json_decode($j, true);
var_dump($v);
return $v;
}
出力
array(2) {
["name"]=>
string(3) "Bob"
["group"]=>
array(1) {
["name"]=>
string(9) "HHVM/Hack"
}
}
JSON_FB_COLLECTIONS
このオプションの場合、パースした結果はMap型になるようです。
// JSON_FB_COLLECTIONS
function test2(): Map<string, mixed> {
$j = file_get_contents("test.json");
$v = json_decode($j, true, 512, JSON_FB_COLLECTIONS);
var_dump($v);
return $v;
}
出力
object(HH\Map)#1 (2) {
["name"]=>
string(3) "Bob"
["group"]=>
object(HH\Map)#2 (1) {
["name"]=>
string(9) "HHVM/Hack"
}
}
JSON_FB_HACK_ARRAYS
このオプションの場合、パースした結果はdict型になるようです。
JSONの構造が静的解析時にわからないので、dict<string, mixed>扱いになりそうです。
// JSON_FB_HACK_ARRAYS
function test3(): dict<string, mixed> {
$j = file_get_contents("test.json");
$v = json_decode($j, true, 512, JSON_FB_HACK_ARRAYS);
var_dump($v);
return $v;
}
出力
dict(2) {
["name"]=>
string(3) "Bob"
["group"]=>
dict(1) {
["name"]=>
string(9) "HHVM/Hack"
}
}
通常はどうするか
普通にJSONを扱うコードを書く分には、次のようにするのをお勧めします。
- 構造の表現にshapeを使用する
- type-assertを使用する
1. 構造の表現にshapeを使用する
dict、array、Mapで型指定してしまうと、どうしてもmixedを使用する羽目になるので、タイプチェッカーと相性が悪くなります。
mixedはnullを含む、全てのものが値として考えられるので、極力使うべきではないと思います。
代わりにJSONのフォーマットをshapeで表現します。
/**
* 次のようなJSONの場合
*
* {
* "name": "Bob",
* "group": {
* "name": "HHVM/Hack"
* }
* }
*/
type GroupJSON = shape(
"name" => string
);
type UserJSON = shape(
"name" => string,
"group" => GroupJSON
);
shapeにした場合、定義されていないフィードに対してのアクセスは静的解析時に検出することができます。
パースした結果を参照したりするコードに非常に有効です。
2. type-assertを使用する
type-assertパッケージを使用して、実行時にチェックを行うようにします。
https://github.com/hhvm/type-assert
パースするJSONをファイルから読み込んだりする場合、期待する構造のものかは実行時にしか基本的にはわからないため、静的解析時の問題検出は諦めて、実行時に検出する方針です。
TypeAssert\matches_type_structureはTypeStructure<T>と検査対象の値を受け取り、値が型の定義を満たしているか検証します。
値が仕様を満たしている場合は、Tの値をそのまま返します。
TypeStructure<T>を得るにはtype_structure関数を使用します。
第1引数にclass名、またはオブジェクト、第2引数にはType Constantの名前を指定します。
この例では、JSON形式のサーバーの設定を読み込む例です。
function server_config_from_json(string $path): ServerConfiguration {
$content = file_get_contents($path);
$rawConfig = json_decode($content, true, 512, JSON_FB_HACK_ARRAYS);
$validConfig = TypeAssert\matches_type_structure(
type_structure(ServerConfiguration::class, 'T'),
$rawConfig
);
return new ServerConfiguration($validConfig);
}
ServerConfigurationの定義はこうなっています。
//JSONの設定ファイル
interface JSONConfiguration {
abstract const type T; //ここの型はインターフェースを実装するクラスで指定する
}
final class ServerConfiguration implements JSONConfiguration {
//JSONのスキーマを指定する
const type T = shape(
"host" => string,
"port" => int
);
public function __construct(private this::T $json) : void
{
}
public function host() : string {
return $this->json['host'];
}
public function port() : int {
return $this->json['port'];
}
}
困りごと
TypeStructure<T>は基本的にtype_structure関数でしか、生成できない見たいです。
ReflectionTypeAliasのgetTypeStructureメソッドでいけないかなと思ったのですが、hhiファイルの定義上はarrayを返すようになっていて、
TypeStructure<T>ではないとタイプチェックでエラーになりました。
TypeStructure<T>はshapeなので、shapeを生成するコードを書くしかないぽいです。