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

More Decks by shimabox

Other Decks in Programming

Transcript

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. 論よりコード よくあるやつ
    - 引数 `$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 full-size 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 full-size slide

  9. これが 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 full-size 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;
    }
    補完が効く(PhpStorm Ctrl + space)

    View full-size slide

  11. 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 full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. 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 full-size 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) を使うのももちろんあり
    - array
    - みんな大好き
    - Typeのところに適用可能
    - https://phpstan.org/r/ae621f91-f82f-4f62-b565-97c4711921d0

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  32. 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 full-size 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,
    ];
    }
    /**
    * @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 full-size slide

  34. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  41. 参考
    - 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 full-size slide

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

    View full-size slide