LoginSignup
4
5

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-08-05

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

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

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

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

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

本記事の対象となる方

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

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

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

Laravel環境構築

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

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

$ curl -s "https://laravel.build/example-app?with=mysql" | bash
# クローンする場合
$ git clone git@github.com:WebEngrChild/laravel-stati-analysis.git
$ composer install

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

$ vim ~/.zshrc

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

$ source ~/.zshrc

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

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

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

Larastanのインストール

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

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

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

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

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

# ファイル作成
$ touch phpstan.neon
phpstan.neon
includes:
    - ./vendor/nunomaduro/larastan/extension.neon

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

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

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

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

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

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

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

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

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とはそれぞれ検知できるエラーが異なる場合があるため、私の場合は両方いれました。

# インストール
$ 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と異なり自動整形ツールになります。
インデントや空白、などのミスを指摘するだけでなく自動的に修正までしてくれる優れものです。

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

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

# 設定ファイルをプロジェクトディレクトリに追加
$ touch .php-cs-fixer.dist.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,
    ]);


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

# 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ファイルにエイリアスを登録します。

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関連のワークフローを実行し、定期実行するワークフローを管理するなど、開発におけるソフトウェア実行の自動化を担います。

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

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

# 設定ファイルの作成
$ mkdir -p .github/workflows && touch .github/workflows/fix.yml
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

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

動作検証

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

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を作成してみましょう。

$ 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

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

スクリーンショット 2022-08-05 12.22.47.png

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

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

スクリーンショット 2022-08-05 12.31.54.png

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5