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層です。

  1. ローカル:エディタ保存時に自動整形(開発者の手を止めない)
  2. コミット時:変更ファイルだけに整形を強制(崩れたコードを入れさせない)
  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

確認方法

  1. わざとインデントや array() 記法を崩したコードを書く
  2. npm run lint / composer cs:check がローカルで失敗することを確認
  3. npm run lint:fix / composer cs:fix で自動修正されることを確認
  4. 崩れたまま PR を出し、CI が赤くなることを確認
  5. エディタで保存し、保存時に自動整形されることを確認

この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 だけに頼らない。エディタ保存時整形とコミットフックで、できるだけ手前で潰すのが効きます。

まとめ

フロントとバックで言語もツールも違っても、やることは同じです。

  1. 整形(フォーマッタ)と検査(リンタ)の役割を分ける
  2. エディタ・コミット・CI の3層で機械に強制させる
  3. 既存コードの一括整形は独立したPRで、blame 汚染を避ける

スタイルは「議論する対象」ではなく「機械に任せて忘れる対象」です。そこに割いていたレビューの労力を、ロジックと設計に回せるようになります。

Sources:

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。