🕌

Laravel 10 時代に備えよう(Larastan の導入)

2022/12/15に公開

前書き

Laravel 10 になると、ユーザー側のファイルには、PHP 言語の「型」が記載されてくることになります。

例)

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, string $id): Response
    {
        //
    }

もちろん、だからと言って強制ではないので、我々が書くときは、その型をカットして書く事もできたりしますが、とは言え、そこら中に型が付いていると、我々も型を書くような流れになるだろうと思います😄

きっちり型を定義して不要なバグを防ぐのが目的ではありますが、場合によっては、型を定義したばかりにバグってしまうという事もあり得ます。(本末転倒というか😅)

という事で今回の記事は、「こんな時にバグってしまったりして、Larastan(PHPStan を元にした Laravel 用静的解析ツール) を入れていると、助けられるよ」という話です。

なお、記事のタイトルには、「Larastan の導入」と書かれていますが、インストール方法や設定については、省略しています🙇‍♂️(検索すると、既に素晴らしい記事が沢山あります)

(以下、動作環境:PHP8.2、Laravel 9.43、Larastan 2.29 )

本題

では、まずはどんな時にバグってしまうか見ていきます。(あくまで1つのシナリオです)
users テーブルに nickname という項目を追加します。

マイグレーション
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
+            $table->string('nickname');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

NOT NULL としています。

続けて、このニックネームを取得するメソッドを User モデルに用意します。

User
    public function getNickname()
    {
        return $this->nickname;
    }

全く意味の無いメソッドですが、そこは気にしないで下さい。
DB にはデータは埋まっているとして、以下のように記述したとします。

web.php
Route::get('/', function () {
    $user = User::findOrFail(1);

    dd($user->getNickname());
});

すると、1番さんのニックネームが表示されます。
ここまでは問題無いですね。

そこで、この段階で Larastan のレベル6以上(執筆時点)で Larastan を実行すると、以下のエラーが出ます。

 ------ ---------------------------------------------------------------------
  Line   Models/User.php
 ------ ---------------------------------------------------------------------
  45     Method App\Models\User::getNickname() has no return type specified.
 ------ ---------------------------------------------------------------------

怒られてしまったので、先程のメソッドに return の型を指定します。

User
    public function getNickname(): string
    {
        return $this->nickname;
    }

これで Larastan のエラーも出なくなります。

ですが、例えば3ヶ月後とかに仕様の変更があり、「ニックネームは非必須でお願い🍷」という要望があり、nicknamenullable に設定したとします。

マイグレーション
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('nickname')->nullable();  // ← ★ ここの nullable()
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

(本来はファイルを分けるべき所ですが、今回はその辺はカットして、先程のファイルを直接編集しています。(分けても以降で見る結果は同じです))

さて、このままで終わらせてしまうと、バグあり状態となります。

例えば、10人に1人の割合でニックネームを入力しない人がいて、nicknamenull の人がいたりすると、そのユーザーで $user->getNickname() すると、以下の PHP エラーが表示されます。

App\Models\User::getNickname(): Return value must be of type string, null returned

ニックネームありの人の場合は大丈夫だけど、無しの人の場合はエラーが出るという、厄介な話ですね。

もし、この時、Larastan のレベル8以上(執筆時点)で Larastan を実行すると、以下のエラーを吐いてくれます。

 ------ -------------------------------------------------------------------------------------
  Line   Models/User.php
 ------ -------------------------------------------------------------------------------------
  47     Method App\Models\User::getNickname() should return string but returns string|null.
 ------ -------------------------------------------------------------------------------------

と言うことで、先程の return の型を以下のように修正します。

User
    public function getNickname(): ?string
    {
        return $this->nickname;
    }

「?string」は、「string|null」でも大丈夫です。これで PHP と Larastan のエラーは出なくなります。リリース前などに Larastan を実行していれば、上記のバグにも気づける訳ですね。

Larastan は、マイグレーションファイルを見て、nicknamenullable という事を認識し、「そこ string だけじゃ無いですよね。null の可能性もありますよね😎」と指摘してくれた訳ですね。Larastan 凄い!

そんな Larastan ですが、あくまで Laravel の後を追っていく形になりますので、Laravel の最新機能に追いついてない事もあります。例えば、執筆時点で言えば、マイグレーションの $table->ulid(); なんかには、まだちゃんと対応していないです。

後書き

PHP の緩い所がまた良さ?の一つではありますが、今後は、型が多い堅苦しい(← 😄)言語に向かっていきそうですね。

間違い等ありましたらコメント下さい。

Discussion