Larastanで始めるLaravelの静的解析:型のない世界に型検査を導入する

概要

PHPは動的型付け言語であり、Laravelはその柔軟さを最大限に活かしたフレームワークです。マジックメソッド、ファサード、Eloquentの動的プロパティなど、「書きやすいが型が追いにくい」コードが大量に生まれます。これらは実行してみるまでエラーに気づけません。

本記事では、PHPStanのLaravel向け拡張である Larastan を導入し、型のない世界に段階的に型検査を持ち込むハンズオンを解説します。導入から、レベルを少しずつ上げていく運用、CIへの組み込みまでを一通り扱います。

環境

  • PHP 8.3 以上
  • Laravel 12 / 13 系
  • Composer
  • Larastan 3.x(PHPStan 2.x ベース)

本記事のコマンドは Laravel 13 系で確認しています。composer require --dev larastan/larastan は現在、デフォルトで 3.x(PHPStan 2.x ベース)を入れます。本記事もこの 3.x / PHPStan 2.x を前提に組み立てています。すでに Larastan 2.x(PHPStan 1.x ベース)を使っているプロジェクトからメジャーを上げる場合は、後述の互換性の注意を参照してください。

発生した問題

型検査のない Laravel プロジェクトでは、次のような不具合が「実行時まで発覚しない」まま潜みます。

// 綴り間違いのメソッド名。IDEもエラーにしないことがある
$user->sendPasswordResetNotificaton(); // typo: Notificaton

// null の可能性があるのに、そのままメソッドを呼ぶ
$user = User::find($id);
return $user->email; // $user が null のとき Fatal error

// 存在しないカラム・リレーションへのアクセス
$post->auther->name; // typo: author

これらはテストが網羅していなければ本番で初めて落ちます。レビューでも見落としがちです。静的解析はこの層を機械的に潰すための道具です。

原因:Laravelで型が追えなくなる要因

Laravel 固有の「型が追えなくなる要因」は主に以下です。

  1. ファサード — Cache::get() の実体は Illuminate\Cache\CacheManager 経由だが、呼び出しコードからは静的メソッドに見える
  2. Eloquent の動的プロパティ — $user->name はDBカラムであり、PHPのプロパティ定義には存在しない
  3. マジックメソッド — __get / __call によるメソッド解決
  4. コンテナ解決 — app(SomeService::class) の戻り値型が mixed になりがち

素のPHPStanはこれらを「未定義プロパティ」「未定義メソッド」として大量に誤検知します。Larastan はこれらの Laravel 固有パターンを理解する拡張ルールとスタブを提供し、誤検知を抑えつつ本物のバグを検出できるようにします。

対応方法

1. インストール

composer require --dev larastan/larastan

> 補足:このコマンドは現在、Larastan 3.x(PHPStan 2.x ベース)を入れます。明示的にメジャーを固定したい場合は composer require --dev "larastan/larastan:^3.0" と指定します。また、古い nunomaduro/larastan は現在 larastan/larastan に移管されています。新規導入では新しいパッケージ名を使ってください。

2. 設定ファイルを作る

プロジェクトルートに phpstan.neon(または phpstan.neon.dist)を作成します。

includes:
    - vendor/larastan/larastan/extension.neon

parameters:
    paths:
        - app
        - config
        - database
        - routes

    # まずは低いレベルから。段階的に上げる
    level: 0

    # 解析対象から除外したいパス
    excludePaths:
        - app/Legacy/*

level は 0〜10(PHPStan 2.x 系)で、数字が大きいほど厳格になります。最初は必ず 0 から始めます。

3. 初回実行

./vendor/bin/phpstan analyse

メモリが足りない場合は上限を上げます。

./vendor/bin/phpstan analyse --memory-limit=1G

実装例

ベースラインで「既存のエラー」を一旦棚上げする

既存プロジェクトにいきなり導入すると、数百〜数千件のエラーが出ることがあります。すべてを即座に直すのは非現実的なので、ベースラインで現状を「既知の負債」として記録し、以後の新規エラーだけを検出対象にします。

./vendor/bin/phpstan analyse --generate-baseline

これで phpstan-baseline.neon が生成されます。phpstan.neon に取り込みます。

includes:
    - vendor/larastan/larastan/extension.neon
    - phpstan-baseline.neon

parameters:
    level: 5
    paths:
        - app

こうすると「今あるエラーは無視、これから増やさない」という運用ができます。負債は別途、時間を取って少しずつベースラインから削っていきます。

レベルを段階的に上げる

おすすめの進め方は以下です。

レベル 主に検出されるもの
0〜1 未定義の変数・関数・メソッド呼び出し
2〜3 未定義プロパティ、$this の誤用、PHPDocの型不一致
4〜5 到達不能コード、常に真/偽の条件、戻り値型の不一致
6 型宣言の欠落(missing type)
7〜8 union型の分岐漏れ、nullableの厳密な扱い
9 mixed に対する操作を厳しく制限
10 型の付いていない暗黙の mixed(level 9 ですり抜ける値)まで検出

1レベル上げる → 出たエラーを直す → コミット、を繰り返します。レベル6以降は型注釈をしっかり書く文化が必要になるので、チームの状況に合わせて止めても構いません。最上位の level 10 は PHPStan 2.x で追加されたもので、暗黙の mixed まで追い込むため、まずは 9 までを目標にして無理なく運用するのが現実的です。

Eloquentモデルに型のヒントを与える

Larastan は find() が nullable を返すことなどを理解しますが、動的プロパティの型までは完全には追えません。モデルに PHPDoc を書くと精度が上がります。

/**
 * @property int $id
 * @property string $name
 * @property string $email
 * @property \Illuminate\Support\Carbon|null $email_verified_at
 * @property-read \Illuminate\Database\Eloquent\Collection<int, Post> $posts
 */
