公開日 2026-04-20

AI生成コードを受け入れる最小品質ゲート(PHPStan + PHPUnit + CS Fixer)

AI生成コードを人間レビュー前にふるいにかける最小品質ゲートを、PHPStanとPHPUnitとPHP CS Fixerで構成する。

目次

  1. 前提環境
  2. 1. ゴールと非対象
  3. 2. 新規デモ環境を作る
  4. 3. 3ツールを導入して最小設定を置く
  5. 4. composer scripts で最小品質ゲートを作る
  6. 5. AI生成コードを置いて失敗を作る
  7. 6. format:check / analyse / test の失敗を読む
  8. 7. 修正して composer qa を通す
  9. 8. 人間レビュー前にどこまで機械で落とすか

PHP CS Fixer / PHPStan / PHPUnit を並べて、AI生成コードの最低限の受け入れゲートを作ります。
狙いは「整形崩れ」「明白な型不整合」「既知の振る舞い崩れ」をローカルで先に落とし、人間レビューに回す差分を絞ることです。

AI生成コードは、整形崩れ、nullable の扱いミス、既存仕様に対する小さな退行が混ざりやすいです。
人間レビューで毎回そこから拾うのではなく、まず機械で検出できるものを先にふるいにかけます。
AIが出した差分をそのままレビューへ流さないための、最小の受け入れ基準として扱います。

確認は composer format:checkcomposer analysecomposer test を主線にします。
最後に日常運用用の composer qa も作ります。
GitHub Actions や pre-commit は別記事に分けます。

前提環境

  • Windows 11
  • WSL2(Ubuntu)
  • VS Code(Remote - WSL)
  • Docker Desktop(WSL連携有効)

以降のコマンドは、特記がない限り WSL 側ターミナルで実行します。
サービス名は app に固定します。

本記事では新規デモのため PHP 8.5 を使います。
既存案件で 8.3 / 8.4 を使っている場合は、composer.json と Dockerfile の PHP バージョンを読み替えてください。

1. ゴールと非対象

この記事で到達する状態:

  • PHP CS Fixer / PHPStan / PHPUnit をローカル品質ゲートとして並べられる
  • AI生成コードを 1 回落としてから修正し、composer qa が通る状態まで持っていける
  • 人間レビュー前に機械で落とす範囲を説明できる

3 ツールの役割は次のとおりです。

  • PHP CS Fixer: 整形崩れを落とす
  • PHPStan: 明白な型不整合や静的に見える危険を落とす
  • PHPUnit: 期待している振る舞い崩れを落とす

この記事で扱わない内容:

  • GitHub Actions による自動実行
  • pre-commit / Husky などのフック運用
  • Rector、Larastan、Psalm などの追加ツール
  • セキュリティレビューや包括的な人間レビュー手順

ここで作る品質ゲートは万能ではありません。
要求妥当性、例外処理、設定、外部I/O、副作用、セキュリティは最終的に人間レビューへ残ります。
それでも、そこへ行く前に「明らかに崩れている差分」を落とせるだけで、人間レビューを例外処理や副作用の確認に使いやすくなります。

2. 新規デモ環境を作る

WSL 側のシェルで作業します。
Windows 側から始める場合は wsl で Ubuntu に入り、以下を実行してください。

# Windows側から始める場合のみ実行
# wsl
mkdir -p ~/projects/php-ai-code-quality-gate-demo
cd ~/projects/php-ai-code-quality-gate-demo
mkdir -p docker/php src tests
code .
VS Codeでデモディレクトリを開いた状態

compose.yml を作成します。

services:
  app:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    working_dir: /workspace
    volumes:
      - ./:/workspace
    command: ["sleep", "infinity"]

docker/php/Dockerfile を作成します。
mbstring は PHPUnit 実行要件に備えて先に入れておきます。

FROM php:8.5-cli

