RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

PHPerのための「PHPと型定義」を語り合う【PHP TechCafe イベントレポート】

弊社で毎月開催し、PHPエンジニアの間で好評いただいているPHP TechCafe。
2023年5月のイベントでは「型定義」について語り合いました。
弊社のメンバーが事前にまとめてきた情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。
今回はその内容についてレポートします。

rakus.connpass.com

PHPと型

静的型付け言語

変数や関数の引数、戻り値などに対してあらかじめ指定された型以外を使用できなくする言語です。例としてはJavaがあります。
入力や出力を縛る言語です。型推論という機能で、明示的に型を指定しなくても文脈などからコンパイラが予測してくれるというパターンもあります。

動的型付け言語

変数や関数の引数、戻り値の型を実行時の値によって決定することが出来る言語です。PHPはこちらに分類されます。
ただPHPのように型を指定できる動的型付け言語もあります。

一般的な誤解

コンパイラ型は静的型付け言語である」や「インタープリター型は動的型付け言語である」というのは間違いです。
静的型付け言語はコンパイル時に変数や関数の型が確定されます。一方、動的型付け言語では実行時に型の決定を行うことができます。
PHPは型宣言できるので定義上は静的型付け言語だと言えてしまう。値の持ち方は動的型付け言語そのものなんですが」というコメントもありました。

PHPの型

単一の式が持つ型

  • null
  • bool
  • int
  • float
  • string
  • array
  • object
  • callable
  • resource

