Shapesで厳格なアプリケーション作り

# Shapesとは

これまでCollectionやHackの配列について触れてきましたが、
PHPライクな配列を扱いたい場面は多くあります。
PHPの関数の戻り値を扱う場合や、PHPのコードをHackにおき買う場合などなど

Shapesは配列ライクなフィールドに対して型を定義したものです。
これを利用することでTypecheckerでフィールド内の型を判定できますので、
不確実な値が入り込みやすい配列に対してのもう一つのアプローチと言えます。

ただし配列ライク、というだけあって次のものは利用できません。

 - 定義されていない・存在しないキーに対してのアクセスはできません `$shape[$var]` 
 - 配列でよく利用する `[]` オペレーターは利用できません
 - Traversableは実装されていないため、foreachなどは利用できません

*json_encodeを利用するとjsonに変換することができます。

それでは早速Shapesについて見ていきましょう

## Shapesの使い方

```hack
<?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利用が簡単になります。

```hack
<?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型となります。

```hack
<?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);
  }
}

```

定数以外では様々な型を利用できます。

```hack
<?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には警告されません。

```hack
<?hh
type OptionalShape = shape('q' => string, 'w' => string, ?'e' => int);
```

存在しないフィールドを追加するとどうなるでしょうか？

```hack
<?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です。

```hack
<?hh
type NullableShape = shape('q' => string, 'w' => string, 'e' => ?int);
```

## Shapes: Functions
Shapesで用意されているメソッドがいくつかあります。

### idx

Shapesのフィールドがあるかどうかを調べて、
フィールドがない場合は、デフォルトの値を指定して利用します。(オプショナルのフィールドのみ)

```hack
<?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'
    );
  }
}
```

存在しないフィールドを指定する場合は、デフォルトの値を指定する必要があります。

```hack
var_dump(Shapes::idx($shapes, 'z', 'asd'));
```

### toArray

Shapesを配列に変換する場合に利用します。

```hack
<?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変換と同様に、
不確実な値の混入を防ぐことができます。

例を見てみましょう。
最初に問題なしとして処理されるパターンです。

```hack
<?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](https://github.com/hhvm/type-assert) の、
TypeAssert\matches_type_structureを利用していました。

```hack
<?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に対して利用する場合は、以下のようになります。

```hack
  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で宣言した型と異なるものを利用してみます。

```hack
<?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で用意されている様々な機能を利用することで、
厳格なアプリケーションが開発できるようになります。
チーム開発などで是非利用してみてください。