Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

level=0 から始める PHPStan(Larastan) 導入ガイド

PHP にはアプリケーションを実行せずに静的にコード品質をチェックする静的解析ツールがいくつかあります。有名なところでは、PHPStan や Psalm、Phan、PHPMD などがあります。本エントリでは、まだこうした静的解析ツールを導入していないプロジェクト向けに PHPStan 導入の流れをまとめてみました。

3(4) 行まとめ

  • composer もしくは Docker で PHPStan をインストール
  • 設定ファイル(phpstan.neon) を作成(level=0)
  • (baseline ファイルを作成)
  • GitHub Actions で 自動実行

サンプルコード

github.com

PHPStan とは

PHPStan は、Ondřej Mirtes(@OndrejMirtes)さんが開発している PHP コードの静的解析ツールです。 MIT ライセンスで公開されています。PHP で実装されており、Composer でインストールして利用できます。また、Docker イメージも公式で公開されているので、こちらを利用すれば Composer パッケージとしてインストールせずに利用することも可能です。

phpstan.org

PHPStan には多くのコード品質をチェックするルールが実装されており、これをアプリケーションコードに適用することでバグの要因となりそうな箇所を検出します。これによりアプリケーションを実行した時にはじめて気づくような問題を未然に察知できます。

具体的には下記のような問題を検知できます。

  • 存在しないクラスをインスタンス化している。
  • 存在しないメソッド、関数を呼び出している。
  • 参照しているクラス名の大文字小文字が定義と異なる。
  • メソッド仮引数とメソッド呼び出し実引数の型が一致しない。*1
  • メソッドに型宣言が指定されていない。*2

Larastan は、PHPStan の拡張の一つで Laravel アプリケーションで型情報などを PHPStan に認識させるための設定が含まれています。Larastan を実行するには、PHPStan のコマンドを利用するので、実行方法は同様になります。

github.com

適用方針

  • level=0 からはじめる

PHPStan では、level を指定することで適用するルールを選択できます。level は 0 から 8 までの数字で指定し、0 が最も緩いルール、8 が最も厳格なルールとなります。level では max も指定でき、これは現時点では 8 と同じ意味合いとなります。

ではどの level を選択するのが良いかというと、これから PHPStan を導入する、特に既存プロジェクトへの適用ではは level=0 から始めることをオススメします。この手のツールではいきなり strict なルールを適用したくなるのは人情ですが、おそらく膨大なエラーが検出されて導入を躊躇ってしまうことになるでしょう。そこから除々に level を下げていくのは妥協していっているようで納得できないかもしれません。

大事なのは PHPStan を導入して、確実にそのルールをパスするサイクルを作るということなので、まずはlevel=0 のエラーから始めて、全てのエラーを取り除けたら level を上げていく方針が良いです。

  • baseline で現時点のエラーは無視する

これは必須では無いのですが、歴史の長いコードベースに PHPStan を実行すると、level=0 でも 3 桁、4 桁のエラーが検出されるケースがあります。これらを全て修正してからで無いと PHPStan が導入できないとなるととてもでは無いですが導入する気になりません。

こういった場合は、その時点で発生しているエラーを無視する baseline という方法が利用できます。これにより、現時点のコードベースは全てパスさせておき、今後に追加修正するコードについてルールを適用させることができます。

baseline は後述するように自動生成できるので、エラーが 4 桁になっても問題ありません。baseline で記録されたエラーは関連のコードを修正する際についでに少しづつ修正していけば無理なくエラーが減らせます。

これも level=0 の話と同様に心情としては現時点のエラーを握りつぶすのは躊躇いがあるかもしれませんが、まずはエラーが 0 の状態で導入して、今後の開発ではそれをキープするようにすることを意識するのが良いでしょう。

  • CI で自動実行する

CI でコードを push した際に PHPStan を自動で実行するようにしておきます。詳細は後述しますが、これは非常に大事なことです。

Playground

PHPStan は公式サイトに Playground が用意されており、ブラウザで PHP コードを入力すると PHPStan が実行され、結果が表示されます。適用する level も選択でき、permlink も発行できるので、試しに実行してみたり、実行結果を共有するなどに利用できます。(ただ、プロダクションコードをそのまま貼るのは避けた方が良いです。)

Playground | PHPStan

PHPStan のインストール

Composer

PHPStan は Composer パッケージとして公開されているので、composer コマンドでインストールできます。下記のようにコマンドを実行されば PHPStan がインストールされ、phpstan コマンドが利用できるようになります。

$ composer require --dev phpstan/phpstan

