【LaravelでCI構築】GitHubActionsとLarastan・psalm/plugin-laravel・PHP-CS-Fixerで静的解析チェックをプルリク作成時に自動実行するCI環境を構築してみた

# 綺麗なコードを維持するために便利な静的解析ツールを活用しつつ自動化して楽をしよう

チーム開発をしていると各個人によってインデントがずれていたり、型定コードの品質にバラつきが生まれてしまいますよね。

レビュー文化が定着している開発組織だとしても人力でコードの品質を担保するのはかなり労力がいるため現実的ではありません。

一定のルールを事前に設定しておき、そのルールにもっとって自動的に解析・整形を行うことは、低コストでコードの品質を担保することができるため非常にメリットが高いです。

今回の記事ではLarastan・psalm・PHP-CS-FixerといったPHP/Laravelの静的解析・整形ツールをGitHubActionsを活用してgitHubでPullRequestを作成するたびに自動的に実行される方法を紹介します。

__本記事の対象となる方__
- Laravelプロジェクトでチーム開発をしていて静的解析ツールに興味がある
- Larastan・psalm・PHP-CS-Fixerを使ってみたい
- GitHubActionsを使ったCI/CDに興味がある

:::note warn
本記事では各解析ツールの細かい説明は割愛しているため詳細は公式サイトをご参照ください
:::

本記事で紹介しているコードはこちらから確認できます。コピペだと転機ミスする可能性も多いため、手っ取り早く確認したい方は`git clone`がおすすめです。　

https://github.com/WebEngrChild/laravel-stati-analysis

## Laravel環境構築

まずはLaravelプロジェクトを作成します。

今回はLaravel公式ドキュメントに掲載されている'sail'を使った方法でインストールします。

```shell
$ curl -s "https://laravel.build/example-app?with=mysql" | bash
```

```shell
# クローンする場合
$ git clone git@github.com:WebEngrChild/laravel-stati-analysis.git
$ composer install
```

https://readouble.com/laravel/9.x/ja/installation.html#:~:text=%E3%81%A6%E3%81%8F%E3%81%A0%E3%81%95%E3%81%84%E3%80%82-,Sail%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E9%81%B8%E6%8A%9E,-Sail%E3%81%A7%E6%96%B0%E3%81%97%E3%81%84

次に'sail'コマンドを簡単に呼び出せるようにpathを通します。

```shell
$ vim ~/.zshrc

# 以下を.zshrc内に貼り付ける
alias sail='[ -f sail ] && bash sail || bash vendor/bin/sail'

$ source ~/.zshrc

```

ここまでできればビルドして立ち上げてみましょう。

```shell
$ sail build --no-cache
$ sail up -d
```

Laravelのwelcomeページが立ち上がれば完了です。

## Larastanのインストール

Larastanは、PHPStanの拡張の一つでLaravelアプリケーションで型情報などをPHPStanに認識させるための設定が含まれています。

元となっているPHPStanは、Ondřej Mirtes（@OndrejMirtes）さんが開発している PHP コードの静的解析ツールです。PHPで実装されており、Composerでインストールして利用することができます。

https://github.com/nunomaduro/larastan

```shell
# プロジェクトルートに移動する
$ cd example-app

# Larastanのインストール
$ sail composer require --dev nunomaduro/larastan
```

次に`phpstan`の設定ファイルの作成を行います。

```shell
# ファイル作成
$　touch phpstan.neon
```

```yml:phpstan.neon
includes:
    - ./vendor/nunomaduro/larastan/extension.neon

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

levelでは、適用するルールの厳格度を指定できます。ここでは、0 （最も緩い）となっています。最大9まで設定することができますが、多数のエラーに見舞われる可能性があるため徐々に上げて潰していくことをおすすめします。

```shell
# 実行
$ sail php ./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G
```

私の実行環境ではPHPのメモリ制限で実行が止まってしまいました。こういった場合は、--memory-limit オプションで利用するメモリ制限を緩和すると良いです。

ターミナル上にエラーが表示された場合は画面に従って修正していきます。

一方で既存プロジェクトに導入する場合など、その時点までのエラーを修正できない場合があります。
その際は`phpstan-baseline.neon`を設定することで該当エラーを無視することが出来ます。

```shell
# 設定ファイル(phpstan-baseline.neon)の作成
$ sail php ./vendor/bin/phpstan analyse --generate-baseline
```

先程のファイルに設定を追記します

```yml:phpstan.neon
includes:
    - ./vendor/nunomaduro/larastan/extension.neon
    - phpstan-baseline.neon #追記

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

