初めて静的解析（Larastan）導入してlevel5まで対応したので、どんな所を怒られたかまとめ

## この記事で書くこと

- 新規開発案件にLarastan（PHPStanのLaravel用拡張）を導入し Level0 から Level5 まで一つずつ上げ、どのようなエラーが出て、どのようにエラー解消したかのまとめ

https://github.com/nunomaduro/larastan

## この記事で書かないこと
- PHPStan, Larastanのインストール方法や使い方
- 静的解析初心者なので、効率的な対応方法などをお探しの方のご期待には沿えないかと思います



## Level 0 

### 実行コマンド

```
./vendor/bin/phpstan analyse --level 0 > phpstan.txt
```

### 実行結果（抜粋）

```bash
 [ERROR] Found 97 errors                                                                                                

 ------ -----------------------------------------------------------------------------------
  Line   Http/Controllers/Hoge/AccountController.php                                                                      
 ------ -----------------------------------------------------------------------------------
  93     Method App\Http\Controllers\Hoge\AccountController::show() should return Illuminate\Http\Response but return     
         statement is missing.                                                                                             
  147    Method App\Http\Controllers\Hoge\AccountController::destroy() should return Illuminate\Http\Response but return  
         statement is missing.                                                                                             
 ------ -----------------------------------------------------------------------------------

```
97のエラーのうちほとんどがこれ。
@returnの記述がPHPDocにあるが return が無いのでエラー。

### 対応内容

ResourceControllerをmakeコマンドで自動生成して以下のような使っていない関数がそのまま放置されていたので削除。

```php
    /**
     * Display the specified resource.
     *
     * @param  \App\Models\Account  $account
     * @return \Illuminate\Http\Response
     */
    public function show(Account $account)
    {
        //
    }
```


### 実行結果（抜粋）

```bash
 ------ --------------------------------------------------------------------- 
  Line   Http/Controllers/Auth/RegisterController.php                         
 ------ --------------------------------------------------------------------- 
  67     Call to static method create() on an unknown class App\Models\User.  
         💡 Learn more at https://phpstan.org/user-guide/discovering-symbols  
 ------ --------------------------------------------------------------------- 
```
存在しないクラス App\Models\User の静的メソッド create() を呼び出していてエラー。

### 対応内容

Auth入れると自動で生成されるControllerなのでいったんコメントアウト

### 実行結果（抜粋）

```bash
 ------ -------------------------------------------------------------------------- 
  Line   Services/Hoge/HogeService.php                                         
 ------ -------------------------------------------------------------------------- 
  23     Call to static method error() on an unknown class App\Services\Hoge\Log.  
         💡 Learn more at https://phpstan.org/user-guide/discovering-symbols       
  24     Instantiated class App\Services\Hoge\DbErrorException not found.          
         💡 Learn more at https://phpstan.org/user-guide/discovering-symbols              
 ------ -------------------------------------------------------------------------- 
```

不明なクラス App\Services\Hoge\Log の静的メソッド error() を呼び出している。
インスタンス化されたクラス App\Services\Hoge\DbErrorException が見つかりません。
というエラー。

### 対応内容

use Illuminate\Support\Facades\Log;
use App\Exceptions\DbErrorException;
をnamespaceに書き忘れていたので追加。

## Level 1

### 実行コマンド

```
./vendor/bin/phpstan analyse --level 1 > phpstan.txt
```

### 実行結果（抜粋）

```bash
 [ERROR] Found 3 errors   
 ------ ----------------------------------------------------------------------------- 
  Line   Http/Controllers/Hoge/HogeController.php                                   
 ------ ----------------------------------------------------------------------------- 
  60     Call to function compact() contains possibly undefined variable $activitys.  
 ------ ----------------------------------------------------------------------------- 
```

]
関数 Compact() の呼び出しには、未定義の変数 $activitiys が含まれている可能性があります。

### 対応内容

```diff_php
if ($account) {
    $activitys = $this->service->getActivitys($account_id);
} else {
+   $activitys = "";
}
```

`if ($account)`が else のときに $activities が未定義でcompact() に渡される…
else 時の処理が間違っていたので修正。

## Level 2

### 実行コマンド

```bash
./vendor/bin/phpstan analyse --level 2 > phpstan.txt
```

### 実行結果（抜粋）

```bash
[ERROR] Found 165 errors
 ------ -------------------------------------------------------------------------------------- 
  Line   Http/Controllers/Hoge/AccountController.php                                          
 ------ -------------------------------------------------------------------------------------- 
  94     Access to an undefined property App\Models\Account::$authority.                       
         💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property  
 ------ --------------------------------------------------------------------------------------
```

未定義のプロパティ App\Models\Account::$authority へのアクセス。
165エラーのうちほとんどがこれ。
該当箇所は以下。
Modelのプロパティがマジックメソッドなので発生している。

```php
if (auth()->user()->authority != 1)
```

### 対応内容

本来、Larastanはmigrationファイルを見て判断してくれるが、migrationファイルのない案件だったので
phpstan.neon ファイルに以下記述。

