PHP-CS-FixerとPHP_CodeSnifferで整えるコーディング規約自動化
概要
チーム開発では、波カッコの位置や配列の記法といった些末なスタイルの差異がレビュー時間を奪い、diff を汚し、本質的な議論を埋もれさせます。この記事では、PHP の2大ツールである PHP-CS-Fixer と PHP_CodeSniffer(phpcs / phpcbf) を組み合わせ、コーディング規約を人手に頼らず機械的に担保する手順をハンズオン形式でまとめます。
ゴールは次の3つです。
- ローカルで保存時・コミット時に自動整形が走る状態を作る
- CI で規約違反を検知してマージをブロックする
- 2つのツールの役割を切り分け、設定の衝突を防ぐ
環境
- PHP 8.2 以上(8.1 は 2025 年末にセキュリティサポートが終了(EOL)しているため、新規に導入するなら 8.2/8.3 以上を前提にします)
- Composer 2.x
- PHP-CS-Fixer 3.x
- PHP_CodeSniffer 4.x
導入はプロジェクトローカル(composer require --dev)を前提とします。グローバルインストールは環境差でブレるため、チームでは避けるのが無難です。
なお PHP_CodeSniffer は、長らく配布元だった squizlabs/php_codesniffer が PHPCSStandards 組織へ移管されました。現在の推奨パッケージは phpcsstandards/php_codesniffer です。squizlabs/... も移管後の別名として引き続きインストール可能ですが、旧リポジトリは abandoned 扱いになっているため、新規導入では新パッケージ名を指定します。
2つのツールの役割を分ける
両ツールは目的が重なりつつも思想が違います。
| 観点 | PHP-CS-Fixer | PHP_CodeSniffer |
|---|---|---|
| 主目的 | コードの自動整形 | コードの静的検査 |
| 自動修正 | fix が本体 |
phpcbf で一部可能 |
| 設定ファイル | .php-cs-fixer.dist.php |
phpcs.xml.dist |
両方に「同じ項目について逆の意見」を持たせると、片方が直した箇所をもう片方が「違反」と言い続けます。そこで役割を次のように分けます。
- PHP-CS-Fixer: スタイル整形の一次担当。実際にコードを書き換える
- PHP_CodeSniffer: 整形では拾いにくい規約・独自ルールの検査担当
手順1: インストール
composer require --dev friendsofphp/php-cs-fixer composer require --dev phpcsstandards/php_codesniffer ./vendor/bin/php-cs-fixer --version ./vendor/bin/phpcs --version
PHP_CodeSniffer 4.0 はメジャー更新で、いくつか破壊的変更があります。既存プロジェクトを 3.x から上げる場合は、主に次の点に注意します。
- PHPCS の命名規約に沿わないカスタム sniff の
includeが廃止され、該当する sniff はエラーで停止する $phpcsFile->tokenizerTypeなど非推奨だった内部プロパティ・機能が削除された- 一部 sniff のエラーコードが変更された(例:
...SpacingAfterVisbility→...SpacingAfterVisibility)ため、phpcs.xml.dist側で特定コードを<exclude>している場合は追従が必要
独自ルールセットや自作 sniff を持っている場合は、公式の Version 4.0 Upgrade Guide に一度目を通しておくと安全です。本記事のように標準規約(PSR12 等)をベースにする範囲であれば、影響は限定的です。
手順2: PHP-CS-Fixer の設定
プロジェクトルートに .php-cs-fixer.dist.php を置きます。dist 付きはチーム共有用、個人の一時的な上書きは .php-cs-fixer.php(gitignore)で行うのが慣習です。
<?php
$finder = PhpCsFixer\Finder::create()
->in([__DIR__ . '/src', __DIR__ . '/tests'])
->exclude(['vendor', 'storage', 'bootstrap/cache'])
->name('*.php')
->notName('*.blade.php');
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']],
'single_quote' => true,
'binary_operator_spaces' => ['default' => 'single_space'],
'blank_line_before_statement' => ['statements' => ['return']],
])
->setFinder($finder);
挙動を変えうる「risky」ルールを使う場合は setRiskyAllowed(true) が必要ですが、導入時はデフォルト(false)から始め、必要なルールだけ個別に有効化する方が安全です。
手順3: PHP_CodeSniffer の設定
ルートに phpcs.xml.dist を置きます。PHP-CS-Fixer と衝突する整形系ルールを明示的に除外するのがポイントです。
<?xml version="1.0"?>
<ruleset name="ProjectStandard">
<description>プロジェクト共通のコーディング規約</description>
<file>src</file>
<file>tests</file>
<exclude-pattern>vendor/*</exclude-pattern>
<exclude-pattern>storage/*</exclude-pattern>
<arg name="colors"/>
<arg value="sp"/>
<arg name="extensions" value="php"/>
<rule ref="PSR12">
<!-- 整形は PHP-CS-Fixer に任せる領域は除外して衝突を避ける -->
<exclude name="PSR12.Operators.OperatorSpacing"/>
<exclude name="Generic.WhiteSpace.DisallowTabIndent"/>
</rule>
<rule ref="Generic.Metrics.CyclomaticComplexity">
<properties>
<property name="complexity" value="10"/>
<property name="absoluteComplexity" value="20"/>
</properties>
</rule>
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="120"/>
<property name="absoluteLineLimit" value="0"/>
</properties>
</rule>
</ruleset>
除外ルールは、両ツールを走らせて「片方が直し、片方が怒る」箇所が出たら、そのルール名を <exclude> に足す運用で詰めます。最初から完璧を目指さないのがコツです。なお 4.0 でエラーコードが変わった sniff もあるため、<exclude> に具体的なコード名を書いている場合は、phpcs が「未知のコードを除外しようとしている」旨を警告しないか確認しておきます。
手順4: Composer スクリプト化
{
"scripts": {
"cs:fix": "php-cs-fixer fix",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"sniff": "phpcs",
"sniff:fix": "phpcbf",
"lint": ["@cs:check", "@sniff"]
}
}
日常操作は次の2つに集約されます。
composer cs:fix # 整形を適用 composer lint # 整形チェック + 検査(CIと同じ基準)
手順5: コミット前フック
ステージ済みの PHP ファイルだけを対象にします。全ファイルを毎回なめると遅いので、差分に絞ります。
#!/bin/sh
# .git/hooks/pre-commit (chmod +x で実行権限を付与)
STAGED=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$')
[ -z "$STAGED" ] && exit 0
echo "$STAGED" | xargs ./vendor/bin/php-cs-fixer fix --path-mode=intersection --
echo "$STAGED" | xargs git add
echo "$STAGED" | xargs ./vendor/bin/phpcs
if [ $? -ne 0 ]; then
echo "phpcs で規約違反が検出されました。修正してください。"
exit 1
fi
exit 0
チーム共有は core.hooksPath か husky 相当の仕組みを使うとよいです。
手順6: GitHub Actions での CI
CI では修正はせず検知のみ(--dry-run)にして、CI がコードを勝手に書き換えないようにします。
name: Coding Standards
on:
pull_request:
push:
branches: [main]
jobs:
cs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
coverage: none
- run: composer install --prefer-dist --no-progress
- name: PHP-CS-Fixer (dry-run)
run: ./vendor/bin/php-cs-fixer fix --dry-run --diff
- name: PHP_CodeSniffer
run: ./vendor/bin/phpcs
--diff を付けておくと、CI ログに「どこをどう直すべきか」が差分で出るため、開発者はローカルで composer cs:fix を叩くだけで直せます。
動作確認
- わざと規約違反のコードを書く(
array()記法、未使用 import など) composer cs:check→ 違反が diff で出るcomposer cs:fix→ 自動で直るcomposer sniff→ 行長・複雑度などの違反が出る- PR 作成 → CI の
Coding Standardsジョブが赤くなりマージがブロックされる
cs:fix 後に sniff が同じ箇所を怒るなら、それが除外すべきルールです。
注意点
- risky ルールは慎重に。有効化したら必ずテストを通してからコミットする
- 既存コードへの一括適用は巨大 diff になる。整形だけの単独コミットを切り、
.git-blame-ignore-revsに SHA を登録して blame を汚さない - バージョン固定。整形結果の差異で CI が突然赤くならないよう、lock を CI とローカルで共有する
- フレームワーク標準の規約と重複しないか確認してからベースを決める
まとめ
- PHP-CS-Fixer = 整形、PHP_CodeSniffer = 検査と役割を分ければ設定が衝突しない
- 衝突するルールは phpcs 側で
<exclude>して潰す composer scriptsで操作を2コマンドに集約する- ローカルは pre-commit で軽く、CI は dry-run で最後の砦にする
規約の是非を人間が毎回議論するのをやめ、「設定ファイルに書いてあること=正」にしてしまうのが自動化の本質です。ツールに判断を委ねた分だけ、レビューは本当に見るべきロジックに集中できます。