PHPStan は、PHP 7.1 以上で動作します。vendor/bin/phpstan が実行コマンドになります。

$ ./vendor/bin/phpstan -V
PHPStan - PHP Static Analysis Tool 0.12.96

Composer(Larastan)

Laravel アプリケーションの場合は、Larastan をインストールすると良いでしょう。Larastan は依存として PHPStan をインストールしますので、上記の phpstan コマンドも合わせてインストールされます。

$ composer require --dev nunomaduro/larastan

Larastan は PHP 7.2 以上かつ Laravel 6 以上が対象となります。こちらもインストールが完了すると vendor/bin/phpstan コマンドが利用可能になります。

$ ./vendor/bin/phpstan -V
PHPStan - PHP Static Analysis Tool 0.12.96

Docker

PHPStan には公式の Docker ファイルが用意されています。Composer をそもそも利用していないプロジェクトやプロジェクトの composer.json に PHPStan を導入したくない場合などに利用できます。

Package phpstan · GitHub

下記のように docker run コマンドを実行すると phpstan コンテナが実行できます。

$ docker run -v `pwd`:/app -w /app ghcr.io/phpstan/phpstan analyse -V
PHPStan - PHP Static Analysis Tool 0.12.92

設定ファイルの作成

PHPStan の設定ファイルを作成します。設定ファイルは NEON という YAML に似たファイル形式で、 phpstan.neon というファイル名で作成します。((phpstan.neon.dist でも可。phpstan コマンドは phpstan.neon.dist , phpstan.neonの順で設定ファイルを検出します。個人的には .neon ファイルは PhpStorm で NEON ファイルとして扱える、さらにチームでは同じ設定ファイルを共有した方が良いので phpstan.neon を利用しています。)) 設定ファイルは下記のようになります。

parameters:
    paths:
        - .
    level: 0

paths では、検出対象のコードディレクトリを指定します。ここではカレントディレクトリを指定しています。

level では、適用する level を指定します。ここでは、0 (最も緩い)となっています。

Larastan を利用する際は、下記のように Larastan の設定ファイルを読み込みようにします。これは Laravel プロジェクトを想定しているので、paths では Laravel アプリケーションディレクトリを指定しています。

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

parameters:
    paths:
        - app
        - bootstrap
        - config
        - database
        - resources/views
        - routes
    level: 0

Config Reference | PHPStan

PHPStan の実行

PHPStan を実行するには phpstan コマンドを利用します。analyse サブコマンドを指定すると設定ファイルの内容に従って静的解析を行い、エラーを出力します(analyse は省略可能)。下記の実行例では 46 ファイルの解析を行い、エラーが検出されませんでした。

$ ./vendor/bin/phpstan analyse
Note: Using configuration file /path/to/phpstan-example/laravel/phpstan.neon.
 46/46 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 [OK] No errors

なお、Laravel アプリケーションのように多くのパッケージを利用する場合、PHP のメモリ制限で実行が止まる場合があります。こういった場合は、--memory-limit オプションで利用するメモリ制限を緩和すると良いです。

# メモリ制限エラー
$ ./vendor/bin/phpstan
(snip)PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes)


# メモリ制限を 1G にする
$ ./vendor/bin/phpstan analyse --memory-limit=1G 

エラーが検出された場合は、下記のような出力になります。エラーが検出されたファイル名、行番号、エラー内容を示すメッセージが出力されます。ここでは、index.php の 13 行目にて、something 関数が見つからないというエラーが検出されています。

 ------ --------------------------------------------------------------------
  Line   index.php
 ------ --------------------------------------------------------------------
  13     Function something not found.
         💡 Learn more at https://phpstan.org/user-guide/discovering-symbols
 ------ --------------------------------------------------------------------

 [ERROR] Found 1 error

実際のコードでは下記の箇所になります。

<?php
// snip
something(); // 存在しない something() の呼び出し

このように PHP コードを実行せずにコードの問題点を検出することができます。

主なエラー(level=0)

