$30 off During Our Annual Pro Sale. View Details »

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を書く際に、クラスのプロパティ, 引数, 返り値が配列の場合、 @var array, @param array, @return array などと書くことが多いと思います。

    が、これだと配列の中身が mixed と言っているのと一緒です。 配列の中身がわかっているなら、ちゃんと定義しよう! 定義する方法として、ArrayShapes/ジェネリクス記法※ というのがあるよ。 というのが今回のお話です。 ※ 引用元はスライドの後半に用意 はじめに
  2. - PHPDocのアノテーションの一つ - ArrayShapesは `ArrayShapes記法` と呼ばれたりもします - Psalmだと `Object-like arrays`

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

    - ジェネリクス記法は `旧PSR-5ジェネリクス記法` と言うらしいです - きちんと表記したほうが良さげだけど、長いのでジェネリクス記法とします ArrayShapes/ジェネリクス記法 とは - 配列構造のブラックボックス化を防げる - 主に”PHPer大好き連想配列”に効く - 大体、`@param array`, `@return array` だよね - 書いておくと補完が効く(PhpStorm)
  4. 論よりコード よくあるやつ - 引数 `$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) {
  5. 論よりコード よくあるやつ - 引数 `$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) {
  6. これが ArrayShapes/ジェネリクス記法 /** * @param array<int, User> $users * @return

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

    array<int, array{name: string, age: int}> */ 論よりコード /** * @param array<int, User> $users * @return array<int, array{name: string, age: int}> */ 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)
  8. 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 }
  9. 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 } &(交差型) も
  10. ジェネリクス記法 をもう少し詳しく リスト, マップ - Type[] - array<Type> - Type[]

    と同義 - User[], int[], etc, - array<TKey, Type> - keyがstring - Typeは | で羅列可能 - もちろん、& も - non-empty-array<TKey, Type> イテレータ - Iterator<Type> - ArrayIterator<TKey, Type> - Collection<Type> - Collection<TKey, Type>
  11. ArrayShapes/ジェネリクス記法 をもう少し詳しく さっきの例で言うと、 `array<int, array{name: string, age: int}>` は、`array<TKey, Type>`

    にあてはまる。 - TKey - int - Type - array{name: string, age: int} - ここが ArrayShapes - Lists in ArrayShapes arrayのところは内部構造を書く。 /** * @param array<int, User> $users * @return array<int, array{name: string, age: int}> */
  12. PHPStan(Larastan) レベル レベル0 基本的なチェック、未知のクラス、未知の関数、 $this上で呼び出された未知のメソッド、 それらのメソッドや関数に渡された引数の数が間違っている、常に未定義の変数を チェック レベル1 未定義の変数、__call と

    __get を持つクラスの未知のマジックメソッドとプロパティがあ る可能性がある レベル2 ($this だけでなく)すべての式で未知のメソッドをチェックし、 PHPDocs を検証する レベル3 戻り値の型、プロパティに割り当てられた型の確認 レベル4 基本的なデッドコードチェック - instanceofやその他の型チェックが常に false、到達しな いelse文、return後の到達不能コードなど
  13. PHPStan(Larastan) レベル レベル5 メソッドや関数に渡される引数の型チェック レベル6 タイプヒントの欠落を報告する レベル7 部分的に間違っている論理和型の報告 - 論理和型の一部の型にしか存在しないメソッ

    ドを呼び出した場合、レベル 7はそのことを報告し始めます (その他の不正確な状況も ) レベル8 null可能な型に対するメソッド呼び出しとプロパティへのアクセスを報告する レベル9 (max) 混合型に厳密であること - この型で唯一許される操作は、この型を別の混合型に渡す ことである Rule Levels | PHPStan
  14. 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
  15. PHPStan(Larastan) レベル5 と 6 - レベル5では array でおkだったのが、レベル6で怒られる - レベル6だと

    array の中身が求められて、レベル5が効いてくる感じ - (6で細かく書くがゆえに、 5でうるさく言われる) - リストだったり、シンプルな連想配列であればそこまで面倒じゃない
  16. PHPStan(Larastan) レベル5 と 6 - 中身がやんちゃな連想配列だと直すのがツラミ - https://phpstan.org/r/abcae777-2431-48f1-8153-774e66f0827c - 適用するのが超絶ダルい

    - あともう一つ、レベル6にするとよく出るエラー - `Method xxx() has no return type specified.` - 返り値が書いていないやつ - 特にテストコードの `: void` - 忘れがち
  17. PHPStan(Larastan) レベル5 と 6 中身がやんちゃな連想配列について - がんばる - メンテナンスコストも含めて -

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

    とか `// @phpstan-ignore-next-line` を使う - 引数のところとか、return するところ - https://phpstan.org/r/c4ba0317-687b-43d3-b6d3-e7ff20d0090e - ignoreErrors(phpstan.neon) を使うのももちろんあり
  19. 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<mixed> - みんな大好き - Typeのところに適用可能 - https://phpstan.org/r/ae621f91-f82f-4f62-b565-97c4711921d0
  20. PHPStan(Larastan) レベル5 と 6 中身がやんちゃな連想配列について - 役割/責務を考えてクラス化する - 引数用のクラス、返却用のクラスなど -

    ツラミはあるかもしれないが、痛みなくして改革なし (No Pain, No Gain) - なんでもかんでもクラス化すればいいものじゃないので、適材適所ではありますが - 少なくとも、array<mixed> は負けな気がする
  21. PHPStan(Larastan) の罠 /** * @return array<string, string|int|bool> */ 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, ]; }
  22. PHPStan(Larastan) の罠 /** * @return array<string, string|int|bool> */ 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<string, string|int|bool> // 怒られない(まぁ分かる) */ 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, ]; }
  23. PHPStan(Larastan) の罠 /** * @return array<string, string|int|bool> // 怒られる */

    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<string, string|int|bool> // 怒られない(まぁ分かる) */ 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
  24. PHPStan(Larastan) の罠 PHPStan最高!! と思いますが、少し罠があります。 - ArrayShapesの場合 - 中身を見る条件が、含まれていればおk (つまり、or) -

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

    ジェネリクス記法の場合 - キー名が変わったことや、増減した場合に気づけない 新規/修正で増減した `Key-Value` に気づけないケースもあることに注意が必要そうです。(解 決方法が分かる人、教えて下さい) なので書き方のルールは決めておいたほうがよさげ && UTで担保しよう!! 専用クラスを用意 → そのクラス、そのクラスの配列(List)を使うのがよさそう。
  26. 参考 - 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