Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Who am I

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

論よりコード よくあるやつ - 引数 `$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) {

Slide 9

Slide 9 text

論よりコード よくあるやつ - 引数 `$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) {

Slide 10

Slide 10 text

これが 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; }

Slide 11

Slide 11 text

これが 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)

Slide 12

Slide 12 text

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 }

Slide 13

Slide 13 text

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 } &(交差型) も

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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, ]; }

Slide 34

Slide 34 text

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, ]; }

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

参考 - 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

Slide 43

Slide 43 text

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