level=0 は最も緩いレベルですが、多くのエラーを検出できます。一部を抜粋すると下記のようなものがあります。これでも一部なので、level=0 を遵守するだけでもコード品質が向上することが分かります。特にクラス名やメソッド名変更などのリファクタリングを行った際の修正漏れなどはこのレベルでも検出できます。

  • Access to an undefined property
    • 未定義プロパティへのアクセス
  • Static call to instance method
    • インスタンスメソッドをクラスメソッドとして呼んでいる
      • ex. クラスメソッドに static が付いていない
  • Instantiated Foo not found
    • 存在しないクラス Foo をインスタンス化している
  • Call to an undefined static method
    • 未定義クラスメソッドを呼んでいる
  • Call to static method method() on an unknown class Foo
    • 存在しない Foo クラスのクラスメソッド method() を呼んでいる
  • Call to an undefined method
    • 存在しないメソッドを呼んでいる
  • Undefined variable
    • 未定義変数を参照している
  • Class Foo referenced with incorrect case: foo
    • クラス Foo を foo という大文字小文字が誤って参照している
  • Method Foo::something() invoked with 0 parameters, 1 required
    • メソッドで定義されている必須の仮引数が指定されていない
    • 別パターンとして下記もある
      • constructor invoked with 0 parameters, 2\-3 required
      • Static method Foo::something() invoked with 0 parameters, 1 required
  • Access to undefined constant
    • 未定義定数にアクセスしている

PHP コードとしてパースできないものもエラーとして検知されます。プロダクションコードでこれが見つかる場合はおそらく利用されていないデッドコードなので、問題無ければ削除(最悪コメントアウト)しておきましょう。

baseline ファイルの作成

この段階で検出されたエラーを今時点では修正が難しいという場合は、baseline ファイルを作成して、エラーを無視するようにします。

baseline ファイルを生成するには、phpstan コマンドの --generate-baseline オプションを利用します。下記のようにオプションを付けて実行すると、phpstan-baseline.neon というファイルに無視するエラーが記録されます。

$ ./vendor/bin/phpstan analyse --generate-baseline
Note: Using configuration file /path/to/phpstan.neon.
 2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
 
 [OK] Baseline generated with 5 errors.

生成された phpstan-baseline.neon は下記のようになります。ignoreErrors キーに無視するエラーメッセージ、ファイル名、回数が記述されています。

parameters:
    ignoreErrors:
        -
            message: "#^Call to an undefined static method Foo\\:\\:sub\\(\\)\\.$#"
            count: 1
            path: index.php

        -
            message: "#^Class Foo referenced with incorrect case\\: foo\\.$#"
            count: 1
            path: index.php

        -
            message: "#^Function something not found\\.$#"
            count: 1
            path: index.php

        -
            message: "#^Instantiated class Bar not found\\.$#"
            count: 1
            path: index.php

        -
            message: "#^Static call to instance method Foo\\:\\:add\\(\\)\\.$#"
            count: 1
            path: index.php

phpstan.neon からこのファイルを読み込むように指定すると、エラーが無視されるようになります。

includes:
    - phpstan-baseline.neon # 追加

parameters:
    paths:
        - .
    level: 0

生成した baseline ファイルは Git リポジトリに追加しておきましょう。

baseline ファイルの修正

baseline ファイルで指定したエラーのコードを修正して、エラーが検出されなくなると下記のようなエラーが発生します。

 ------ --------------------------------------------------------------------
  Line   index.php
 ------ --------------------------------------------------------------------
         Ignored error pattern #^Class Foo referenced with incorrect case\:
         foo\.$# in path /app/index.php was not matched in reported errors.
 ------ --------------------------------------------------------------------

この場合、baseline ファイルから当該箇所を削除するとエラーが解消します。

-               -
-                       message: "#^Class Foo referenced with incorrect case\\: foo\\.$#"
-                       count: 1
-                       path: index.php
-

もし phpstan コマンドで検出されたエラーが Ignored error pattern のみであれば、baseline ファイルを --generate-baseline オプションで再生成しても良いでしょう。

The Baseline | PHPStan

CI での自動実行

開発者個人の環境では PHPStan で静的解析ができるようになりました。次は、CI で自動実行するように設定します。こうしたツールは CI で自動実行することで真価を発揮します。もし、開発者個人の環境だけで実行している状況だと、個々人の意識やその時の気分によって実行するか否かが分かれたり、エラーが検出されてもそのままリリースされてしまいます。こうなると PHPStan によるコード品質チェックは形骸化していきます。

そうではなく、Git リポジトリに push した段階で CI で自動実行するようにしておけば、リポジトリにあるコードは誰が変更したコードであろうと常に等しくチェックが行われるようになります。さらに GitHub にあるブランチ保護ルールを設定することで、PHPStan によるチェックがパスしない限りメインブランチにマージできないという運用にすることも可能です。

ここでは、GitHub Actions を利用して、PHPStan をコード push 時に自動実行するようにします。CircleCI のような他の CI サービスや Jenkins でも同じことは実現できるので、利用しているサービスや環境に応じて設定してください。

