【PHPDoc】PHPのarray型/Collection型をもっとわかりやすく!
はじめに
こんにちは。kouです。
前回の記事を書いてから2年が経ちました。🤔
現在自分が開発に携わっている マナリンク では、バックエンドにLaravelを使用しています。
今年に入ってから、PHPStan(PHPの静的解析ツール)が導入されることとなり、現在はレベル6で運用に載っています。
PHPStanのレベルが6に上がったことを受けて、型宣言周りをより詳細に書く必要が出てきました。
PHPでは、配列の型を言語仕様レベルではarray
型としか書くことができず、それが数値の配列なのか、オブジェクトの配列なのか、はたまた連想配列なのかをこの型自体から読み取ることは難しいため、「この引数(あるいは返り値)のarray
型は何が来るんだ?」という思いを抱いたことがある方は多いかと思います。
例に漏れず自分もその一人であり、今後PHPStanのレベル6(もしかしたらそれ以上)の中で開発を進めていくに当たって、如何にしてarray
型に詳細な型付けをするのか という部分を一度整理しておきたいと思い、今回記事を書くことにしました(タイトルにもある通り、詳細な型付けには PHPDoc を用います)。
またLaravelでの開発を行なうにあたって、欠かすことのできないCollection
についてもarray
型と同様のことが言えるため、記事後半ではCollection
型への詳細な型付けについても取り上げています。
同じような思いを抱いている方の参考になれば幸いです。
(2年前の記事でも型定義周りについて書いていたような…)
余談:TypeScriptと比べてのarray型の煩わしさ
余談ですが、自分は元々、JavaScriptの静的型付け言語であるTypeScriptを先に学んだ状態で、後からPHPの型システムを知ったので、PHPのarray
型を見たとき、とても煩わしい気持ちであったことを記憶しています。
注意書き(PHPDocの配列型の書き方について)
本題に入る前に、PHPDocで配列型を表すときの書き方が2通りあるので軽く紹介します。
※以下で「〇〇キーワード」というような呼び方をしていますが、一般的な呼び方が分からなかったため便宜的にこのような呼び方をしています。
-
array
キーワード(array<T>
と書く。※Tは任意の型)
/**
* `array`キーワードを用いるパターン
* @param array<int> $arr 数値の配列
*/
public function sampleMethod(array $arr): void
-
[]
キーワード(T[]
と書く。※Tは任意の型)
/**
* `[]`キーワードを用いるパターン
* @param int[] $arr 数値の配列
*/
public function sampleMethod(array $arr): void
以降の記事では、基本的には[]
キーワードの方を用いて、PHPDocにおける配列の詳細な型定義について説明します。
但し、PHPの連想配列についてはarray
キーワードを用いることでしか詳細な型を書くことができないようなので、連想配列のみarray
キーワードを用いて説明します。
逆に言えば、基本的に[]
キーワードを使って配列ということを示しておくことで、「array
を使っているものは連想配列である」というメンタルモデルを形成することができるメリットがあるかもしれません。
PHPDocを用いた配列型の詳細な型付け
では、本題に入っていきます。
array型の詳細な型付け その1(プリミティブ値の配列)
まずは基本的な文字列や数値を要素に含んだ配列のPHPDocの書き方です。
特に解説することもないので、書き方だけ示すに留めておきます。
書き方
/**
* @param int[] $numArr 数値の配列
*
* @return string[] 文字列の配列
*/
public function sampleMethod(array $numArr): array
{
}
array型の詳細な型付け その2(オブジェクトの配列)
PHPにおいて「オブジェクト」とは、クラスのインスタンスを指すので、ここでは「クラスのインスタンスの配列」と書いたほうが伝わりやすいかもしれません。
以下の例では、関数の引数として、Userクラスのインスタンスの配列が渡ってくることを示しています。
書き方
/**
* @param User[] $userArr Userオブジェクトの配列
*/
public function sampleMethod(array $userArr)
{
}
array型の詳細な型付け その3(連想配列)
連想配列に対する詳細な型付け方法です。
PHPDocでの配列の詳細な型付け方法の中でも、個人的なハマりポイントはここでした。
注意書きで述べた通り、連想配列の型定義にはarray
キーワードを用いる必要があります。
連想配列は、array
キーワードを用いて配列であることを示したあと、連想配列の中身である部分を{ key名: 型 }
の形で定義します。波括弧({}
)を用いて中身を表す点がポイントですね。
書き方
/**
* 連想配列は`array`キーワードを用いて型付けをする
*
* @return array{ name: string, age: int }
*/
public function sampleMethod(): array
{
return ['name' => 'aiko', 'age' => 47];
}
array型の詳細な型付け その4(連想配列の配列)
連想配列の配列の書き方について説明します。ここも個人的ハマりポイントでした。
連想配列の配列とは以下のようなものを指します。
[
[key1 => 'value1', key2 => 'value2'],
[key3 => 'value3', key4 => 'value4'],
[key5 => 'value5', key6 => 'value6'],
...
]
連想配列への型付けの項目で説明した通り、連想配列はarray
キーワードを用いて詳細な型を示す(ex. array{ name: string, age: int }
)ので、その連想配列の"配列"ということで、[]
キーワードと組み合わせて以下のように書くことができます。
書き方
/**
* `array{...}`で連想配列を示し、`T[]`で配列で表す
*
* @return array{ name: string, age: int }[]
*/
public function sampleMethod(): array
{
return [
['name' => 'aiko', 'age' => 47],
['name' => 'kou', 'age' => 27],
];
}
ただ、説明しておいてあれですが、この「連想配列の配列」の書き方はぱっと見でわかりにくい(と個人的に感じる)ので、できれば連想配列部分はクラスとして切り出すなどするほうがより見やすそうです。
余談:arrayキーワードで配列を示す方法だとよりカオスに
この記事では、配列の型定義の際に[]
キーワードを用いて説明するようにしていますが、勿論array
キーワードを用いて配列を表すこともできます。
連想配列の配列を、array
キーワードを用いて配列を表すようにすると以下のようになります。
/**
* `array{...}`で連想配列を示し、`array<T>`で配列で表す
*
* @return array<array{ name: string, age: int }>
*/
public function sampleMethod(): array
{
return [
['name' => 'aiko', 'age' => 47],
['name' => 'kou', 'age' => 27],
];
}
わ、わかりにくい…。
「1つ目のarray
と2つ目のarray
の何が違うんだ?」と初見でとても混乱したことをよく覚えています。
一度慣れてしまえばなのかもしれないですが、慣れを要するぐらいであれば、最初から"まだ"わかりやすい[]
キーワード方式で配列であることを表現したほうが読解はしやすいように思います。
しかし、上でも述べているように、「連想配列の配列」の「連想配列」部分をクラスとして切り出せる場合であれば、クラスとして切り出したほうが読解のしやすさはより向上するかと思われます。
Collection型の詳細な型付け
最後に、Laravelでの開発をする上では欠かすことのできないCollection
の詳細な型付けについて説明します。
このCollection
もarray
型同様、型宣言上ではCollection
型としてしか宣言することができず、このコレクションが一体何のコレクションであるかを確認するためには、いちいち実装詳細まで確認しにいかなければなりません。
そんなCollection
型についても、PHPDocを用いることで詳細な型付けをすることができます。
Collection
型に詳細な型付けをする場合、Collection<T>
の形で書くことができます(array<T>
の書き方と同じですね。T[]
の書き方はできないので注意)。
書き方
/**
* @param Collection<int> $userIds 数値のコレクション
*
* @return Collection<User> Userオブジェクトのコレクション
*/
public function sampleMethod(Collection $userIds): Collection
{
}
素朴な疑問コラム:型のためだけにuse(=import)を増やすべきか
上記例として、返り値にCollection<User>
という書き方をしましたが、この書き方はCollection
及びUser
をuseしている場合の書き方になります。
useしない場合は以下のように書くことになります。
/**
* @param \Illuminate\Support\Collection<int> $userIds
*
* @return \Illuminate\Database\Eloquent\Collection<\App\Models\User> Userオブジェクトのコレクション
*/
public function sampleMethod(Collection $userIds): Collection
{
}
ここで考えるべきは、「型宣言のためだけにuseするのはどうなのか?」 という部分だと思います。
ここは意見の分かれるところだと思っていて、自分としてもまだどちらがいいのかの結論は出ていません。
判断材料となる観点は幾つか考えられ、それぞれについてどこまで重要であるかが決める際のポイントになってくると思われます。
考える際の観点
- PHPDocの見やすさ・使う側での視認性
- 型宣言の詳細度
- 不必要な依存
「PHPDocの見やすさ・使う側での視認性」という観点で考えると、useする方がより見やすさの観点ではメリットがあるように思われます。
以下はuseした場合と、useしない場合で、使う側でのメソッドのホバー時にどのような違いがあるかを示した画像です。
- useした場合
- useしない場合
画像の通り、useする場合の方が、ぱっと見の視認性は高いように感じます。
「型宣言の詳細度」と「不必要な依存を減らす」という観点で考える場合、useせずに直接書いたほうがメリットがあるように思われます。
「PHPDocの見やすさ・使う側での視認性」の観点で比較した画像をもう一度見てみると、useする方では引数も返り値もCollection
ということしか分からず、返り値Collectionの詳細はUser
としか分かりません。
この場合、使う側は、実装詳細を見ずには、Collection
型が\Illuminate\Support\Collection
型か、\Illuminate\Database\Eloquent\Collection
型かの判別が付きません。
また同様に、User
クラスに関しても、独自にUser
エンティティクラスを作成している場合、User
エンティティであるのか、User
モデルであるのか、はたまた別のUser
クラスであるのかの判別が付きません。
上記は、「型宣言の詳細度」の観点から見た意見でしたが、「不必要な依存」という観点からも、あくまで型宣言のためだけにuseすると、そのクラスのuseをざっと見た際に「あれ、このクラスにも直接依存しているのかな?」という印象を与えることにも繋がる可能性があります。
というわけで色々述べましたが、個人的にはまだ結論のできっていない部分であり、これを機に今後社内でも話し合ってみたいテーマだなと思いました。
おわりに
この記事では、PHPのarray
型・LaravelのCollection
型にPHPDocを用いてどのように詳細な型付けをするかについて説明しました。
PHPDocのarray型やCollection型周りの書き方で分からないところがある方の参考になったら嬉しいです。
今後、言語レベルでのarray
型の詳細な型指定ができるようになるとより嬉しいですね。
参考
オンライン家庭教師マナリンクを運営するスタートアップNoSchoolのテックブログです。 manalink.jp/ 創業以来年次200%前後で売上成長しつつ、技術面・組織面での課題に日々向き合っています。 カジュアル面談はこちら! forms.gle/fGAk3vDqKv4Dg2MN7
Discussion