ESLintとPHP-CS-Fixerで統一するコードスタイル:フロントとバックの両面から
概要
フロントエンドとバックエンドが分かれたプロジェクトでは、片方だけコードスタイルを整えても「全体が揃っている」感覚は得られません。レビューの指摘が「スタイル」と「ロジック」で混ざり、PRごとに差分がノイズまみれになります。
この記事では、フロント側を ESLint(+ Prettier)、バック側を PHP-CS-Fixer で統一し、最終的に CI とエディタの両方で「人間が気にしなくていい状態」まで持っていく手順をハンズオンでまとめます。言語もツールも違いますが、「フォーマッタとリンタを役割分担させる」「CI で同じ基準を機械に強制させる」という設計思想は共通です。
対象は、TypeScript/React のフロントと PHP(Laravel を想定)のバックが1リポジトリ、または隣接する2リポジトリで動いている構成です。
環境
手元では新規ディレクトリで再現確認しています。
- Node.js 24.x LTS / npm 11.x
- ESLint 10.x(Flat Config
eslint.config.js) - Prettier 3.x
- TypeScript 7.x
- PHP 8.5 / Composer 2.x
- PHP-CS-Fixer 3.x
ESLint は 10.x で .eslintrc.* 形式が完全に削除され、設定は Flat Config(eslint.config.js)のみになりました。そのため本記事も Flat Config で書きます。まだ ESLint 9 以前に留まっている環境の場合は後述の「注意点」を参照してください。
発生していた問題
複数人が触るプロジェクトで、こういう状態になっていました。
- インデントがスペース2とスペース4で混在
- import の順序がファイルごとにバラバラ
- PHP 側で
array()記法と[]記法が混在 - 「それ本質じゃないよね」というスタイル指摘がレビューの大半を占める
- フォーマッタをかける/かけないが人によって違い、無関係な差分が PR に乗る
スタイルの議論そのものに価値はありません。価値は「議論しなくていい状態」を作ることにあります。
原因
原因はシンプルで、「コードスタイルを人間の規律に任せていた」ことです。
- 設定ファイルはあるがエディタに統合されていない
- CI でスタイルを検査していないので、崩れても気づかない
- フォーマッタ(整形)とリンタ(検査)の役割が曖昧で、両方が同じことを争う
特に3つ目は見落としがちです。ESLint にもスタイル整形機能がありますが、整形は Prettier に寄せ、ESLint は「バグになりうるコードの検査」に専念させるのが現代的な分担です。PHP 側も同様に、PHP-CS-Fixer(整形)と静的解析(PHPStan 等。本記事では扱いません)を分けて考えます。
対応方法
方針は3層です。
- ローカル:エディタ保存時に自動整形(開発者の手を止めない)
- コミット時:変更ファイルだけに整形を強制(崩れたコードを入れさせない)
- CI:差分が出たら落とす(最終防衛線)
この3層を、フロントは ESLint+Prettier、バックは PHP-CS-Fixer でそれぞれ実装します。
実装例
フロントエンド:ESLint + Prettier
インストール
npm install -D eslint @eslint/js typescript-eslint \ prettier eslint-config-prettier \ eslint-plugin-import
ポイントは eslint-config-prettier です。これは「Prettier と競合する ESLint のスタイルルールを無効化する」ための設定で、整形は Prettier、検査は ESLint という役割分担を成立させます。
eslint.config.js(Flat Config)
// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import importPlugin from "eslint-plugin-import";
import prettier from "eslint-config-prettier";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.{ts,tsx}"],
plugins: {
import: importPlugin,
},
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/no-explicit-any": "warn",
"import/order": [
"error",
{
groups: ["builtin", "external", "internal", "parent", "sibling", "index"],
"newlines-between": "always",
alphabetize: { order: "asc", caseInsensitive: true },
},
],
},
},
prettier, // 競合するスタイルルールを最後に無効化(必ず末尾に置く)
];
prettier を配列の最後に置くのが重要です。先に定義されたスタイル系ルールを後ろから上書きして無効化するため、順序を間違えると競合が残ります。
.prettierrc
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}
package.json のスクリプト
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}
format:check は CI 用です。整形差分があると非ゼロで終了します。
バックエンド:PHP-CS-Fixer
インストール
composer require --dev friendsofphp/php-cs-fixer
.php-cs-fixer.dist.php
PHP-CS-Fixer は設定を PHP ファイルで書きます。@PSR12 をベースに、よくある表記ゆれを機械的に潰します。
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__ . '/app')
->in(__DIR__ . '/tests')
->name('*.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
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']],
'strict_comparison' => false,
'no_whitespace_in_blank_line' => true,
])
->setRiskyAllowed(false)
->setFinder($finder);
setRiskyAllowed(false) にしておくと、挙動を変えうる「risky」なルールが誤発動しません。strict_comparison のような意味が変わりうるものは、最初は無効にして、別途レビューしながら段階導入するのが安全です。
composer.json のスクリプト
{
"scripts": {
"cs:fix": "php-cs-fixer fix",
"cs:check": "php-cs-fixer fix --dry-run --diff"
}
}
--dry-run --diff は「直さずに差分だけ出す」モードで、CI で使います。整形が必要なら非ゼロ終了します。
エディタ統合(VS Code)
保存時に自動で揃うようにします。.vscode/settings.json をリポジトリにコミットしておくと、チーム全員の挙動が揃います。
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"[php]": {
"editor.defaultFormatter": "junstyle.php-cs-fixer"
}
}
これで「整形は Prettier / PHP-CS-Fixer」「ESLint の自動修正可能なものは保存時に適用」という役割分担がエディタ上でも成立します。
コミット時フック(任意だが推奨)
変更ファイルだけに整形をかけると速いです。フロント側は lint-staged + husky が定番です。
npm install -D husky lint-staged npx husky init
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.php": ["php-cs-fixer fix"]
}
}
# .husky/pre-commit npx lint-staged
CI(GitHub Actions 例)
最終防衛線です。ローカルでフックを入れていない人がいても、ここで止まります。
name: lint
on: [pull_request]
jobs:
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 24
- run: npm ci
- run: npm run lint
- run: npm run format:check
backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: shivammathur/setup-php@v2
with:
php-version: "8.5"
- run: composer install --no-interaction --prefer-dist
- run: composer cs:check
確認方法
- わざとインデントや
array()記法を崩したコードを書く npm run lint/composer cs:checkがローカルで失敗することを確認npm run lint:fix/composer cs:fixで自動修正されることを確認- 崩れたまま PR を出し、CI が赤くなることを確認
- エディタで保存し、保存時に自動整形されることを確認
この5つが通れば、3層(ローカル・コミット・CI)がすべて機能しています。
注意点
- 既存コードへの一括適用は別PRにする。整形だけの巨大差分とロジック変更を混ぜると、レビューもブレ追跡(
git blame)も壊れます。一括整形コミットは.git-blame-ignore-revsに登録しておくと blame が汚れません。 - ESLint と Prettier の競合は
eslint-config-prettierで解消する。両方にスタイルを持たせて殴り合わせない。 - risky ルールは段階導入。PHP-CS-Fixer の
strict_comparisonなど挙動を変えるものは、一括適用せずレビューしながら入れる。 - まだ ESLint 9 以前に留まっている環境では、
.eslintrc形式のままeslint-config-prettierをextendsの末尾に置く形で同じ分担を再現できます。ただし ESLint 10 では.eslintrc形式自体が完全に廃止されているため、この手は使えません。Flat Config へ移行してください(npx @eslint/migrate-configで既存の.eslintrcから変換できます)。 - CI だけに頼らない。エディタ保存時整形とコミットフックで、できるだけ手前で潰すのが効きます。
まとめ
フロントとバックで言語もツールも違っても、やることは同じです。
- 整形(フォーマッタ)と検査(リンタ)の役割を分ける
- エディタ・コミット・CI の3層で機械に強制させる
- 既存コードの一括整形は独立したPRで、blame 汚染を避ける
スタイルは「議論する対象」ではなく「機械に任せて忘れる対象」です。そこに割いていたレビューの労力を、ロジックと設計に回せるようになります。
Sources:
