Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPDocにおける配列の型定義を少し知る

shimabox
December 31, 2022

 PHPDocにおける配列の型定義を少し知る

kaonavi Tech Talk #9 で発表した資料です。

※ 動画はこちら

shimabox

December 31, 2022
Tweet

Other Decks in Programming

Transcript

  1. PHPDocにおける
    配列の型定義を少し知る
    2022/09/28
    @島袋隆広

    View Slide

  2. Who am I

    View Slide

  3. あじぇんだ
    - はじめに
    - ArrayShapes/ジェネリクス記法 とは
    - PHPStan(Larastan) との関係
    - まとめ

    View Slide

  4. PHPDocを書く際に、クラスのプロパティ, 引数, 返り値が配列の場合、
    @var array, @param array, @return array などと書くことが多いと思います。
    が、これだと配列の中身が mixed と言っているのと一緒です。
    配列の中身がわかっているなら、ちゃんと定義しよう!
    定義する方法として、ArrayShapes/ジェネリクス記法※ というのがあるよ。
    というのが今回のお話です。
    ※ 引用元はスライドの後半に用意
    はじめに

    View Slide

  5. あじぇんだ
    - はじめに
    - ArrayShapes/ジェネリクス記法 とは
    - PHPStan(Larastan) との関係
    - まとめ

    View Slide

  6. - PHPDocのアノテーションの一つ
    - ArrayShapesは `ArrayShapes記法` と呼ばれたりもします
    - Psalmだと `Object-like arrays`
    - ジェネリクス記法は `旧PSR-5ジェネリクス記法` と言うらしいです
    - きちんと表記したほうが良さげだけど、長いのでジェネリクス記法とします
    ArrayShapes/ジェネリクス記法 とは

    View Slide

  7. - PHPDocのアノテーションの一つ
    - ArrayShapesは `ArrayShapes記法` と呼ばれたりもします
    - Psalmだと `Object-like arrays`
    - ジェネリクス記法は `旧PSR-5ジェネリクス記法` と言うらしいです
    - きちんと表記したほうが良さげだけど、長いのでジェネリクス記法とします
    ArrayShapes/ジェネリクス記法 とは
    - 配列構造のブラックボックス化を防げる
    - 主に”PHPer大好き連想配列”に効く
    - 大体、`@param array`, `@return array` だよね
    - 書いておくと補完が効く(PhpStorm)

    View Slide

  8. 論よりコード よくあるやつ
    - 引数 `$users` は `User` がつまったリスト
    - `User[]` とか使っている人も多そう
    - こう書いて補完を効かせたりとかも
    - 返却値は、nameとageを持つ連想配列を保持して
    いる配列
    /**
    * @param array $users
    * @return array
    */
    function activeUsers(array $users): array
    {
    $result = [];
    foreach ($users as $user) {
    if (!$user->isActive) {
    continue;
    }
    $result[] = [
    'name' => $user->name,
    'age' => $user->age,
    ];
    }
    return $result;
    }
    /** @var User $user */
    foreach ($users as $user) {

    View Slide

  9. 論よりコード よくあるやつ
    - 引数 `$users` は `User` がつまったリスト
    - `User[]` とか使っている人も多そう
    - こう書いて補完を効かせたりとかも
    - 返却値は、nameとageを持つ連想配列を保持して
    いる配列
    この関数を使う人
    - この関数を見に行かないと引数と返り値に何
    が入っているかわからない :ぴえん:
    - 補完も効かない :ぴえん:
    /**
    * @param array $users
    * @return array
    */
    function activeUsers(array $users): array
    {
    $result = [];
    foreach ($users as $user) {
    if (!$user->isActive) {
    continue;
    }
    $result[] = [
    'name' => $user->name,
    'age' => $user->age,
    ];
    }
    return $result;
    }
    /** @var User $user */
    foreach ($users as $user) {

    View Slide

  10. これが ArrayShapes/ジェネリクス記法
    /**
    * @param array $users
    * @return array
    */
    論よりコード
    /**
    * @param array $users
    * @return array
    */
    function activeUsers(array $users): array
    {
    $result = [];
    foreach ($users as $user) {
    if (!$user->isActive) {
    continue;
    }
    $result[] = [
    'name' => $user->name,
    'age' => $user->age,
    ];
    }
    return $result;
    }

    View Slide

  11. これが ArrayShapes/ジェネリクス記法
    /**
    * @param array $users
    * @return array
    */
    論よりコード
    /**
    * @param array $users
    * @return array
    */
    function activeUsers(array $users): array
    {
    $result = [];
    foreach ($users as $user) {
    if (!$user->isActive) {
    continue;
    }
    $result[] = [
    'name' => $user->name,
    'age' => $user->age,
    ];
    }
    return $result;
    }
    補完が効く(PhpStorm Ctrl + space)

    View Slide

  12. ArrayShapes をもう少し詳しく
    - array{foo: int, bar: string}
    - array{'foo': int, "bar": string}
    - キー名はクォーテーションで囲まなくてよい
    - array{foo: int, bar: string|array}
    - | (合併型)でTypeの羅列可能
    - array{0: int, 1?: int}
    - array{foo?: int, bar: ?string}
    - ? つけるとオプショナル
    - array{int, int}
    - 返却は `[int, int];` になっていないと駄目
    /**
    * @return array{int, int}
    */
    function a(): array
    {
    return [1, 'a']; // NG
    }

    View Slide

  13. ArrayShapes をもう少し詳しく
    - array{foo: int, bar: string}
    - array{'foo': int, "bar": string}
    - キー名はクォーテーションで囲まなくてよい
    - array{foo: int, bar: string|array}
    - | (合併型)でTypeの羅列可能
    - array{0: int, 1?: int}
    - array{foo?: int, bar: ?string}
    - ? つけるとオプショナル
    - array{int, int}
    - 返却は `[int, int];` になっていないと駄目
    /**
    * @return array{int, int}
    */
    function a(): array
    {
    return [1, 'a']; // NG
    }
    &(交差型) も

    View Slide

  14. ジェネリクス記法 をもう少し詳しく
    リスト, マップ
    - Type[]
    - array
    - Type[] と同義
    - User[], int[], etc,
    - array
    - keyがstring
    - Typeは | で羅列可能
    - もちろん、& も
    - non-empty-array
    イテレータ
    - Iterator
    - ArrayIterator
    - Collection
    - Collection

    View Slide

  15. ArrayShapes/ジェネリクス記法 をもう少し詳しく
    さっきの例で言うと、
    `array`
    は、`array` にあてはまる。
    - TKey
    - int
    - Type
    - array{name: string, age: int}
    - ここが ArrayShapes
    - Lists in ArrayShapes
    arrayのところは内部構造を書く。
    /**
    * @param array $users
    * @return array
    */

    View Slide

  16. ArrayShapes/ジェネリクス記法 メリデメ
    メリット
    - 配列構造のブラックボックス化を防げる
    - 補完が効く(PhpStormが強い)
    - なんかそれっぽい

    View Slide

  17. ArrayShapes/ジェネリクス記法 メリデメ
    メリット
    - 配列構造のブラックボックス化を防げる
    - 補完が効く(PhpStormが強い)
    - なんかそれっぽい
    デメリット
    - とりあえずめんどくさい
    - 誰がチェックするの?

    View Slide

  18. ArrayShapes/ジェネリクス記法 メリデメ
    メリット
    - 配列構造のブラックボックス化を防げる
    - 補完が効く(PhpStormが強い)
    - なんかそれっぽい
    デメリット
    - とりあえずめんどくさい
    - 誰がチェックするの?
    - そこで PHPStan(Larastan)

    View Slide

  19. あじぇんだ
    - はじめに
    - ArrayShapes/ジェネリクス記法 とは
    - PHPStan(Larastan) との関係
    - まとめ

    View Slide

  20. PHPStan(Larastan)
    ● こいつは レベル`6` から牙をむき始める
    ● なので牙をむかす
    phpstan.neon
    parameters:
    level: 6

    View Slide

  21. PHPStan(Larastan) レベル
    レベル0 基本的なチェック、未知のクラス、未知の関数、 $this上で呼び出された未知のメソッド、
    それらのメソッドや関数に渡された引数の数が間違っている、常に未定義の変数を
    チェック
    レベル1 未定義の変数、__call と __get を持つクラスの未知のマジックメソッドとプロパティがあ
    る可能性がある
    レベル2 ($this だけでなく)すべての式で未知のメソッドをチェックし、 PHPDocs を検証する
    レベル3 戻り値の型、プロパティに割り当てられた型の確認
    レベル4 基本的なデッドコードチェック - instanceofやその他の型チェックが常に false、到達しな
    いelse文、return後の到達不能コードなど

    View Slide

  22. PHPStan(Larastan) レベル
    レベル5 メソッドや関数に渡される引数の型チェック
    レベル6 タイプヒントの欠落を報告する
    レベル7 部分的に間違っている論理和型の報告 - 論理和型の一部の型にしか存在しないメソッ
    ドを呼び出した場合、レベル 7はそのことを報告し始めます (その他の不正確な状況も )
    レベル8 null可能な型に対するメソッド呼び出しとプロパティへのアクセスを報告する
    レベル9 (max) 混合型に厳密であること - この型で唯一許される操作は、この型を別の混合型に渡す
    ことである
    Rule Levels | PHPStan

    View Slide

  23. PHPStan(Larastan) レベル5 と 6
    サンプル
    - https://phpstan.org/r/fbf3e824-1633-4eb1-9a29-481aadeda5f2
    6にすると以下のエラーが出る
    - プロパティ ※サンプルにはなし
    - `Property xxx type has no value type specified in iterable type array.`
    - 引数
    - `Method xxx has parameter $xxx with no value type specified in iterable type array.`
    - 返り値
    - `Method xxx has parameter $xxx return type has no value type specified in iterable type array.`
    ArrayShapes/ジェネリクス記法 を使ってfix
    - https://phpstan.org/r/d3b09015-69f9-418a-8126-a326b09250f8

    View Slide

  24. PHPStan(Larastan) レベル5 と 6
    - レベル5では array でおkだったのが、レベル6で怒られる
    - レベル6だと array の中身が求められて、レベル5が効いてくる感じ
    - (6で細かく書くがゆえに、 5でうるさく言われる)
    - リストだったり、シンプルな連想配列であればそこまで面倒じゃない

    View Slide

  25. PHPStan(Larastan) レベル5 と 6
    - 中身がやんちゃな連想配列だと直すのがツラミ
    - https://phpstan.org/r/abcae777-2431-48f1-8153-774e66f0827c
    - 適用するのが超絶ダルい
    - あともう一つ、レベル6にするとよく出るエラー
    - `Method xxx() has no return type specified.`
    - 返り値が書いていないやつ
    - 特にテストコードの `: void`
    - 忘れがち

    View Slide

  26. PHPStan(Larastan) レベル5 と 6
    中身がやんちゃな連想配列について

    View Slide

  27. PHPStan(Larastan) レベル5 と 6
    中身がやんちゃな連想配列について
    - がんばる
    - メンテナンスコストも含めて

    View Slide

  28. PHPStan(Larastan) レベル5 と 6
    中身がやんちゃな連想配列について
    - がんばる
    - メンテナンスコストも含めて
    - あきらめる
    - parameters:
    level: 6
    checkMissingIterableValueType: false
    - checkMissingIterableValueType
    - trueだと、`xxx no value type specified in iterable type array.` の解析が走る
    - 設定しない場合、デフォルトは true
    - baselineを使って、いったん検知対象外にする方法もある

    View Slide

  29. PHPStan(Larastan) レベル5 と 6
    中身がやんちゃな連想配列について
    - 無視する
    - `// @phpstan-ignore-line` とか `// @phpstan-ignore-next-line` を使う
    - 引数のところとか、return するところ
    - https://phpstan.org/r/c4ba0317-687b-43d3-b6d3-e7ff20d0090e
    - ignoreErrors(phpstan.neon) を使うのももちろんあり

    View Slide

  30. PHPStan(Larastan) レベル5 と 6
    中身がやんちゃな連想配列について
    - 無視する
    - `// @phpstan-ignore-line` とか `// @phpstan-ignore-next-line` を使う
    - 引数のところとか、return するところ
    - https://phpstan.org/r/c4ba0317-687b-43d3-b6d3-e7ff20d0090e
    - ignoreErrors(phpstan.neon) を使うのももちろんあり
    - array
    - みんな大好き
    - Typeのところに適用可能
    - https://phpstan.org/r/ae621f91-f82f-4f62-b565-97c4711921d0

    View Slide

  31. PHPStan(Larastan) レベル5 と 6
    中身がやんちゃな連想配列について
    - 役割/責務を考えてクラス化する
    - 引数用のクラス、返却用のクラスなど
    - ツラミはあるかもしれないが、痛みなくして改革なし (No Pain, No Gain)
    - なんでもかんでもクラス化すればいいものじゃないので、適材適所ではありますが
    - 少なくとも、array は負けな気がする

    View Slide

  32. PHPStan(Larastan) の罠
    PHPStan最高!! と思いますが、少し罠があります。

    View Slide

  33. PHPStan(Larastan) の罠
    /**
    * @return array
    */
    function state1(): array
    {
    return [
    'name' => 'name',
    'age' => 17,
    'isActive' => true,
    ];
    }
    /**
    * @return array{name: string, age: int, isActive: bool}
    */
    function state2(): array
    {
    return [
    'name' => 'name',
    'age' => 17,
    'isActive' => true,
    ];
    }

    View Slide

  34. PHPStan(Larastan) の罠
    /**
    * @return array
    */
    function state1(): array
    {
    return [
    'name' => 'name',
    'age' => 17,
    'isActive' => true,
    ];
    }
    /**
    * @return array{name: string, age: int, isActive: bool}
    */
    function state2(): array
    {
    return [
    'name' => 'name',
    'age' => 17,
    'isActive' => true,
    ];
    }
    /**
    * @return array // 怒られない(まぁ分かる)
    */
    function state3(): array
    {
    return [
    'name' => 'name',
    'age' => 17,
    'isActive' => true,
    'sex' => 1,
    ];
    }
    /**
    * @return array{name: string, age: int, isActive: bool} // 怒られない!?
    */
    function state4(): array
    {
    return [
    'name' => 'name',
    'age' => 17,
    'isActive' => true,
    'sex' => 1,
    ];
    }

    View Slide

  35. PHPStan(Larastan) の罠
    /**
    * @return array // 怒られる
    */
    function state5(): array
    {
    return [
    'name' => 'name',
    'age' => 17,
    'isActive' => true,
    'hobby' => [],
    ];
    }
    /**
    * @return array{name: string, age: int, isActive: bool} // 怒られない!?
    */
    function state6(): array
    {
    return [
    'name' => 'name',
    'age' => 17,
    'isActive' => true,
    'hobby' => [],
    ];
    }
    /**
    * @return array // 怒られない(まぁ分かる)
    */
    function state7(): array
    {
    return [
    'name' => 'name',
    'age' => 17,
    'activated' => true,
    ];
    }
    /**
    * @return array{name: string, age: int, isActive: bool} // 怒られる
    */
    function state8(): array
    {
    return [
    'name' => 'name',
    'age' => 17,
    'activated' => true,
    ];
    }
    https://phpstan.org/r/0d35eccf-d02f-41fd-99a1-5cb5432e4525

    View Slide

  36. PHPStan(Larastan) の罠
    PHPStan最高!! と思いますが、少し罠があります。
    - ArrayShapesの場合
    - 中身を見る条件が、含まれていればおk (つまり、or)
    - ジェネリクス記法の場合
    - キー名が変わったことや、増減した場合に気づけない
    新規/修正で増減した `Key-Value` に気づけないケースもあることに注意が必要そうです。(解
    決方法が分かる人、教えて下さい)
    なので書き方のルールは決めておいたほうがよさげ && UTで担保しよう!!

    View Slide

  37. PHPStan(Larastan) の罠
    PHPStan最高!! と思いますが、少し罠があります。
    - ArrayShapesの場合
    - 中身を見る条件が、含まれていればおk (つまり、or)
    - ジェネリクス記法の場合
    - キー名が変わったことや、増減した場合に気づけない
    新規/修正で増減した `Key-Value` に気づけないケースもあることに注意が必要そうです。(解
    決方法が分かる人、教えて下さい)
    なので書き方のルールは決めておいたほうがよさげ && UTで担保しよう!!
    専用クラスを用意 → そのクラス、そのクラスの配列(List)を使うのがよさそう。

    View Slide

  38. あじぇんだ
    - はじめに
    - ArrayShapes/ジェネリクス記法 とは
    - PHPStan(Larastan) との関係
    - まとめ

    View Slide

  39. まとめ
    - ArrayShapes/ジェネリクス記法 便利
    - これらを知っておくとPHPStan(Larastan)のレベル上げに対応可能
    - ただし、PHPStan(Larastan)には罠がありそう
    - レベル7からも、ちょっと厳しくなります
    - ルールを知っていればなんとかなる

    View Slide

  40. まとめ
    - 静的解析が通っているから安心しない
    - 目的は、読みやすい/修正しやすいコードを書くことのはず
    - やんちゃな配列でも静的解析が通っているからおkと思わない
    - 静的解析をきっかけとして、よりよい設計
    を考えられるといいですね

    View Slide

  41. まとめ
    - 静的解析が通っているから安心しない
    - 目的は、読みやすい/修正しやすいコードを書くことのはず
    - やんちゃな配列でも静的解析が通っているからおkと思わない
    - 静的解析をきっかけとして、よりよい設計
    を考えられるといいですね
    - カオナビでもCIで回してきちんとチェックしています
    - 現在レベル0ですが、レベルアップに向けて鋭意対応中!!

    View Slide

  42. 参考
    - ArrayShapes
    ○ https://phpstan.org/writing-php-code/phpdoc-types#array-shapes
    ○ https://psalm.dev/docs/annotating_code/type_syntax/array_types/#object-like-arrays
    - ジェネリクス記法
    ○ https://phpstan.org/writing-php-code/phpdoc-types#general-arrays
    ○ https://psalm.dev/docs/annotating_code/type_syntax/array_types/#generic-arrays
    - Rule Levels | PHPStan
    - Solving PHPStan error "No value type specified in iterable type" | PHPStan
    - The Baseline | PHPStan
    - Ignoring Errors | PHPStan
    - Larastan v1.0 Released
    - array shapes記法(Object-like arrays)と旧PSR-5記法で型をつける - Qiita

    View Slide

  43. ご清聴ありがとうございました

    View Slide