RUN apt-get update \
    && apt-get install -y --no-install-recommends git unzip libonig-dev \
    && docker-php-ext-install mbstring \
    && rm -rf /var/lib/apt/lists/*

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /workspace

composer.json は最小状態で始めます。

{
  "name": "example/php-ai-code-quality-gate-demo",
  "type": "project",
  "require": {
    "php": "^8.5"
  },
  "require-dev": {},
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Tests\\": "tests/"
    }
  }
}

composer.jsonrequirephp を先に入れておくのは、このデモの最低対応 PHP を明示するためです。
PHP CS FixerNo PHP version requirement found in composer.json 警告も防げます。

起動して確認します。

docker compose up -d --build
docker compose exec app php -v
docker compose exec app composer --version

執筆時点の確認例:

PHP 8.5.3 (cli)
Composer version 2.9.5
コンテナ起動後のPHPとComposerのバージョン確認

以後のコマンドは docker compose exec app ... に統一します。
Dockerfile を変更したときは、docker compose up -d --build を再実行してください。

詰まり時:

  • コンテナが起動しない: docker compose logs app
  • イメージ変更が反映されない: docker compose up -d --build

3. 3ツールを導入して最小設定を置く

次に、3 ツールを dev 依存として導入します。

docker compose exec app composer require --dev phpunit/phpunit:^13 phpstan/phpstan:^2.1 friendsofphp/php-cs-fixer:^3.89
docker compose exec app composer dump-autoload
docker compose exec app vendor/bin/phpunit --version
docker compose exec app vendor/bin/phpstan --version
docker compose exec app vendor/bin/php-cs-fixer --version

執筆時点で実際に解決されたバージョン例:

friendsofphp/php-cs-fixer 3.94.2
phpstan/phpstan           2.1.40
phpunit/phpunit           13.0.5
ツールバージョン確認の出力(PHPUnit 13)

記事では patch 番号を固定せず、レンジを ^13 / ^2.1 / ^3.89 にしています。 ただし、実際にどの patch が入ったかは composer show --direct で確認できます。

phpunit.xml を作成します。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
  <testsuites>
    <testsuite name="default">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>

phpstan.neon を作成します。

parameters:
  level: 5
  paths:
    - src
    - tests

今回は最小品質ゲートに集中するため、level: 5 のまま始めます。
最初から厳しくしすぎると、設定調整や既存コード対応が主題になりやすいため、導入しやすい level: 5 から始めます。
strict rules やフレームワーク専用設定は入れません。

.php-cs-fixer.dist.php を作成します。
対象は srctests に限定します。

<?php

$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__ . '/src')
    ->in(__DIR__ . '/tests')
    ->name('*.php')
    ->ignoreDotFiles(true)
    ->ignoreVCS(true);

return (new PhpCsFixer\Config())
    ->setRiskyAllowed(false)
    ->setRules([
        '@PSR12' => true,
        'array_syntax' => ['syntax' => 'short'],
        'single_quote' => true,
        'no_trailing_whitespace' => true,
    ])
    ->setFinder($finder);

詰まり時:

  • vendor/bin が見つからない: docker compose exec app ls -la vendor/bin
  • 依存解決に失敗する: docker compose exec app php -vdocker compose exec app composer --version を再確認

4. composer scripts で最小品質ゲートを作る

次に、読者向けの操作入口を composer.json の scripts に固定します。
qaformat:fix を含めないのがポイントです。

  • format:check: 非破壊チェック
  • format:fix: 手動修正時の補助
  • analyse: PHPStan 実行
  • test: PHPUnit 実行
  • qa: format:check -> analyse -> test

順番は、まず軽く直せる整形崩れを落とし、その次に静的解析、最後に実行コストの高いテストを見るためです。

PHP CS Fixer には check 専用コマンドがないため、fix --dry-run --diff を非破壊チェックとして使います。

この時点の composer.json は次の最終版に更新します。

{
  "name": "example/php-ai-code-quality-gate-demo",
  "type": "project",
  "require": {
    "php": "^8.5"
  },
  "require-dev": {
    "phpunit/phpunit": "^13",
    "phpstan/phpstan": "^2.1",
    "friendsofphp/php-cs-fixer": "^3.89"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Tests\\": "tests/"
    }
  },
  "scripts": {
    "format:check": "@php vendor/bin/php-cs-fixer fix --dry-run --diff",
    "format:fix": "@php vendor/bin/php-cs-fixer fix",
    "analyse": "@php vendor/bin/phpstan analyse",
    "test": "@php vendor/bin/phpunit",
    "qa": [
      "@format:check",
      "@analyse",
      "@test"
    ]
  }
}

Docker / WSL 環境では、Composer scripts の中でも vendor/bin/... を明示しておくほうが安定します。 phpunitphpstan を裸のコマンド名で書くと、環境によっては sh: 1: phpunit: not found のように解決できないことがあります。

qa を非破壊チェックだけで構成しておくと、CI に載せるときも考え方がぶれません。
composer.jsonrequirephp を残しておくと、PHP CS Fixer が参照する最低対応 PHP バージョンも明示できます。
整形を直したいときだけ、明示的に composer format:fix を実行します。

詰まり時:

  • composer analysecomposer test が見つからない: scripts のキー名と文字列を確認
  • qa が想定順で動かない: qa 配列の順番を確認
  • composer format:checkNo PHP version requirement found in composer.json と出る: composer.jsonrequirephp で最低対応 PHP バージョンを明示する
  • sh: 1: phpunit: not found のように出る: scripts を @php vendor/bin/phpunit / @php vendor/bin/phpstan analyse / @php vendor/bin/php-cs-fixer ... の形にする

5. AI生成コードを置いて失敗を作る

あえて崩れた初期コードを置きます。
例として DraftReplyBuilder を「AI生成コードの初期版」と見立てます。

この失敗版は、3種類の失敗を1回で見せるために意図的に人工的な崩れを混ぜています。
特に buildPriorityLabel() は、型エラーを再現するための補助メソッドです。

src/DraftReplyBuilder.php を作成します(失敗版)。

<?php
declare(strict_types=1);

namespace App;

final class DraftReplyBuilder{
public function build(?string $name): string{
$normalized = $this->normalizeName($name ?? '');

if ($normalized === '') {
if ($name === null) {
$priorityLabel = $this->buildPriorityLabel($name);
}

return 'Thank you for your message.';
}

return "Thank you, {$normalized}.";
}

private function normalizeName(string $name): string{
return trim($name);
}

private function buildPriorityLabel(int $priority): string{
return "P{$priority}";
}
}

この失敗版には、次の 3 つを入れています。

  • PSR-12 崩れ
  • null が入り得る値を int が必要なメソッドへ渡している静的解析エラー
  • 末尾 ! が欠けているため、テストが失敗する戻り値

tests/DraftReplyBuilderTest.php を作成します。

<?php

declare(strict_types=1);

namespace Tests;

use App\DraftReplyBuilder;
use PHPUnit\Framework\TestCase;

final class DraftReplyBuilderTest extends TestCase
{
    public function testBuildReturnsReplyWithExclamation(): void
    {
        $builder = new DraftReplyBuilder();

        $actual = $builder->build('Taro');

        $this->assertSame('Thank you, Taro!', $actual);
    }
}

この段階では、まだ qa は通りません。
次章で 3 つの失敗を個別に読みます。

詰まり時:

  • Class "App\\DraftReplyBuilder" not found: docker compose exec app composer dump-autoload
  • テストが検出されない: phpunit.xmltestsuite パスを確認

6. format:check / analyse / test の失敗を読む

qa 一発ではなく、3 つを個別に実行します。

composer qa は先頭から順に実行され、どこか 1 つで失敗するとそこで止まります。
そのため、最初は format:check / analyse / test を個別に見たほうが、どこで落ちたか把握しやすいです。

docker compose exec app composer format:check
docker compose exec app composer analyse
docker compose exec app composer test

format:check の失敗例:

1) src/DraftReplyBuilder.php
   ---------- begin diff ----------
-final class DraftReplyBuilder{
-public function build(?string $name): string{
+final class DraftReplyBuilder
+{
+    public function build(?string $name): string
composer format:check の失敗出力

ここで見ているのは、あくまで整形です。
ロジックの正しさや型安全性までは見ていません。

analyse の失敗例:

Line   src/DraftReplyBuilder.php
12     Parameter #1 $priority of method
       App\DraftReplyBuilder::buildPriorityLabel() expects int, null given.
       argument.type
composer analyse の失敗出力

ここで見ているのは、実行前に分かる明白な型不整合です。
今回は null 分岐の中で int が必要なメソッドへ null を渡しているため、PHPStan が落としてくれます。

test の失敗例:

Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'Thank you, Taro!'
+'Thank you, Taro.'
composer test の PHPUnit 13 失敗出力

ここで見ているのは、既知の期待値とのズレです。
整形が通っても、静的解析が通っても、期待している振る舞いは別途テストしないと守れません。

3つは似た品質チェックではなく、見る対象がそれぞれ違います。

整理すると、次の役割分担になります。

コマンド何を落とすか
composer format:check整形崩れ
composer analyse明白な型不整合や静的に見える危険
composer test既知の振る舞い崩れ

どれか 1 つだけでは足りません。
AI生成コードを受け入れる最小ゲートとしては、この 3 本を並べると役割が分かりやすいです。

7. 修正して composer qa を通す

次に DraftReplyBuilder.php を修正版へ更新します。

<?php

declare(strict_types=1);

namespace App;

final class DraftReplyBuilder
{
    public function build(?string $name): string
    {
        $normalized = $this->normalizeName($name);

        if ($normalized === '') {
            return 'Thank you for your message.';
        }

        return "Thank you, {$normalized}!";
    }

    private function normalizeName(?string $name): string
    {
        return trim((string) $name);
    }
}

必要なら整形もかけます。

docker compose exec app composer format:fix

その後、最終確認として qa を実行します。

docker compose exec app composer qa

成功例:

Found 0 of 2 files that can be fixed

[OK] No errors

OK (1 test, 1 assertion)
composer qa の成功出力(PHPUnit 13)

Found 0 of 2 files は、2ファイルを確認した結果、修正対象が 0 件だったという意味なので成功です。

これで、AI生成コードは「人間レビューへ渡す前に最低限通しておきたい品質ゲート」を越えた状態になりました。
qa が通ることは「公開可」ではなく、「人間レビューで見るべき論点を絞れた」という意味です。

詰まり時:

  • 修正後も analyse が落ちる: nullable の扱いとメソッド引数型を見直す
  • 修正後も test が落ちる: 期待値と実装の文字列を見比べる

8. 人間レビュー前にどこまで機械で落とすか

最後に、機械ゲートと人間レビューの境界を整理します。

機械ゲートで先に落とすもの
整形崩れPSR-12 崩れ、配列記法、空白、引用符
明白な型不整合null を受け取れない引数へ渡す、戻り値型との不整合
既知の振る舞い崩れ期待する文字列、戻り値、既存の仕様からの退行
人間レビューに残すもの
要求妥当性そもそもこの返信文でよいか、問い合わせ種別ごとの仕様に合っているか
セキュリティSQL 条件の抜け、権限チェック漏れ、入力値の扱い、外部API呼び出し、ログ出力
例外処理例外時にログだけ出して握りつぶしていないか、通知や再試行方針が妥当か
設定と運用環境変数、タイムアウト、監視、デプロイ方法
外部I/O と副作用DB更新、メール送信、Webhook、課金処理が二重実行されないか

機械ゲートは「明らかに崩れた差分を先に落とす」ためのものです。
レビューそのものを不要にするものではありません。

GitHub Actions で同じゲートを CI に載せる話は、別記事に分けたほうが主線がはっきりします。
この記事の形で、まずローカルの受け入れ基準を揃えることをおすすめします。

深掘りしたい場合は、次の記事がつながります。

  1. VS CodeでPHPコード整形をそろえる(EditorConfig + PHP CS Fixer最小導入)
  2. PHPStan入門(最初のエラー1件を直す)
  3. PHPUnit入門(最初のテスト1本)

次の一歩:

  1. composer qa をチームのローカル受け入れ基準として固定する
  2. 主要な振る舞いをテストへ増やす
  3. GitHub Actions で同じ順序のチェックを自動化する