型システムで扱える型

  • 組み込みの型

    • ヌル(null)
    • スカラー型:
    • 配列(array)
    • オブジェクト(object)
    • リソース(resource)
    • never
    • void
    • クラス内での関係を示す相対型: self, parent, static
  • リテラル

    • false
    • true
  • ユーザー定義型(一般的に、クラス型とも呼びます)

    • インターフェース
    • クラス
    • 列挙型(Enum
  • callable

never型について

neverは8.1以降で使えるようになった、関数が戻ってこないことを示す戻り値の型です。
exitがコールされるか、例外がスローされるか、無限ループに入るかのいずれかであることを意味します。
使い方としてはneverや@return neverで書いた関数の直後のコードが絶対実行されないとPHPStanが警告してくれるのでどこかでexitしてしまうようなコードに「exitするから気をつけろよ」ということを示すことができます。
クラスにしたときに「この関数を呼んだら終わります」と明記できるという意見がありました。

void型について

関数が値を返さないことを示す型です。
使いどころとして以下のような意見、コメントがありました。
「voidは重要、副作用があることを暗示してくれる」
「事故防止のため、コードに密結合したところに付箋を貼れる意味がある」
「voidが呼ばれていると中でなにかやっているぞ、なにか更新しているぞってなりますよね」
「純粋なコードとそうじゃないものを色分けできる」

self,parent,static型について

self,parent,staticも型にあたります。返り値の型に宣言することができます。
「よく自分自身を返すときには使います」という意見がありました。

resource型について

resource型は型宣言には記述できません。

evalでresource型を宣言すると

関数のパラメータにresource型を宣言するとバージョン8.0以降では「サポートされている組み込み型ではないため、クラス名として解釈されます。」というWarningが出ます。
「PG系の関数やファイル系の拡張関数がresourceではなく新しいクラスを返すようになる変更があり、元々resource型だったため型宣言が上手く出来ないからという経緯があるのでは」という意見がありました。
またバージョンによってエラー内容は異なります。

リテラル型について

false型とtrue型です。falseが返ることを宣言できます。
8.0.0でfalse型、8.2.0でtrue型がサポートされました。
8.2.0以前では、false型はunion型の一部としてのみ使える型でした。
以下の意見がありました。
「false型が出てきた背景は、PHPの標準関数のなかにfalseを返すものが多かったため」
「trueは一応成功してくれればそのまま、失敗はエラー処理にできる」
「例外を使えという話ではあるが、レガシー向け対応」

ユーザー定義型について

一般的にクラス型と呼ばれます。Enumもここに含まれます。

複合型について

交差型とUnion型。
交差型を構成する個別の型は & でつなぎます。Unionは | でつなぎます。

以下の意見がありました。
「Union型は引数や戻り値で指定する時によく使うかなと思います。」
「交差型を使う例として、インターフェースでイベントを受け取るんですが、実装しているどのイベントなのかっていうのをメソッドの中でチェックしなければいけない……という時に、&で繋いでいけばメソッドの中でチェックする処理を書かずに済むというやり方もあります。」
「型を指定しておけば、与えられた引数のチェックをしなくてよくなるっていうのがいいですね」

&で繋ぐ書き方の例としてはこちらの記事が参考になります。
doganoo.medium.com

型のエイリアス

mixed

mixedはobject | resource | array | string | float | int | bool | nullのエイリアスです。
五年前くらいに会社のチャットで「mixedって意味あんのかよ、レガシーをそのまま放置しているだけじゃないか」という意見があり、 「いやいやラベル付けのためにはいるんじゃない?」や「どうしてもここはmixedにするしかなかったって意思表示ができる」 という話をしていました。

以下の意見・コメントがありました。
「ここにresource入るんですね。エイリアスと言いつつ型宣言出来ないじゃん!」
「ここは分からないからmixedにする、頑張ったけどって意味づけも出来るかなと」
「型がなかったプロジェクトにあとから型をつける場合に、mixedに逃げる形になっています」
「どうしようもなかったってラベリングには使えそうですね」
「is_mixed()それはTrueしか返らない」

iterable

Traversable | arrayのエイリアスです。
foreachで回せます。

PHPで取り入れられた型表現

そもそもですがPHPは元々型表現があってないような動的なものでしたが、徐々に指定できるように取り入れられました。
例えば戻り値に指定しますよとか、引数に宣言できるようになりましたよというところ。

7.1.0まで遡ると、nullableという型が追加、voidが追加と徐々に増えています。
8.0.0でUnionとmixedが出来るようになりました。PHPerKaigiでmixedステッカーがありましたね。
8.1.0で戻り値にのみ指定できる型として、never型が追加されました。そして戻り値をvoidとした関数からリファレンスを返すことが非推奨になりました。交差型が追加されました。
8.2.0でnullとfalse型が独立して使えるようになり、trueが追加されました。DNF型も追加されました。DNF型とはANDとORの組み合わせを使えるものです。

以下の意見がありました。
「unionがなかったのでmixedで書かれているものがあった」
「unionのおかげでmixedってどうしても書かなきゃいけないシーンは減ったかもしれません」

型宣言のメリット

作った関数が誤った意図で使われることを防げる。
受け取った値のチェックを型宣言に任せられる。
毎回is_arrayでチェックしなくてよい。

PHPの歴史を振り返る

PHPのドキュメントを見ると、Nullの表現が3種類ある

「null」と「Null」と「NULL」とあって、文脈上なんかあるのかなと思ったんですが、英語の文法上、先頭にいると大文字になってそれがそのまま翻訳されていると考えられます。
歴史的経緯と翻訳の都合。

PHPの歴史的な理由

以下のスライドで紹介されています。

www.slideshare.net

implod()の歴史的な理由

inplodeは歴史的な理由により引数をどちらの順番でも受け付けることができます。
PHP2から3の間に生まれ、PHP3.0a3にて、第一引数と第二引数を交換したので、splitと同じように動作するようになりました。
わずか3ヶ月の歴史。

PHPマニュアル 日本語版で「歴史的」を検索したらたくさん出てくる

Code search results · GitHub 検索でヒットしたのは12箇所。
以下の記述がありました。
「歴史的な理由によりfloatの場合は"float"ではなく、"double"が返されます」
「gettypeは歴史的な理由で決まった型の名前を返します。この関数はそれよりも実際に使われている型により近い名前を返すという点が異なります」
皆さん歴史的なものがほしいときはgettype()を、そうでないときはget_debug_type()を使いましょう。

PHPDocの役割

型宣言と切っても切り離せないのがPHPDocです。PHPソースコードに記載するコメントです。
実装者がコードの内容を確認できるだけでなく、ルールに従って記載することでIDEや静的解析などのツールで活用することができます。
下記を参考にしていました。
PHPDoc リファレンス — phpDocumentor

メリット

静的解析に使えること、コメントなので実際の挙動に影響を与えない。
なのでバグにならないことが保証されている。
リファクタリングでとっつきやすい。
以下資料を参考
speakerdeck.com

デメリット

所詮はコメントなので、内容が間違っていても動く。
片方が書かれていない、書いてあるけど型の指定が無い、実際はstring型なのにコメントはint型になっているなど。

以下の意見がありました。
「書かれている内容が間違っている可能性があるのは問題ですね」
PHPを長年愛用されているレガシーの皆様にとっては、それでもIDEで縛れるのがいいのかなと思います」
「サボらずにDocもメンテしましょう」

言語仕様の型表現とPHPDocのどちらを使うべき?

以下のような意見・コメントがありました。
「新規プロダクトと既存プロダクトで変わってくる」
「新規プロダクトでは型宣言は必須。既存ならPHPDoc」
「ビジネス上でスピードを求められるならとりあえず無しで作る戦略はアリなのかな。ところが後々困るのは自分だよな......」
「既存もテストを書いて型宣言しよう」
「手戻りのバグ調査の工数って見えにくいですからね。既存は入出力が空のところが多いからリスクが大きすぎる」
「昔から型は口ほどに物を言うと言いますからね」
「スピードを求めるからこそ型宣言」
「既存のプロダクトに型をつけるのは危険過ぎる。テストがしっかりと出来ているなら問題ないんですが...」
「私は実際プロダクトで型をつけてすっごく変なルートで入ってきて落ちるというのを何度も見ましたし」
「複雑なデータは型を検討していると先が見えてくるのでそういう意味で重要ですね」

この方法ならレガシーにもテストを書けますという記事を共有いただきました。
qiita.com
スクレイピングによるテストです。この方法ならスクレイピングしてこれがあるかどうかというものなら簡単にかけます。

「既存のプロダクトは頑張って先程の方法か、無難にPHPUnitに書くのか、頑張ってテストに型をガシガシ書くのか、PHPDocから攻めるのか」
「テストを書く時間がないときはスピードを求めるからこそ型宣言に立ち返りましょう」

事前にいただいた質問コーナー

never型(noreturn型)のうまい使い所、(一部過激派による)ジェネリクス待望論などあれば聞きたいです

リダイレクトしてしまう場合。レガシーコードのときに付けるなど。

ジェネリクス待望論についてはarray_shapeを使いましょうという結論です。
PhpStormを利用していればPHPDocの中にAttributesで中のコードを書くことで静的解析のように利用できます。

また、言語組み込みのジェンネリクスは実行速度に影響があるため不要であり、PHPDocで書けば十分ということでした。
エディタが警告を出してもらうことで問題はないのでarray_shapeのような静的解析でよいのではないでしょうか。

テストコードは静的解析の対象に入れるべきか否か

静的解析の対象です。入れてもデメリットが無いと思います。
ただし、静的解析ツールからの警告やエラーメッセージに対してイライラしてしまうことがあるかも知れません。 また、データプロバイダ(テストデータを提供するメソッドや機能のこと)には多くの場合、複数のパラメータが必要となり、これらのパラメータに適切な型を指定するためにPHPDocのコメントを書くことが推奨されています。 しかし、これによりPHPUnitのコメントが長大になってしまい逆に読みにくくなるという点はデメリットと言えるかも知れません。

以下のようなコメントがありました。
「たくさんのパラメータが必要なデータプロバイダこそ複雑なことをしている自覚を持ってちゃんとした型を書いてほしい」

型運用のための設計についてお聞きしたいです。(DTOクラスはPHPでも使うものなのかなど)

スピードと質を求めるなら型はしっかり設計しておくべきです。
以下のコメントと意見がありました。

「最近はCopilotである程度いい感じに補完してくれるのでDTOを書きやすくなっている」
「生存期間が短いものは配列でいい、旅をするものはDTOにしてほしい」
「Readonly出来るようになったからDTOしやすくなった」
「製品コードやテストコードで実行する解析やルールを変えることがあるので、個人的には製品コードとテストコードで同等の静的解析をするかどうかが問題」

まとめ

今回はPHPの型について参加者の方々にそれぞれの型の特徴や使い所、歴史的背景などについて語り合っていただきました。
PHPは型指定ができる動的型付け言語という独特な言語であり、長い歴史からくる仕様などの事情が語られていてとても勉強になりました。 PHPの型について理解をより一層深めることができたのではないでしょうか。

PHP TechCafe」では今後もPHPに関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております。

Copyright © RAKUS Co., Ltd. All rights reserved.