🏷️

【PHPDoc】PHPのarray型/Collection型をもっとわかりやすく!

2023/07/02に公開

はじめに

こんにちは。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の詳細な型付けについて説明します。

このCollectionarray型同様、型宣言上では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型の詳細な型指定ができるようになるとより嬉しいですね。

参考

https://zonuexe.github.io/phpDocumentor2-ja/references/phpdoc/index.html


GitHubで編集を提案
マナリンク Tech Blog

Discussion