3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Hack and HHVMAdvent Calendar 2018

Day 3

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

Last updated at Posted at 2018-12-03

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

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?