```yaml
ignoreErrors:
    # Modelのプロパティがマジックメソッドなので発生するエラー。migrationファイルが無くエラー回避できないので無視する
        - '#Access to an undefined property App\\Models(.*)#'
        - '#Access to an undefined property object#'
```

## Level 3

### 実行コマンド

```bash
./vendor/bin/phpstan analyse --level 3 > phpstan.txt
```

### 実行結果（抜粋）

```bash
[ERROR] Found 2 errors  
 ------ ------------------------------------------------------------------------------------------------------------------------------------------- 
  Line   Http/Controllers/Hoge/HogeController.php                                                                                           
 ------ ------------------------------------------------------------------------------------------------------------------------------------------- 
  26     Method App\Http\Controllers\Hoge\HogeController::index() should return
Illuminate\Http\Response but returns Illuminate\View\View.  
 ------ ------------------------------------------------------------------------------------------------------------------------------------------- 
```

メソッド App\Http\Controllers\Hoge\HogeController::index() は Illuminate\Http\Response を返す必要がありますが、Illuminate\View\View を返しています。

### 対応内容

```diff_php
/**
 * Display a listing of the resource.
 *
- * @return \Illuminate\Http\Response
+ * @return \Illuminate\View\View
 */

public function index()
{
    $records  = $this->service->getRecords();
    
    return view(self::PAGE . '.index', compact('records'));
}
```

@return の型指定が間違っていたので修正。

## Level 4

### 実行コマンド

```bash
./vendor/bin/phpstan analyse --level 4 > phpstan.txt
```

### 実行結果（抜粋）

```bash
 [ERROR] Found 3 errors 
 ------ -------------------------------------------------------------------------------------------------------------------------------------------- 
  Line   Rules/HogeExtention.php                                                                                                                    
 ------ -------------------------------------------------------------------------------------------------------------------------------------------- 
  30     Else branch is unreachable because previous condition is always true.                                                                       
         💡 Because the type is coming from a PHPDoc, you can turn off this check by
 setting treatPhpDocTypesAsCertain: false in your phpstan.neon.  
 ------ -------------------------------------------------------------------------------------------------------------------------------------------- 
```

前の条件が常に true であるため、Else ブランチに到達できません。

### 対応内容

```diff_php
class HogeExtention implements InvokableRule
{
    /**
     * Run the validation rule.
     *
     * @param  string  $attribute
- * @param  array  $value
+ * @param  array|\Illuminate\Http\UploadedFile  $value
     * @param  \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     * @return void
     */
    public function __invoke($attribute, $value, $fail)
    {
        if (is_array($value)) {
            // 省略
        else {
            //ここに到達できないよというエラー
        }
    }
```

@paramの型指定が間違っていたので修正。

### 実行結果（抜粋）

```bash
 ------ ---------------------------------------------------------------------------------- 
  Line   Services/Hoge/HogeService.php                                                 
 ------ ---------------------------------------------------------------------------------- 
  12     Property App\Services\Hoge\HogeService::$model is never read, only written.   
         💡 See: https://phpstan.org/developing-extensions/always-read-written-properties  
 ------ ---------------------------------------------------------------------------------- 
```

プロパティ App\Services\Hoge\HogeService::$model は読み取られることはなく、書き込まれるだけです。

### 対応内容

```diff_php
class HogeService extends Service
{
    public function __construct(
        private Activity $model
    ) {
    }

    public function getActivitys($id)
    {
-       $activity = Activity::find($id);
+       $activity = $this->model::find($id);
        return $activity;
    }
}
```

プロパティを宣言していたが、間違えてModelを使っていたので修正。

## Level 5

### 実行コマンド

```bash
./vendor/bin/phpstan analyse --level 5 > phpstan.txt
```

### 実行結果（抜粋）

```bash
[OK] No errors 
```

レベル５までエラーが出なくなりました:tada: 

## ちなみにLevel 6 に上げると

```bash
 [ERROR] Found 326 errors 
 ------ -------------------------------------------------------------------------------------------------- 
  Line   Http/Controllers/Admin/AccountController.php                                                      
 ------ -------------------------------------------------------------------------------------------------- 
  42     Method App\Http\Controllers\Hoge\HogeController::storeInput() has no return type specified.       
 ------ -------------------------------------------------------------------------------------------------- 
```

メソッドに戻り値の型が指定されていませんエラー。
326エラーのほとんどがこれだったので断念:dizzy_face:
level max まで辿りつけるように精進します...

## 感想

初めて、静的解析を使ってみましたが、なぜもっと早く使わなかったのかと思いました。
宣言したのに使っていないプロパティとか、namespaceの間違いなど、バグに繋がるところを指摘してもらえるのでとても有り難い。
ということで、全社的に本番環境にpushする前に必ず静的解析かけようという決まりができました。
level6以上を達成するには社内のコーディングルールの見直しが必要なのですぐには難しそうですが、少しずつリファクタリングして上げていきたいです。