Shapesとは
これまでCollectionやHackの配列について触れてきましたが、
PHPライクな配列を扱いたい場面は多くあります。
PHPの関数の戻り値を扱う場合や、PHPのコードをHackにおき買う場合などなど
Shapesは配列ライクなフィールドに対して型を定義したものです。
これを利用することでTypecheckerでフィールド内の型を判定できますので、
不確実な値が入り込みやすい配列に対してのもう一つのアプローチと言えます。
ただし配列ライク、というだけあって次のものは利用できません。
- 定義されていない・存在しないキーに対してのアクセスはできません
$shape[$var]
- 配列でよく利用する
[]
オペレーターは利用できません - Traversableは実装されていないため、foreachなどは利用できません
*json_encodeを利用するとjsonに変換することができます。
それでは早速Shapesについて見ていきましょう
Shapesの使い方
<?hh // strict
namespace Acme\Shapes;
class Standard {
public function introduction(): shape('a' => int, 'b' => int) {
return shape(
'a' => 0,
'b' => 1
);
}
}
shape('フィールド名' => 型)
を指定することでshapeが利用できます。
型宣言に shape(なんたらかんたら)
と記述するのが面倒くさそう。。
そんな時にはtype aliasを利用することでshape利用が簡単になります。
<?hh // strict
namespace Acme\Shapes;
type SampleShape = shape('a' => int, 'b' => int);
class Standard {
public function introduction(): SampleShape {
return shape(
'a' => 0,
'b' => 1
);
}
}
shapeの出力は下記の通りです。
arrayと表示されますが、arrayと同一のものではありません。
array(2) {
["a"]=>
int(0)
["b"]=>
int(1)
}
定数に対しても利用できますが、
文字列型か、int型となります。
<?hh // strict
namespace Acme\Shapes;
type PointS = shape(Standard::KEYA => int, Standard::KEYB => int);
type PointI = shape(Standard::KEYX => int, Standard::KEYY => int);
class Standard {
const string KEYA = 'x';
const string KEYB = 'y';
const int KEYX = 10;
const int KEYY = 23;
public function constants(): PointI {
return shape(Standard::KEYX => -1, Standard::KEYY => 2);
}
}
定数以外では様々な型を利用できます。
<?hh // strict
namespace Acme\Shapes;
type SampleShape = shape('a' => int, 'b' => int);
type NestShape = shape('sample' => SampleShape, 'vec' => vec<string>);
class Standard {
public function nest(): NestShape {
return shape(
'sample' => shape(
'a' => 0,
'b' => 1
),
'vec' => vec[
'shapes'
],
);
}
}
幅広く利用できるのではないでしょうか
filedをオプションにしたい場合
フィールドをオプションとしたい場合は、
フィールド名の前に ?
をつけることでフィールド自体がなくてもTypecheckerには警告されません。
<?hh
type OptionalShape = shape('q' => string, 'w' => string, ?'e' => int);
存在しないフィールドを追加するとどうなるでしょうか?
<?hh // strict
namespace Acme\Shapes;
type OptionalShape = shape('q' => string, 'w' => string, ?'e' => int);
class Standard {
public function optional(): OptionalShape {
return shape(
'q' => 'qwerty',
'w' => 'wertyu',
'z' => 1,
);
}
}
Typecheckerで以下のエラーとなります。
The field 'z' is not defined in this shape type, and this shape type does not allow unknown fields.
The field 'z' is set in the shape.
値をnullableにしたい場合
フィールドの値をnullableにしたい場合は、型に ?
をつければOKです。
<?hh
type NullableShape = shape('q' => string, 'w' => string, 'e' => ?int);
Shapes: Functions
Shapesで用意されているメソッドがいくつかあります。
idx
Shapesのフィールドがあるかどうかを調べて、
フィールドがない場合は、デフォルトの値を指定して利用します。(オプショナルのフィールドのみ)
<?hh // strict
namespace Acme\Shapes;
type NullableShape = shape('q' => string, 'w' => string, ?'e' => int);
use function var_dump;
class Standard {
public function index(): void {
$shapes = $this->optinal();
var_dump(Shapes::idx($shapes, 'e', 123));
var_dump(Shapes::idx($shapes, 'e'));
}
public function optinal(): NullableShape {
return shape(
'q' => 'qwerty',
'w' => 'wertyu'
);
}
}
存在しないフィールドを指定する場合は、デフォルトの値を指定する必要があります。
var_dump(Shapes::idx($shapes, 'z', 'asd'));
toArray
Shapesを配列に変換する場合に利用します。
<?hh // strict
namespace Acme\Shapes;
type NullableShape = shape('q' => string, 'w' => string, ?'e' => int);
use function var_dump;
class Standard {
public function index(): void {
$shapes = $this->optinal();
var_dump(Shapes::toDict($shapes), Shapes::toArray($shapes));
}
public function optinal(): NullableShape {
return shape(
'q' => 'qwerty',
'w' => 'wertyu'
);
}
}
構造体ライクにShapesを利用する
Shapesらしい実践的な利用方法を紹介します。
値を確認するだけではなく、通常の配列に対してのvalidationライクに検査を行うことができます。
つまり、マイクロサービスや、サービス間の連携で利用するメッセージングブローカーのメッセージなどに対して、
値の厳格な検査ができるようになります。
静的型付言語のjson変換と同様に、
不確実な値の混入を防ぐことができます。
例を見てみましょう。
最初に問題なしとして処理されるパターンです。
<?hh // strict
namespace Acme\Shapes;
use function var_dump;
class Standard {
const type embeddedLinks = shape(
'name' => string,
'_links' => shape(
'self' => shape(
'href' => string
)
)
);
public function index(): void {
$url = [
'name' => 'hack',
'_links' => [
'self' => [
'href' =>'uri'
]
]
];
var_dump($url as this::embeddedLinks);
}
}
shapesを使って、フィールドの検査を行う場合は、Hackの as
が利用できます。
shapesで定義した型と一致していれば、var_dumpが実行されます。
この例ではType Constantsを利用していますが、
通常のshapesでも構いません。
HHVM3.27までは、hhvm/type-assert の、
TypeAssert\matches_type_structureを利用していました。
<?hh // strict
use namespace Facebook\TypeAssert;
class Foo {
const type TAPIResponse = shape(
'id' => int,
'user' => string,
'data' => shape(
/* ... */
),
);
public static function getAPIResponse(): self::TAPIResponse {
$json_string = file_get_contents('https://api.example.com');
$array = json_decode($json_string, /* associative = */ true);
return TypeAssert\matches_type_structure(
type_structure(self::class, 'TAPIResponse'),
$array,
);
}
}
これは複雑な、例えばHypertext Application Language (HAL) や、
JSONAPIといったハイパーメディアにはもちろん、
様々なものに利用できます。
HALに対して利用する場合は、以下のようになります。
const type embeddedLinks = shape(
'name' => string,
'_links' => shape(
'self' => shape(
'href' => string
)
)
);
const type hateoasStructure = shape(
'id' => int,
'name' => string,
'title' => string,
'_links' => shape(
'self' => shape(
'href' => string,
'type' => string
)
),
'_embedded' => shape(
'environments' => array<self::embeddedLinks>
)
);
次に失敗する場合です。
一部の値をShapesで宣言した型と異なるものを利用してみます。
<?hh // strict
namespace Acme\Shapes;
use function var_dump;
class Standard {
const type embeddedLinks = shape(
'name' => string,
'_links' => shape(
'self' => shape(
'href' => string
)
)
);
public function index(): void {
$url = [
'name' => 1,
'_links' => [
'self' => [
'href' =>'uri'
]
]
];
var_dump($url as this::embeddedLinks);
}
}
これを実行すると、以下のエラーとなります。
Fatal error: Uncaught exception 'TypeAssertionException' with message 'Expected string at ["name"], got int' in oath/to/hack.file
Shapesで宣言したフィールドに含まれないフィールドがある場合も同様にエラーとなります。
素晴らしい!
PHPではなかなか難しい処理もHackで用意されている様々な機能を利用することで、
厳格なアプリケーションが開発できるようになります。
チーム開発などで是非利用してみてください。