出戻りPHPerだからわかるPHPの素敵さ

サムネイル

はじめに

みなさん、こんにちは!二次元&イノベーション開発本部 二次元コンテンツ事業/同人開発部の尾崎です。

ここ数年はRustaceanとしてRustを推しています。 Rustいいですよね。なんと言ってもコンパイルを通せばだいたいの事故を防げるので脳への負荷が少ない言語としてとても気に入っています。

そんな私ですが新卒からPHPを使い続け、PHPカンファレンスにも3年に一度の頻度でLTやスピーカーとして登壇していたPHPerでした。 しかし、最後に触ったPHPのバージョンは7.2だし、PHPで登壇もできていないのでもうPHPerとして名乗れない…寂しい!というのでPHPをまた触り始めることにしました。

というわけで、出戻りPHPerから見た今のPHPについて、感想を紹介していきます。

1. 型周りは違和感がない

これはかなり意外でしたが、コードを書いていて特に違和感はありませんでした。

今のPHPでは型の定義方法が2種類あります。

  1. PHPの標準機能を用いた型宣言
  2. PHPStanPsalmといった外部ツールを使ったPHPDocベースの型宣言

1つ目はPHP7のころから徐々に宣言できる場所が増えてきて、現在のバージョン8.3ではほとんどの場所で宣言ができる印象です。(ここでは型を宣言できない、と思った記憶がありません)

また、宣言できる場所だけでなく、使用できる型も増えています。

例として、ユニオンやインターセクション、NULLを許容する型など、他言語で採用されている型を使用できます。 記事の執筆時点ではまだリリースされていませんが、PHP 8.4からはプロパティフックといったより安全なプロパティアクセスを提供する機能も予定されています。

ただ、PHPの標準機能だけでは型としては弱いと感じる瞬間もあります。例えばジェネリクスがないことや、配列や連想配列の型が定義できないことです。

そういった場合、PHPStanやPsalmといった静的解析ツールを使用することで解決できます。 個人的にこれらのツールがサポートしているArray shapes記法は非常に気に入っており、これなしでは開発ができないほどです。

人によっては外部ツールを必要とすることに違和感があるかもしれません。 JavaScriptでFlowtypeやTypeScriptの経験がある私にとっては、動的型付け言語では緩いやり方を選択したいときがあるので、型の強さを選べるというのは便利です。

ただし、外部ツールにも問題がないかというとそうでもありません。標準ライブラリやフレームワークを経由すると、型がmixedになってしまう瞬間があるのは少し面倒です。 もちろんこれにも解決策はあり、PHPStanではStub Filesを定義できます。 しかし、TypeScriptの.d.tsと同様、バージョンアップ時に潜在的なリスクが伴います。

他にも@varを使った型の強制的な上書きも可能ですが、同様にバージョンアップ、変更時などにリスクがあります。

というわけで今のPHPの型ではなかなか良いバランス感があり、触り心地も悪くないです。 高階型に慣れている人には物足りないかもしれないですが、ジェネリクスを扱っている言語からPHPを触る人には悪くないです。

2. マジックメソッドの楽しさ

久しぶりにマジックメソッドを触るととても楽しいです。 マジックメソッドは存在しないプロパティやメソッド呼び出しをするときなど、特定の操作の際に呼び出される特別なメソッド群です。

個人的なお気に入りは__toStringです。 これは例えば(string) $aというように対象を文字列変換した際に呼び出されるメソッドです。 PHP 8以降ではStringableというインターフェースで__toStringの実装を強制できるようになりました。

この__toStringはよく文字列系のValue Objectを作る際に利用しています。

<?php

class MyId implements Stringable
{
    public function __construct(private string $id)
    {
        assert(strlen($id) > 0);
    }

    /**
     * {@inheritdoc}
     */
    public function __toString(): string {
        return $this->id;
    }
}

このように、文字列として扱えるようにしつつ、制約を持った型を作りたい場合にとても便利です。 この辺り感覚的には、Rustで言うInto<T>From<T>に近いと感じていて、とても便利に使っています。PHPでも似たような機能があってよかったです。

欲を言うなら__toIntのような他のプリミティブな型変換のメソッドはほしいです。 他にも常に変換に成功するわけではないので__tryStringがほしいとか、逆に__fromStringのような変換元を指定したいなど完全にRust脳になっています。 というかRustのFrom<T>TryFrom<T>が地味に神なので全言語にほしいです。

マジックメソッドは非常に便利ではあるものの、単純に利用すると型との相性が悪くなります。

public __get(string $name): mixed
public __call(string $name, array $arguments): mixed

