綺麗なコードを維持するために便利な静的解析ツールを活用しつつ自動化して楽をしよう
チーム開発をしていると各個人によってインデントがずれていたり、型定コードの品質にバラつきが生まれてしまいますよね。
レビュー文化が定着している開発組織だとしても人力でコードの品質を担保するのはかなり労力がいるため現実的ではありません。
一定のルールを事前に設定しておき、そのルールにもっとって自動的に解析・整形を行うことは、低コストでコードの品質を担保することができるため非常にメリットが高いです。
今回の記事では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
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
先程のファイルに設定を追記します
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
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
ファイルにエイリアスを登録します。
"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
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
が出てくるので、それをチェックします。
これで静的解析チェックがクリアされなければマージされないようにできました。
動作検証
今回はシンプルな例で試してみます。
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
の名前で追加されています。
さらにDetails
を選択すると何が原因で弾かれたのかも確認できるようになっています。
上記を修正して再プッシュすると無事通っていることが確認できます。
# PHP-CS-Fixerのみ修正
$ sail composer lint