追記と補足
Twitterにて諸先輩方からご指摘を頂いたのでそのやりとりを載せます。(許可もいただいてます)
要約すると、pre-commitで静的解析するよりもCIのタイミングで静的解析した方がいろいろいいことがあるよってところです。
そちらの意見を推奨なので、この記事は 「こういうこともできるよ。でももっといい運用があるから特別推奨はしないよ。」 程度の気持ちで読んでください。
ただ、せっかく慣れないシェルスクリプト書いたしまぁ使えないわけではないので元のスクリプトの部分も残してはおきます。
補足終わり。
PHPer向けのpre-commitスクリプト
Gitに上げる前に静的解析する為に作りました。
一応Githubにも上げました。
※Laravel想定なので、他のフレームワークの場合はphpstanの設定周りで若干読み替えてください。
pre-commitとは
設定した状態でgit commit
を実行するとコミットの前にこのスクリプトが走り、内容に問題があるとコミットが中止されます。(雑)
構成
- phpstan(larastan)
- php -l(phpstan成功時に実行)
- phpstanがどの程度検出できるか正直わからないので念の為
- php-cs-fixer
特定ディレクトリ以下などを対象に実行してしまうと改修範囲が膨大になり導入コストが上がってしまう為、
既存プロジェクトなどへの導入コスト低減の為コミット対象のファイルにのみ実行させている。
pre-commitファイル
シェルは慣れてないので書き方はよくないかもです。
もっと良い書き方があればコメントいただけると幸いです。
特にエラー判定のあたりは正直「本当にこれでいいのか?」と思いながら書いてます...。
あと後半は概ねデフォルトのまんま。
↓がpre-commitのファイルです。
#!/bin/sh
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=`git hash-object -t tree /dev/null`
fi
phpfiles=`git diff --name-only --diff-filter=d $against | grep \.php`
if [ "$phpfiles" != "" ]; then
echo '####\033[32m check phpstan \033[m####'
echo '####\033[32m check php-cs-fixer \033[m####'
echo '####\033[32m check php -l \033[m####'
echo
# ディレクトリ定義
ROOT_DIR=`git rev-parse --show-toplevel`
# エラー定義
errorStan=false
errorCs=false
errorL=false
for file in `git diff-index --name-status $against -- | grep -E '^[AUM].*\.php$'| cut -c3-`
do
# phpstan実行 全ファイルにチェックすると既存ファイルにエラーがある時コミットできないので、コミット対象のファイルのみチェックする
phpstanCheck=`./vendor/bin/phpstan analyze --memory-limit=2G $ROOT_DIR/$file`
# エラーがなければ'No errors'という文字列があるので、それがない場合にエラーと判断
echo $phpstanCheck | grep 'No errors' > /dev/null 2>&1
# $?には直前の実行結果が入る
if [ $? != 0 ]; then
echo "####\033[31m phpstan failed!!!! \033[m####$phpstanCheck\n"
errorStan=true
else
# php -lによるシンタックスチェック実行 app/配下以外だとphpstanが実行されない気がする?為入れてる
syntaxCheck=`php -l $file`
# エラーがなければ'No syntax errors'という文字列があるので、それがない場合にエラーと判断
echo $syntaxCheck | grep 'No syntax errors' > /dev/null 2>&1
if [ $? != 0 ]; then
# シンタックスエラーのあった場合はエラー内容を出力
echo "####\033[31m php -l failed!!!! \033[m####$syntaxCheck\n"
errorL=true
fi
fi
# php-cs-fixer実行 --dry-run で修正はせずに引っ掛かったらコミットさせずに修正を促す
# --path-modeをintersectionにすることで、Finderが無視されないようにする
.tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --path-mode=intersection --dry-run $ROOT_DIR/$file > /dev/null 2>&1
if [ $? != 0 ]; then
echo "####\033[31m php-cs-fixer failed!!!! \033[m####\n $ROOT_DIR/$file\n"
errorCs=true
fi
done
# "phpstan", "php-cs-fixer", "php -l"のどれかに引っ掛かっていたらコミットを中断
if "${errorStan}"; then
echo "####\033[31m Commit fail\033[m please fix \033[31mphpstan errors\033[m ####"
fi
if "${errorL}"; then
echo "####\033[31m Commit fail\033[m please \033[31msyntax check\033[m ####"
fi
if "${errorCs}"; then
echo "####\033[31m Commit fail\033[m please run \033[31m\"tools/php-cs-fixer/vendor/bin/php-cs-fixer fix\"\033[m command ####"
fi
if [ $errorStan -o $errorL -o $errorCs ]; then
exit 1
fi
echo '####\033[32m phpstan, php -l, php-cs-fixer all completed!! \033[m####'
fi
# この下はほぼ`git init`で生成されたデフォルトのまま
# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --bool hooks.allownonascii)
# Redirect output to stderr.
exec 1>&2
# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
echo "####\033[31m ここでエラーの場合はコミットしたファイルに空白行に余分なスペースがある可能性があります。\033[m ####
####\033[31m もしどうしても解決できない場合は'git commit -m \"commit message\" --no-verify'でprecommitを無効化させてください。(非推奨) \033[m ####"
cat <<\EOF
####
Error: Attempt to add a non-ASCII file name.
This can cause problems if you want to work with people on other platforms.
To be portable it is advisable to rename the file.
If you know what you are doing you can disable this check using:
git config hooks.allownonascii true
####
EOF
exit 1
fi
# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --
Set up
- macOS想定
- gitは導入済みな想定
- composerも導入済みな想定
静的解析に必要なライブラリの導入
- 各プロジェクトに
phpstan(larastan)
とphp-cs-fixer
が導入されている前提なので、これらの導入が必要。 - ここではLaravel想定なので、
phpstan
ではなくlarastan
を導入するが、Laravel以外の場合はphpstan
を導入してください。下に導入方法も書いてますが、古い可能性もあるので以下リンクの公式のインストールガイドを参照してください。
phpstan(larastan)の導入
$ cd path/to/your/project
$ composer require --dev nunomaduro/larastan
# Laravel以外の場合
# $ composer require --dev phpstan/phpstan
$ vim phpstan.neon # 好きなエディタでプロジェクトrootに以下を作成
↓phpstan.neon
です。ほぼ公式のまま。
# phpstan.neon(for larastan)
includes:
- ./vendor/nunomaduro/larastan/extension.neon
parameters:
paths:
- app
# The level 8 is the highest level
level: 5
ignoreErrors:
- '#Unsafe usage of new static#'
excludePaths:
checkMissingIterableValueType: false
php-cs-fixerの導入
$ cd path/to/your/project
$ mkdir -p tools/php-cs-fixer
$ composer require --working-dir=tools/php-cs-fixer friendsofphp/php-cs-fixer
$ echo .php_cs.cache >> .gitignore # .gitignoreに.php_cs.cacheを追記
$ vim .php_cs.dist # 好きなエディタでプロジェクトrootに以下を作成
↓.php_cs.dist
です。
こちらはsuinさんのソースコードの“赤ペン先生”PHP-CS-Fixerのインストールと設定をありがたく流用させていただいています。感謝。
長いので折りたたんでます。
<?php
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
$rules = [
'array_syntax' => ['syntax' => 'short'],
'binary_operator_spaces' => [
'default' => 'single_space',
'operators' => ['=>' => null]
],
'blank_line_after_namespace' => true,
'blank_line_after_opening_tag' => true,
'blank_line_before_statement' => [
'statements' => ['return']
],
'braces' => true,
'cast_spaces' => true,
'class_attributes_separation' => [
'elements' => ['method']
],
'class_definition' => true,
'concat_space' => [
'spacing' => 'none'
],
'declare_equal_normalize' => true,
'elseif' => true,
'encoding' => true,
'full_opening_tag' => true,
'fully_qualified_strict_types' => true, // added by Shift
'function_declaration' => true,
'function_typehint_space' => true,
'heredoc_to_nowdoc' => true,
'include' => true,
'increment_style' => ['style' => 'post'],
'indentation_type' => true,
'linebreak_after_opening_tag' => true,
'line_ending' => true,
'lowercase_cast' => true,
'lowercase_constants' => true,
'lowercase_keywords' => true,
'lowercase_static_reference' => true, // added from Symfony
'magic_method_casing' => true, // added from Symfony
'magic_constant_casing' => true,
'method_argument_space' => true,
'native_function_casing' => true,
'no_alias_functions' => true,
'no_extra_blank_lines' => [
'tokens' => [
'extra',
'throw',
'use',
'use_trait',
]
],
'no_blank_lines_after_class_opening' => true,
'no_blank_lines_after_phpdoc' => true,
'no_closing_tag' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_mixed_echo_print' => [
'use' => 'echo'
],
'no_multiline_whitespace_around_double_arrow' => true,
'multiline_whitespace_before_semicolons' => [
'strategy' => 'no_multi_line'
],
'no_short_bool_cast' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_spaces_after_function_name' => true,
'no_spaces_around_offset' => true,
'no_spaces_inside_parenthesis' => true,
'no_trailing_comma_in_list_call' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_trailing_whitespace' => true,
'no_trailing_whitespace_in_comment' => true,
'no_unneeded_control_parentheses' => true,
'no_unreachable_default_argument_value' => true,
'no_useless_return' => true,
'no_whitespace_before_comma_in_array' => true,
'no_whitespace_in_blank_line' => true,
'normalize_index_brace' => true,
'not_operator_with_successor_space' => true,
'object_operator_without_whitespace' => true,
'ordered_imports' => ['sortAlgorithm' => 'alpha'],
'phpdoc_indent' => true,
'phpdoc_inline_tag' => true,
'phpdoc_no_access' => true,
'phpdoc_no_package' => true,
'phpdoc_no_useless_inheritdoc' => true,
'phpdoc_scalar' => true,
'phpdoc_single_line_var_spacing' => true,
'phpdoc_summary' => true,
'phpdoc_to_comment' => true,
'phpdoc_trim' => true,
'phpdoc_types' => true,
'phpdoc_var_without_name' => true,
'psr4' => true,
'self_accessor' => true,
'short_scalar_cast' => true,
'simplified_null_return' => false, // disabled by Shift
'single_blank_line_at_eof' => true,
'single_blank_line_before_namespace' => true,
'single_class_element_per_statement' => true,
'single_import_per_statement' => true,
'single_line_after_imports' => true,
'single_line_comment_style' => [
'comment_types' => ['hash']
],
'single_quote' => true,
'space_after_semicolon' => true,
'standardize_not_equals' => true,
'switch_case_semicolon_to_colon' => true,
'switch_case_space' => true,
'ternary_operator_spaces' => true,
'trailing_comma_in_multiline_array' => true,
'trim_array_spaces' => true,
'unary_operator_spaces' => true,
'visibility_required' => [
'elements' => ['method', 'property']
],
'whitespace_after_comma_in_array' => true,
];
$finder = Finder::create()
->in([
__DIR__ . '/app',
__DIR__ . '/config',
__DIR__ . '/database',
__DIR__ . '/resources',
__DIR__ . '/routes',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return Config::create()
->setFinder($finder)
->setRules($rules)
->setRiskyAllowed(true)
->setUsingCache(true);
Note: ※これ以下のgitの設定については詳しくは公式ドキュメントを参照してください。
端末の全リポジトリに共通で設定する場合
{pre-commitファイルをダウンロード}
$ mkdir -p ~/.config/git/hooks
$ mv ~/Downloads/pre-commit ~/.config/git/hooks/pre-commit
$ chmod +x ~/.config/git/hooks/pre-commit
$ git config --global core.hooksPath '~/.config/git/hooks'
既にclone済みの特定のリポジトリにのみ設定する場合
{pre-commitファイルをダウンロード}
$ cd path/to/your/project
$ mkdir .git/hooks
$ mv ~/Downloads/pre-commit .git/hooks/pre-commit
$ chmod +x .git/hooks/pre-commit
既存のリポジトリには設定したくないが、これから新規にcloneするリポジトリは全て反映させたい場合
{pre-commitファイルをダウンロード}
$ mkdir -p ~/.git_template/hooks
$ mv ~/Downloads/pre-commit ~/.git_template/hooks/pre-commit
$ chmod +x ~/.git_template/hooks/pre-commit
$ git config --global init.templatedir '~/.git_template/hooks'
実行
ちなみに既知の不具合として、Sourcetreeで行うと色が微妙な感じになりますが、対応する予定はありません。
余談
- 本当はこれはエディタレベルでやってしまいたい。
- VS Codeの
intelephense
というPHPの拡張機能が優秀なのでみんな入れましょうね。 - 余裕ができたらGitLabのCI環境も整備したい。