特に__call__getなどでは戻り値の型はmixedになるため、RustのDerefのように内部で保持した値にマジックメソッドでアクセスを提供すると型情報がなくります。 そうなると呼び出し元でIDEなどでの補完や検査できず、それに起因してバグが発生することになります。

この問題は、PHPDocで@mixin@property-read@methodを使えばマジックメソッドで動的に呼び出される型を定義することで回避できます。 ただし、型定義がズレたときには実行時エラーが発生するため、この辺りはテストで担保する必要です。

というのでPHPにあるマジックメソッドは柔軟さと安全性を保てるためとても楽しいです。

3. 例外の再評価

ここ数年はResult<T, E>やそれに相当するエラー処理ばかりを使っていたので、久しぶりにPHPを触ると「例外とはなんだろう...」と戸惑いました。 例外に対しての解像度がPHPだけを触っているときと変わっていることに気づきました。

例えば、先ほどのMyIdクラスをassertではなくExceptionに変えた場合を考えてみましょう。

class MyId
{
    public function __construct(private string $id)
    {
        if ($id === '') {
            throw new InvalidMyIdException('ID cannot be empty');
        }
    }
}

ここで一口に例外を投げるといっても、PHPではExceptionの種類によって期待される振る舞いが異なります。

  • LogicException: プログラムのロジック内での欠陥を表す例外であり、コード修正が必要
  • RuntimeException: 実行時にだけ発生するような障害を表す例外
  • Exception: すべての例外の基底

最近のPHPのツールやフレームワークでは、LogicExceptionRuntimeExceptionは通常、非チェック例外としてcatchされることを期待しません。 これらはRustのpanic動作に相当し、回復不可能なエラーと同様な振る舞いをする例外です。

対して、Exceptioncatchされることを期待しており、エラーを返しつつ処理を続行するために使用されます。 こちらはRustのResult?の動作に相当し、回復可能なエラーとして呼び出し元で処理されることを期待します。

この考え方に基づくと、コンストラクタでExceptionthrowするのは適切なのか。 コンストラクタは契約として適切な値が渡されることを期待すべきで、失敗する可能性のある操作を別に提供すべきではないか。 と、いろいろ考えられとても面白いです。(特に答えがあるわけではありません)

他の言語で別の概念を学んだからこそ見えてくるものがあるものですね。

4. コンパイルなしで動作する

「お前は何を言っているんだ」と思われるかもしれませんが、コンパイル必須の言語を触った後にコンパイルなしで動作するのは地味に感動します。

もちろん、最近の言語はコンパイルが非常に早くなっているため、コンパイル時間はそこまで問題にはなりません。 TypeScriptなどであればホットリロードがあるため、保存すると自動的に変更が反映されるので体験的にも問題になることは少ないです。

しかし、それでも数秒から数十秒のラグが生じます。 これまでその待ち時間が当たり前になっていましたが、PHPをあらためて触ることでコンパイル時間は少しストレスだったと気づかされました。

ちょっとした変更をすぐに確認できるのはやっぱり便利ですね。

PHPで足りないと感じる瞬間

AI対応に関してはPHPは弱いと感じます。 他の言語、特にRustやJavaScript系と比べると使い勝手が下がります。

PHP 8系であればそこまで悪くはありませんが、古いバージョンでは明確にAIのタスク実行精度、品質が下がります。 なんと言うかRustでここまでインターフェースとドキュメントを書けばあとはAIに任せられるというラインを超えても、PHPではトンチンカンな結果になるという印象です。

また、AIを使った機能ではPHPをサポートしていることが稀です。 例えばGitHub Copilot code reviewQodoなどとても便利な機能が出ていますが、PHPがサポートされておらず使えない、使えても品質が低いということがあります。

個人的にはAIなしで開発することが苦痛というレベルに達しているので、AIのPHPサポート、強化が早くきてほしいです。

おわりに

正直、今は本当にPHPが楽しくて仕方ないです。 私は今の時代に何か新しく作るならRust一択で、特定のユースケースがあるならTypeScriptやScala、Golangなど他の言語を使うかもしれない、という暴論の持ち主です。 特にコンテナを使ったWeb開発において変更容易性の保ちやすさ、運用面やインフラコストなどバランスを考えるとRustしかないです。

しかし、PHPは私のWebエンジニアとしての基礎を作った言語であり、やはりPHPを書くのは楽しくて仕方ありません。 これはもう、愛ですね。

一緒に働く仲間の募集

DMM.com 二次元&イノベーション開発本部 二次元コンテンツ事業/同人開発部では、一緒に働く仲間を募集しています。 同人開発部はPHPを触れる良い職場なので、興味がある方は以下のリンクから応募してみてください。

dmm-corp.com