ここまででLarastanの設定は完了です。
再度実行して見るとエラーが消えているのが確認できるはずです。

## psalm/plugin-laravelのインストール

psalmもphpstanと同様に静的解析ツールになります。phpstanとはそれぞれ検知できるエラーが異なる場合があるため、私の場合は両方いれました。

https://n314.hatenablog.com/entry/2020/12/05/214806

```shell
# インストール
$ sail composer require --dev vimeo/psalm

# 設定ファイル(psalm.xml)の生成
$ sail php ./vendor/bin/psalm --init

# Laravel puluginの有効化
$ sail composer require --dev psalm/plugin-laravel
$ sail php ./vendor/bin/psalm-plugin enable psalm/plugin-laravel

# 実行
$ sail php ./vendor/bin/psalm
```

`psalm.xml`は特に修正は不要です。
これでpsalmも対応完了になります。

## PHP-CS-Fixer

こちらのツールはLarastanやPsalmと異なり自動整形ツールになります。
インデントや空白、などのミスを指摘するだけでなく自動的に修正までしてくれる優れものです。

```shell
# インストール
$ sail composer require --dev friendsofphp/php-cs-fixer

# gitignoreにキャッシュファイルを追加
$ echo /.php-cs-fixer.cache >> ./.gitignore

# 設定ファイルをプロジェクトディレクトリに追加
$ touch .php-cs-fixer.dist.php
```

設定ファイルに以下を追記します。
自動整形の対象となるディレクトリや各種ルールを細かく設定できます。

:::note warn
以下はサンプルなので実際はプロジェクトのコーディングルールや実装内容に照らしわせて修正してください。
:::

