[PHP/Laravel]やろうぜ、静的解析

# はじめに

## きっかけ

この話題を話そうと思ったきっかけは主に２点

- 主催している勉強会で話題になった。
    - 意外と導入している企業少ない？
- 結構PHP Conferenseで定期的に話題になってる
    - 2021 [https://youtu.be/-XcpNC-5P4I](https://youtu.be/-XcpNC-5P4I)
    - 2021 [https://youtu.be/jDtlylId9Gk](https://youtu.be/jDtlylId9Gk)
    - 2020 [https://youtu.be/1A75v3gsjZE](https://youtu.be/1A75v3gsjZE)

## 想定読者
- PHPで既存のコードによるバグに悩んでいる人
- これからプロジェクトの基盤を作る人（僕はこれだった）

# 静的解析とは？

https://ja.wikipedia.org/wiki/%E9%9D%99%E7%9A%84%E3%82%B3%E3%83%BC%E3%83%89%E8%A7%A3%E6%9E%90

> 実行ファイルを実行することなく解析を行うこと

エラーとかバグがないかを検証

## 要は...

ポチポチテストせずにバグの温床潰しこめてハッピー！ってこと

# PHPstanとは？

https://phpstan.org/user-guide/getting-started

- 多分PHPで一番有名でスター数も圧倒的な静的解析ライブラリ
    - PHP Conferenseでも大体これが取り上げられる
- JavaScriptで行くとeslintの立ち位置
- 検出する静的解析エラーのレベルを0~9で指定できるので、段階的に導入も可能
- どうしようもないやつはignoreできたり、phpstanが用意している検証ルールを無効にしたりと柔軟性〇

# 導入の必要性

以下メリットあり

## 【運用効率向上】想定外バグを生むコードをマージしないようにできる

よくわからないまま使ったメソッドが予想外の戻り値を返す
↓
原因不明のバグが発生
↓
対応に追われる

...みたいなことが無くなる

## 【開発効率向上】PHP DOC見ただけでメソッドの使い方がパッとわかる
- level4以上だとPHPDocで@returnや@paramを書かないとエラーになる
- 必ずメソッドやプロパティにPHPdocが書かれるようになり、ぱっと見でそのメソッドやプロパティの内容がわかるようになる

# 使い方
散々PHPstan, PHPstanいうてましたが、今回のデモはラッパーライブラリのLarastanを使います。

とはいえ、大本はPHPStanなのでやれることは同じです。

https://github.com/nunomaduro/larastan

### インストール

https://github.com/nunomaduro/larastan#-getting-started-in-3-steps

Docの通りcomposer installすればOK

※最新版インストールにはPHP 8系以上＋Laravel 9系以上（まじかよ）が必要なので、ここはバージョン指定が必要。。。なので、以下でOKだと

https://tech-tech.blog/php/laravel/larastan/

```php
composer require nunomaduro/larastan:^1.0 --dev
```

## いろんなカスタマイズ

### まずは設定ファイルの作成

`phpstan.neon` か `phpstan.neon.dist`をプロジェクト直下に作成（以下はDocのまま）

※コメントで各項目の説明あり

```php
includes: // 1, larastanの取り込み. ≒requireみたいなもの
    - ./vendor/nunomaduro/larastan/extension.neon

parameters:

    paths:  // larastanで解析する対象範囲決め. ここではappは以下のファイルを全て対象
        - app
		
    # The level 9 is the highest level
    level: 5  // 先ほどお話したレベル。詳細は https://phpstan.org/user-guide/rule-levels

    ignoreErrors:  // どうしてもはじきたい、どうにもならないエラーを登録（※）
        - '#PHPDoc tag @var#'

    excludePaths:  // 除外したいファイル「appのこのファイルだけは除外したい」みたいな
        - ./*/*/FileToBeExcluded.php

		// こっからは詳細ルール. 詳細は以下
    // Doc: https://github.com/nunomaduro/larastan/blob/master/docs/rules.md
    // Qiita: https://qiita.com/MasaKu/items/7ed6636a57fae12231e0#larastan%E3%81%AE%E8%A7%A3%E6%9E%90%E3%83%AB%E3%83%BC%E3%83%AB
	  // ちなみに以下はPHPのタイプヒントの欠落の検出を無効にするエラー
		// https://phpstan.org/config-reference#vague-typehints
    checkMissingIterableValueType: false
```

※ignoreはソースコード側で以下でもできる（Docにも記載有）

```php
// @phpstan-ignore-next-line 
$test->badMethod();
```

もっと詳細知りたいならLarastanのDocかPHPstanのDocへGo

https://phpstan.org/user-guide/getting-started

## 実行

```php
./vendor/bin/phpstan analyse
```

## 使用例

### 未定義の変数呼び出し、未定義関数・クラスの呼び出し、インスタンス化（ザ・致命的なエラー）

- レベル…0
↓実際にやってみる

```php
------ --------------------------------------
  Line   Http\Controllers\HelloController.php
 ------ --------------------------------------
  27     Undefined variable: $hoge
 ------ --------------------------------------
```

実際はほぼ通らないけどバグの温床になりうるコードを検出できる

```php
if (false) {
    echo $hoge; // $hogeなんて変数定義してない！
}
```

### 常にtrueを返す無駄な条件文検出

- レベル…4
↓実際にやってみる

```php
------ -----------------------------------------------------------------------
  Line   Http\Controllers\HelloController.php
 ------ -----------------------------------------------------------------------
  28     If condition is always true.
```

無意味な条件分岐は消しましょう、ということ

```php
$true = true;
if ($true) { // 100%trueになる条件文...
    echo $hoge;
}
```

### 実引数、戻り値の指定し忘れ

- レベル…6
↓実際にやってみる

```php
------ -----------------------------------------------------------------------------------
  Line   Http\Controllers\HelloController.php
 ------ -----------------------------------------------------------------------------------
  19     Method App\Http\Controllers\HelloController::index() has no return type
         specified.
```

チャンと引数、戻り値を指定しましょう

```php
/**
 * index.
 *
 * @param Request $request
 * @return \Illuminate\Contracts\View\View
 */
public function index(Request $request)
{
		return view('hello.index', $data);
}
```

### nullプロパティへのアクセス検出

- レベル…8 （僕はこれで実務。最初はPHPStanの回避が結構キツかった(笑)）
↓実際にやってみる

```php
------ -------------------------------------------------------------------------------------------------------------------
  Line   HelloController.php
 ------ -------------------------------------------------------------------------------------------------------------------
  29     Cannot access property $userAttribute on App\Models\User|null.
```

Laravelの組み込み関数のfirst()はDBからデータが取得できない場合はnullを返すので注意

https://github.com/laravel/framework/blob/9.x/src/Illuminate/Database/Concerns/BuildsQueries.php#L288

※完全余談ですが、元をたどるとヘルパー関数のArr::first呼び出してnullを返してるんですけどね

https://github.com/laravel/framework/blob/9.x/src/Illuminate/Collections/Arr.php#L191

だからabortするとか

```php
$user = User::first();
if (is_null($user)) {
    abort(404);
}
```

abortだとコード汚くなるのでデータないときは404返すfirstOrFail()使うか

```php
$user = User::firstOrFail();
```

プロパティやメソッドにアクセス時にnull安全オペレータ使うとか

```php
$user?->name;
$user?->fill(...);
```
で対応。

### mixed型の禁止

レベル...9

➡かなりキツイ。。。Laravelの組み込み関数などで戻り値によく使われているので大量のエラーの想定

※やったことないので割愛

# 最後に
個人的にはエラーが起きた時にPHP、Laravelの組み込みメソッドをしっかり見るようになるので、コードリーディングになってすごく勉強になりました。

CIへの導入が最終到達点だと思います。

以下がCIの自動化を最終ゴールにした記事なので参考になります。

https://blog.shin1x1.com/entry/getting-stated-with-phpstan