class User extends Model
{
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

この PHPDoc は手で書くと大変なので、barryvdh/laravel-ide-helper の php artisan ide-helper:models で自動生成できます。生成した注釈を Larastan とIDE双方が利用します。

null安全を検出させる

レベルを上げると、次のようなコードが検出されます。

public function show(int $id): string
{
    $user = User::find($id); // User|null
    return $user->email;     // ← Larastan: Cannot access property on User|null
}

修正は意図に応じて選びます。

// 見つからなければ 404 にする(Laravel らしい書き方)
$user = User::findOrFail($id); // User を返す
return $user->email;

// あるいは明示的に分岐
$user = User::find($id);
if ($user === null) {
    abort(404);
}
return $user->email;

これが静的解析の価値です。「nullを考慮し忘れている箇所」を機械が指摘してくれます。

確認方法

ローカルでの再実行

# キャッシュをクリアして再解析したいとき
./vendor/bin/phpstan clear-result-cache
./vendor/bin/phpstan analyse

PHPStanは解析結果をキャッシュするので、2回目以降は高速です。おかしな挙動のときはキャッシュクリアを試します。

CI(GitHub Actions)への組み込み

新規エラーが混入したPRを機械的に弾くため、CIに入れます。

name: static-analysis

on: [pull_request]

jobs:
  phpstan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          coverage: none

      - name: Install dependencies
        run: composer install --prefer-dist --no-progress

      - name: Run PHPStan
        run: ./vendor/bin/phpstan analyse --no-progress --error-format=github

--error-format=github を付けると、エラーがPRの該当行にアノテーション表示されます。

注意点

  • Larastan 2.x(PHPStan 1.x ベース)から 3.x(PHPStan 2.x ベース)へ上げる場合はベースラインを取り直してください。 PHPStan 2.x ではレベルの意味合いが一部変わり、level: 10 が追加されています。レベルの厳密化に伴って検出内容が変わるため、既存の phpstan-baseline.neon はそのままだと噛み合わなくなることがあります。なお PHPStan 1.x はすでにメンテナンス対象外なので、新規導入では 2.x ベースの Larastan 3.x を選びます。
  • Larastan は アプリをブートして解析するわけではありません。純粋に静的なので、実行時にしか決まらない設定(動的なバインディングなど)は追えないことがあります。その場合は PHPDoc や @phpstan-ignore-line で補助します。
  • @phpstan-ignore-line の乱用は本末転倒です。抑制する場合は理由をコメントで残し、定期的に棚卸しします。
  • 静的解析はテストの代替ではありません。「型レベルの正しさ」は検証できますが「振る舞いの正しさ」は検証できません。両輪で使います。

まとめ

  • Larastan は Laravel 固有のマジックを理解した PHPStan 拡張で、ファサードや Eloquent の動的プロパティを正しく扱える
  • 導入は level: 0 から始め、ベースラインで既存負債を棚上げし、段階的にレベルを上げるのが現実的
  • Eloquent の精度は IDE Helper の PHPDoc 生成で底上げできる
  • 最大の効果は「nullの考慮漏れ」「typo」「存在しないメソッド呼び出し」といった、実行時まで気づけないバグをコミット前に機械が指摘してくれること
  • CIに組み込めば、型のない世界でも「型の負債をこれ以上増やさない」防波堤になる

型のない言語だからこそ、静的解析でカバーできる範囲は大きいです。まずは level: 0 で入れてみることをおすすめします。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。