```php:.php-cs-fixer.dist.php
<?php

declare(strict_types=1);

$finder = PhpCsFixer\Finder::create()
    ->in([
        __DIR__ . '/app',
        __DIR__ . '/config',
        __DIR__ . '/database/factories',
        __DIR__ . '/database/seeders',
        __DIR__ . '/routes',
        __DIR__ . '/tests',
    ]);

$config = new PhpCsFixer\Config();

return $config
    ->setFinder($finder)
    ->setRiskyAllowed(true)
    /**
     * https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/master/doc/rules/index.rst
     * https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/master/doc/ruleSets/index.rst
     */
    ->setRules([
        '@PSR12' => true,
        '@PSR12:risky' => true,
        '@PHP80Migration:risky' => true,

        // Alias
        'array_push' => true,
        'ereg_to_preg' => true,
        'no_alias_language_construct_call' => true,
        'pow_to_exponentiation' => true,
        'random_api_migration' => true,
        'set_type_to_cast' => true,

        // Array Notation
        'array_syntax' => ['syntax' => 'short'],
        'no_multiline_whitespace_around_double_arrow' => true,
        'no_trailing_comma_in_singleline_array' => true,
        'no_whitespace_before_comma_in_array' => true,
        'normalize_index_brace' => true,
        'trim_array_spaces' => true,
        'whitespace_after_comma_in_array' => true,

        // Basic
        'braces' => true,
        'encoding' => true,
        'non_printable_character' => true,

        // Casing
        'constant_case' => true,
        'magic_constant_casing' => true,
        'native_function_casing' => true,

        // Cast Notation
        'cast_spaces' => ['space' => 'single'],
        'modernize_types_casting' => true,
        'no_short_bool_cast' => true,

        // Strict
        'declare_strict_types' => true,
        'strict_comparison' => true,
        'strict_param' => true,

        // PHPDoc
        'align_multiline_comment' => true,
        'general_phpdoc_annotation_remove' => ['annotations' => ['class', 'package', 'author']],
        'no_blank_lines_after_phpdoc' => true,
        'no_empty_phpdoc' => true,
        'phpdoc_add_missing_param_annotation' => ['only_untyped' => false],
        'phpdoc_align' => ['tags' => ['param']],
        'phpdoc_annotation_without_dot' => true,
        'phpdoc_indent' => true,
        'phpdoc_no_access' => true,
        'phpdoc_no_empty_return' => true,
        'phpdoc_no_package' => true,
        'phpdoc_order' => true,
        'phpdoc_return_self_reference' => true,
        'phpdoc_scalar' => true,
        'phpdoc_single_line_var_spacing' => true,
        'phpdoc_summary' => false,
        'phpdoc_to_comment' => false,
        'phpdoc_trim' => true,
        'phpdoc_types' => true,
        'phpdoc_types_order' => true,
        'phpdoc_var_without_name' => true,

        // Comment
        'no_empty_comment' => true,
        'single_line_comment_style' => false,

        // Whitespace
        'method_chaining_indentation' => true,
        'no_spaces_around_offset' => true,

        // Semicolon
        'no_empty_statement' => true,
        'no_singleline_whitespace_before_semicolons' => true,
        'semicolon_after_instruction' => true,
        'space_after_semicolon' => ['remove_in_empty_for_expressions' => true],

        // String Notation
        'escape_implicit_backslashes' => true,
        'explicit_string_variable' => true,
        'heredoc_to_nowdoc' => true,
        'single_quote' => true,

        // Operator
        'binary_operator_spaces' => true,
        'concat_space' => ['spacing' => 'one'],
        'object_operator_without_whitespace' => true,
        'unary_operator_spaces' => true,
        'standardize_not_equals' => true,
        'ternary_to_null_coalescing' => true,

        // list_syntax
        'list_syntax' => true,

        // PHP Tag
        'linebreak_after_opening_tag' => true,

        // Import
        'no_unused_imports' => true,

        // Namespace Notation
        'no_leading_namespace_whitespace' => true,

        // Language Construct
        'combine_consecutive_issets' => true,
        'dir_constant' => true,
        'explicit_indirect_variable' => true,
        'function_to_constant' => true,

        // Class Notation
        'class_attributes_separation' => ['elements' => [
            'const' => 'none',
            'method' => 'one',
            'property' => 'only_if_meta',
            'trait_import' => 'only_if_meta'
        ]
        ],
        'no_null_property_initialization' => true,
        'protected_to_private' => true,
        'self_accessor' => true,

        // Function Notation
        'function_typehint_space' => true,
        'return_type_declaration' => ['space_before' => 'none'],
        'void_return' => true,

        // Return Notation
        'simplified_null_return' => true,

        // Control Structure
        'include' => true,
        'no_trailing_comma_in_list_call' => true,
        'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false],
        'no_unneeded_control_parentheses' => true,
        'no_unneeded_curly_braces' => true,

        // Naming
        'no_homoglyph_names' => true,

        // PHPUnit
        'php_unit_construct' => true,
        'php_unit_dedicate_assert' => true,
    ]);


```

上記で設定した内容は以下サイトから細かく設定できるため適宜確認してください。

https://mlocati.github.io/php-cs-fixer-configurator/#version:3.1

https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/master/doc/rules/index.rst

https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/master/doc/ruleSets/index.rst

```shell
# dry-run実行
$ sail php ./vendor/bin/php-cs-fixer fix -v --diff --dry-run

# 実行
$ sail php ./vendor/bin/php-cs-fixer fix -v
```

PHP-CS-Fixerは実際にファイル修正まで行うため、
実行する前に`--dry-run`オプションをつけて事前確認をすることがおすすめです。

## Composer コマンドの登録

上記だけでも静的解析と自動整形は実施することは可能ですが、
コマンドを簡略化するために`composer.json`ファイルにエイリアスを登録します。

