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を生成するコードを書くしかないぽいです。