GitHub Actions の設定

GitHub Actions の設定は .github/workflows/phpstan.yml に記述します。下記では、PHPStan を composer でインストールする場合と Docker イメージを利用する場合の例を示しています。

  • composer でインストールする場合

compoer でインストールする場合です。ここでは、shivammathur/setup-php という PHP 環境を構築するアクションを利用しています。PHP 8 と Composer 2 をセットアップして、composer install を行い、最後に phpstan コマンドを実行しています。

name: phpstan

on:
  push:

jobs:
  laravel:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.0'
        tools: composer:v2
    - name: Resolve dependencies
      run: composer install --no-progress --prefer-dist --optimize-autoloader
    - name: Run phpstan
      run: ./vendor/bin/phpstan --memory-limit=1G analyse
  • Docker イメージを利用する場合

Docker イメージを利用する場合は、docker run コマンドで PHPStan イメージを実行するだけです。プロジェクトによっては composer インストールなどのセットアップが必要であれば、上記の shivammathur/setup-php アクションを利用する、もしくは Composer の Docker イメージを利用するなどを行ってください。

name: phpstan

on:
  push:

jobs:
  vanilla:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run phpstan
        run: docker run -v `pwd`:/app -w /app ghcr.io/phpstan/phpstan analyse

このファイルを GitHub に push すると Action が実行されます。下記はサンプルコードでの GitHub Actions 実行例です。

github.com

GitHub Actionsのドキュメント - GitHub Docs

ブランチ保護ルール

ブランチ保護ルールの設定により、GitHub Actions での PHPStan 実行が失敗するとマージできないように制限をかけられます。

ブランチ保護ルールの設定は、リポジトリの「Settings」 - 「Branches」-「Branch protection rules」にある「Add rule」をクリックして、ルールを適用するブランチの設定を行います。GitHub Flow を採用している場合は、master や main といったメインブランチを指定すると良いでしょう。

下記が設定例です。ここでは main ブランチの保護ルールを設定しています。本エントリに関連しているのは Require status checks to pass before merging です。ここにチェックを入れて、PHPStan を実行結果を示すステータス名(タスク名)を入力します。下記では、phpstan がステータス名となっています。

f:id:shin1x1:20210824195232p:plain

ステータス名は GitHub Actions の実行結果でも参照できます。下記の Jobs には phpstan と表示されており、これがステータス名となります。

f:id:shin1x1:20210824195250p:plain

これを設定しておけば、phpstan ステータスが OK(green)にならないとマージボタンが有効になりません。なお、デフォルトでは管理者ユーザはこの状態でもマージ可能です。ブランチ保護ルール画面で Include administrators にチェックを入れておけば、管理者ユーザでもルールが適用されますが、このあたりは開発チームの運用次第となります。

ブランチ保護ルールを管理する - GitHub Docs

さいごに

PHPStan や Psalm といった静的解析ツールを利用して開発していると、実装時の考慮漏れやコード変更の修正漏れといったうっかりのミスを検知してくれます。もちろん自動テストでも発見できるのですが、テストよりも手軽かつ網羅的にコードをチェックできるのが利点です。コードは静的解析ツールでチェックし、アプリケーションはテストで確認するというように役割を分けて考えられます。

以前から PHPStan を利用していたのですが、どちらかというと型宣言や PHPDoc の型をチェックするために導入していました。level も 5 以上が前提だったので、従来から型を意識したプロジェクトでは効果的だったのですが、様々な事情でそうではないプロジェクトではそもそも導入を検討しなかったり、導入しようとしたものの大量のエラーに圧倒されて見送るという場面がありました。

しかし、PHPStan の進化に加えて、level=0baseline を利用することで導入の障壁が大きく下がりました。本文で記したように level=0 でも多くのルールがあり、Fatal Error を引き起こすような致命的な問題のいくつかはこのレベルでもチェックしてくれるので十分に導入する効果はあるでしょう。

特に CI をまだ導入していないプロジェクトでは、CI で実行する最初のタスクとして PHPStan や Psalm のような静的解析ツールは手間と効果のバランスを考えても良い選択肢です。*3 これを機にツールやシステムができることはそれに任せて、開発やコードレビューなどでは人がやるべき仕事に集中できるようしていくと良いですね。

*1:本エントリで設定する level=0 ではチェックされません

*2:本エントリで設定する level=0 ではチェックされません

*3:もちろん phpcs や phpunit でも良いのですが、phpcs より効果を感じやすく、phpunit より手間がかからないので。