```json:composer.json
    "scripts": {

# ...省略

        "phpstan": [
            "@php ./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G"
        ],
        "psalm": [
            "@php ./vendor/bin/psalm"
        ],
        "dry-lint": [
            "@php ./vendor/bin/php-cs-fixer fix -v --diff --dry-run"
        ],
        "lint": [
            "@php ./vendor/bin/php-cs-fixer fix -v --diff"
        ],
        "fix": [
            "@php ./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
            "@php ./vendor/bin/psalm",
            "@php ./vendor/bin/php-cs-fixer fix -v --diff --dry-run"
        ]
```

## GitHubActionsで自動化

これまでに設定しただけでもローカルでコマンドを叩けば静的解析ツールを実行することができます。
一方で各個人の手動に任せておくと実施漏れが多発することは容易に想像できます。

そこで自動化の出番になります。

GitHubActionsとはGitHubが提供するCI/CDのためのワークフローエンジンです。 

ワークフローエンジンは、ビルド、テスト、デプロイといったCI/CD関連のワークフローを実行し、定期実行するワークフローを管理するなど、開発におけるソフトウェア実行の自動化を担います。

https://docs.github.com/ja/actions

今回はgithubリポジトリにpushするタイミングで静的解析・整形を実施しますが、タイミングや実施内容は細かくカスタマイズすることが可能になります。

そこでワークフローを制御するファイルが`.yml`のファイルになります。

```shell
# 設定ファイルの作成
$ mkdir -p .github/workflows && touch .github/workflows/fix.yml
```

```yaml:fix.yml
name: fix

on:
  pull_request:

jobs:
  fix:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.1'
        tools: composer:v2
    - name: Resolve dependencies
      run: composer install --no-progress --prefer-dist --optimize-autoloader
    - name: Run phpstan
      run: ./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G
    - name: Run psalm
      run: ./vendor/bin/psalm
    - name: Run php-cs-fixer
      run: ./vendor/bin/php-cs-fixer fix -v --diff --dry-run
```

ここでは、shivammathur/setup-phpというPHP環境上でcomposerを使ってインストールと上記で設定した各コマンドを実行しています。

上記でGithubActionsの設定も完了です。


## Github上で保護をかける

GitHubにはブランチ保護機能があり、特定のブランチに対してpushできなくしたり、レビューしてもらわないとマージできないようにしたり、CIや静的コード解析に失敗したらマージできないようにできます。今回はこの機能を使っていきます。

GitHubのリポジトリの`Settings` > `Branches` に移動し、Add ruleボタンを押します。
Branch name patternに main と入力します。

次に`Require status checks to pass before merging`にチェックを入れましょう。
上記で作ったGitHub Actionsが認識されていればymlファイル名で指定した`fix`が出てくるので、それをチェックします。

![スクリーンショット 2022-08-05 11.49.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1824740/d21b59d1-c126-4f2c-2093-82d73e9e4069.png)

これで静的解析チェックがクリアされなければマージされないようにできました。


## 動作検証

今回はシンプルな例で試してみます。

```php:Controller.php

final class Controller extends BaseController
{
    use AuthorizesRequests;
    use DispatchesJobs;
    use ValidatesRequests;

    // 以下を追記します
    public function csFixerError(): string{$hoge = 'hoge';$huga = 'huga';return $hoge . $huga;}
}

```

実際はこんなコードは書かないと思いますが、インデントもぐちゃぐちゃのコードです。
さらに別ブランチを切ってプッシュgithub上でPull Requestを作成してみましょう。

```shell
$ git switch -c feature_bad_code
$ git add --all 
$ git commit -m"bad code"
$ git push origin feature_bad_code
```

Pull Request画面では以下の画像のように新しくチェック項目が`fix`の名前で追加されています。

![スクリーンショット 2022-08-05 12.28.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1824740/abb7878d-495a-30e4-b27b-1fb5d5b7b322.png)

さらに`Details`を選択すると何が原因で弾かれたのかも確認できるようになっています。

![スクリーンショット 2022-08-05 12.22.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1824740/4ee5f468-9ad9-f9c2-4757-773fbc0e0850.png)

上記を修正して再プッシュすると無事通っていることが確認できます。

```shell
# PHP-CS-Fixerのみ修正
$ sail composer lint
```

![スクリーンショット 2022-08-05 12.31.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1824740/ffd03d47-42e4-52e6-bdc0-36cbff92d35b.png)