AIにコードを書かせる量が増えると、自分が読める速度を超えます。超えたときに何が起きるかというと、読まずに通すようになります。
そのとき効くのは、レビューを頑張ることではなく、壊れたら勝手に赤くなるものを、どれだけ用意してあるかでした。読まなくて済む量は、赤くなる仕組みの量で決まります。
WordPressプラグインを1年ほどこの形で作ってきたので、最初に用意する環境を全部まとめます。設定ファイルはそのままコピーして使える形にしてあります。上から読む必要はないので、必要なところだけ持って帰ってください。
先に、入れる順番だけ書いておきます。全部いっぺんに入れなくて大丈夫です。
-
.gitignoreとディレクトリ構成(10分) - PHPCS(コーディング規約の機械判定・30分)
- CLAUDE.md(AIに渡す規約・30分)
- PHPUnit(テスト・1〜2時間)
- GitHub Actions(CIで自動実行・30分)
- Plugin Check(WordPress.org の審査基準チェック・15分)
1と2だけでも、体感はかなり変わります。 3以降は、必要になってからで間に合います。
0. 前提と、全体の考え方
用意するものは、役割で3つに分かれます。
AIに渡す規約(CLAUDE.md)。何をしてほしいか、何をしないでほしいかを、言葉で書きます。守られないことがあります。
機械が判定するもの(PHPCS、PHPUnit、型チェック)。守られなければ、赤くなって止まります。
最後の関門(CI、Plugin Check)。手元で無視できても、ここは通らないと先へ進めません。
大事なのは、言葉で書いた規約に依存しないことです。CLAUDE.md に「関数は短く」と書いても、守られたり守られなかったりします。同じことを PHPCS のルールにすると、破った瞬間に落ちます。
言葉で書けることは、なるべく機械が判定できる形に変換する。 これが全体の方針です。
1. ディレクトリ構成と .gitignore
最初にやります。10分で終わって、あとの全部に効きます。
my-plugin/
├── my-plugin.php # メインファイル(プラグインヘッダ)
├── includes/ # PHP のクラス・関数
├── assets/ # ビルド前の CSS / JS
├── build/ # ビルド済み(git 管理外)
├── languages/ # .pot / .po / .mo
├── tests/ # PHPUnit
├── .github/workflows/ # CI
├── composer.json
├── package.json
├── phpcs.xml
├── phpunit.xml
├── CLAUDE.md
└── .gitignore
# .gitignore
# 依存
/vendor/
/node_modules/
# ビルド生成物
/build/
*.zip
# 環境
.env
.env.*
wp-config.php
# エディタ・OS
.DS_Store
.idea/
.vscode/
# ローカルのメモ(※後述の注意を読んでください)
/notes/
/scratch/
ここで一度だけ考えてほしいこと
.gitignore に入れたものは、git から復元できません。
自分は一度、git 管理外にしていた作業メモをまとめて失いました。/notes/ のようなディレクトリを除外するなら、そこは「消えたら戻らない場所」だと分かったうえで置いてください。別の場所にバックアップするか、いっそ git に入れてしまうか。
AIに作業させるなら、なおさらです。AIから見ると、git 管理下のファイルと管理外のファイルは同じに見えます。 「git にあるから戻せる」という前提が、片方だけ成立しません。
2. PHPCS(コーディング規約を機械が判定する)
いちばん費用対効果が高いのが、ここだと思います。
AIが書いたコードのレビューで、自分が言いたくなることの大半は、機械が言えます。長い。ネストが深い。エスケープしていない。プレフィックスが付いていない。全部、静的解析で拾えます。
インストール
composer require --dev \
squizlabs/php_codesniffer \
wp-coding-standards/wpcs \
phpcompatibility/phpcompatibility-wp \
dealerdirect/phpcodesniffer-composer-installer
composer.json に、プラグインの許可を書いておきます。これが無いとインストーラが動きません。
{
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"scripts": {
"lint": "phpcs",
"fix": "phpcbf",
"test": "phpunit"
}
}
確認します。
vendor/bin/phpcs -i
# WordPress, WordPress-Core, WordPress-Docs, WordPress-Extra,
# PHPCompatibilityWP などが並べば成功
phpcs.xml(コピペ用)
<?xml version="1.0"?>
<ruleset name="My Plugin">
<description>WordPress プラグインのコーディング規約</description>
<!-- 対象 -->
<file>.</file>
<exclude-pattern>/vendor/*</exclude-pattern>
<exclude-pattern>/node_modules/*</exclude-pattern>
<exclude-pattern>/build/*</exclude-pattern>
<exclude-pattern>/tests/fixtures/*</exclude-pattern>
<!-- 実行時の既定オプション(毎回打たなくて済む) -->
<arg name="extensions" value="php"/>
<arg name="colors"/>
<arg name="parallel" value="8"/>
<arg value="ps"/><!-- 進捗とスニペットを表示 -->
<!-- WordPress 標準 -->
<rule ref="WordPress"/>
<!-- PHP のバージョン互換 -->
<config name="testVersion" value="8.1-"/>
<rule ref="PHPCompatibilityWP"/>
<!-- 関数・クラスのプレフィックス(衝突防止。WordPress.org の審査でも見られる) -->
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<properties>
<property name="prefixes" type="array">
<element value="myplugin"/>
<element value="MyPlugin"/>
<element value="MYPLUGIN"/>
</property>
</properties>
</rule>
<!-- テキストドメイン(翻訳漏れの検出) -->
<rule ref="WordPress.WP.I18n">
<properties>
<property name="text_domain" type="array">
<element value="my-plugin"/>
</property>
</properties>
</rule>
<!-- 対応する WordPress の最低バージョン -->
<config name="minimum_wp_version" value="6.5"/>
<!-- ここから、AIに書かせる前提での追加ルール -->
<!-- 関数が長くなりすぎるのを止める -->
<rule ref="Generic.Metrics.CyclomaticComplexity">
<properties>
<property name="complexity" value="10"/>
<property name="absoluteComplexity" value="20"/>
</properties>
</rule>
<rule ref="Generic.Metrics.NestingLevel">
<properties>
<property name="nestingLevel" value="3"/>
<property name="absoluteNestingLevel" value="5"/>
</properties>
</rule>
<!-- テストコードでは緩める(モックで長くなりがちなので) -->
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<exclude-pattern>/tests/*</exclude-pattern>
</rule>
</ruleset>
使い方
vendor/bin/phpcs # チェック
vendor/bin/phpcbf # 自動修正できるものを直す
vendor/bin/phpcs includes/foo.php # ファイル指定
vendor/bin/phpcs --report=summary # 件数だけ
既存コードが真っ赤になったら
途中から入れると、たいてい数百件出ます。ここで心が折れて、ルールを緩める人が多い。
順番があります。
# 1. まず自動修正で減らす
vendor/bin/phpcbf
# 2. 残りを種類別に見る
vendor/bin/phpcs --report=source | head -30
--report=source は、どのルールで何件出ているかを一覧にします。上位数件で全体の大半を占めていることが多いので、そこから潰します。
ここで大事なのは、緩めるならルールごと外して、理由を書くことです。
<!-- ❌ ファイル内に // phpcs:disable を撒く(理由が残らない) -->
<!-- ✅ ruleset で外して、理由を書く -->
<!--
出力バッファで生成済み HTML を書き換えるため、この時点で
enqueue ハンドルが存在しない。管理画面側は wp_add_inline_script を使用。
-->
<rule ref="WordPress.WP.EnqueuedResources">
<exclude-pattern>/includes/class-output-filter.php</exclude-pattern>
</rule>
コードに // phpcs:disable を撒くと、なぜ外したのかが半年後に分かりません。設定ファイルに理由付きで置くと、「この理由はまだ有効か」を後から判断できます。
そして、AIには phpcs:disable を使わせないでください。 放っておくと、エラーを消す最短経路としてこれを選びます。CLAUDE.md に明記します(後述)。
3. CLAUDE.md(AIに渡す規約)
機械で判定できないことだけを、ここに書きます。判定できることは PHPCS に移します。
# プロジェクト概要
- WordPress プラグイン。PHP 8.1+ / WordPress 6.5+
- 配布は WordPress.org(GPL)。有料版は別リポジトリ
- テキストドメインは `my-plugin`、プレフィックスは `myplugin_`
# 守ってほしい書き方
- 関数は20行以内、ネストは3段まで(PHPCS で判定される)
- 出力は表示直前にエスケープ(`esc_html` / `esc_attr` / `esc_url`)
- DB アクセスは必ず `$wpdb->prepare` を通す
- 外部入力は `wp_unslash` + `sanitize_*`、状態変更には nonce
- REST API には必ず `permission_callback` を書く
# やらないこと
- `// phpcs:disable` / `@phpstan-ignore` でエラーを黙らせない。根本を直す
- WordPress コア(wp-admin/ wp-includes/)と他人のプラグインを編集しない
- `vendor/` `node_modules/` `build/` の中を直接編集しない(生成物なので消える)
- git 管理外のディレクトリ(/notes/ など)を削除・移動しない
- 動作中のサイトの DB に書き込まない(明示的に依頼したときを除く)
# テストの書き方
- ルール(フィルタ、並び順、上限、検証、整形)は pure 関数に切り出して、
それ自体をテストする
- ファイル読み書き・HTTP・時刻は、引数で渡して差し替え可能にする
- 新しいテストは、対象を壊したときに赤くなることを確認してから完成とする
# 廃止した方針
<!-- 削除せず残す。古い方針が戻ってきたときの否定の根拠になる -->
- ~~エラーを戻り値で返す~~ → 廃止(例外に統一したため)。
今後は例外を投げる。戻り値方式が出てきたら、それは古い指示。
# 作業の進め方
- 大きな変更の前に、方針を先に提示してほしい
- 変更したファイルの一覧を、最後に出してほしい
- 分からない前提があれば、勝手に決めずに聞いてほしい
書き方のコツ
「読みやすく」ではなく、判定できる形で書きます。
❌ 読みやすいコードを書いてください
✅ 関数は20行以内、ネストは3段まで
禁止事項には理由を一行添えます。 理由があると、状況が変わったときに「これは当てはまるか」で判断してくれます。理由がないと、例外扱いで破られます。
「廃止した方針」の節を作ります。 方針を変えたとき、古い記述を消すのではなく、廃止として残す。消すと、手元からは消えるのに振る舞いには残っていて、あとから復活してきます。名指しで否定できる根拠を、文書に残しておきます。
長くしすぎないこと。 全部書くと、いちばん守ってほしい一行がその他大勢に埋もれます。常駐させるのは、プロジェクトの間ずっと変わらないものだけ。そのときだけ必要な指示は、依頼文で渡します。
4. PHPUnit(テスト)
WordPress プラグインのテストは、2種類に分けると楽になります。
pure 関数のテスト。WordPress を読み込まずに動きます。速い。ここを厚くします。
結合テスト。WordPress の関数やDBが必要なもの。遅い。最小限にします。
設計を先に変える
テストを増やす前に、テストできる形にするほうが効きます。
// ❌ テストしにくい:WordPress と時刻と DB が混ざっている
function myplugin_is_expired( $post_id ) {
$date = get_post_meta( $post_id, 'expire_at', true );
return strtotime( $date ) < time();
}
// ✅ ルールを pure 関数に切り出す
function myplugin_is_expired_at( string $expire_at, int $now ): bool {
return strtotime( $expire_at ) < $now;
}
// 呼び出し側に、WordPress と時刻を残す
function myplugin_is_expired( int $post_id ): bool {
$date = get_post_meta( $post_id, 'expire_at', true );
return myplugin_is_expired_at( (string) $date, time() );
}
判断のルールと、外の世界への接続を分ける。 これだけで、テストできる範囲が一気に広がります。時刻、ファイル、HTTP、DB は引数で渡す。
そして、これは CLAUDE.md に書いておきます。自分が心がけるのではなく、AIにそう書かせるためです。
phpunit.xml(コピペ用)
<?xml version="1.0"?>
<phpunit
bootstrap="tests/bootstrap.php"
colors="true"
failOnWarning="true"
failOnRisky="true"
beStrictAboutOutputDuringTests="true">
<testsuites>
<!-- WordPress 不要。速いのでこちらを厚くする -->
<testsuite name="unit">
<directory suffix="Test.php">tests/unit</directory>
</testsuite>
<!-- WordPress が要る。最小限に -->
<testsuite name="integration">
<directory suffix="Test.php">tests/integration</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">includes</directory>
</include>
</source>
</phpunit>
failOnWarning と failOnRisky を true にしておくのが要点です。アサーションのないテストや、警告が出ているテストを、緑のまま通さないためです。AIにテストを書かせると、たまに「実行しているだけで何も検証していないテスト」が混ざります。
テストの書き方(AIに渡す観点)
依頼するとき、観点を明示します。
以下の観点でテストを書いてください。
- 正常系
- 境界値(最小/最大、off-by-one)
- 空(空文字列、空配列、null)
- 不正入力(型違い、壊れたデータ)
- 過去に出たバグ(リグレッション)
そして、書いたテストが対象を壊したときに赤くなることを確認してください。
条件を反転させる、ガードを消す、などして赤くなるのを見てから戻す。
最後の一行が大事です。 壊れたコードでも通るテストは、何か別のものをテストしています。そして、確認しない限り、それは永遠に見えません。
緑になったら、テストの差分を先に見る
これは運用の話です。
AIに赤いテストを直させて、緑に戻ったとき、直っていたのが実装ではなくテストだったことがあります。厳しくチェックしていた行が、緩い書き方に変わっている。緑は緑でも、基準が下がっただけの緑です。
# 緑になったら、まずこれを見る
git diff -- tests/
実装より先にテストの差分を見る。習慣にすると、静かな劣化を捕まえられます。
5. GitHub Actions(CIで自動実行)
手元で無視できるものは、いずれ無視されます。CI に置くと、無視できなくなります。
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: PHPCS
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
coverage: none
tools: composer
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHPCS
run: vendor/bin/phpcs --report=full --report-checkstyle=phpcs.xml
test:
name: PHPUnit (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
tools: composer
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run unit tests
run: vendor/bin/phpunit --testsuite unit
build:
name: Build assets
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
対応する PHP のバージョンを、matrix で回す
プラグインを配布するなら、これが効きます。手元は8.3でも、使う人の環境は8.1かもしれません。手元にない環境を持つ方法は、CI しかありません。
自分の場合、これで「ローカルは通るのに、古い PHP で落ちる」を何度か捕まえました。
ゲートにするか、レポートにするか
CI に足すとき、毎回考えることがあります。落とすのか、報告だけにするのか。
自分の基準は「既存の負債があるか」です。
負債がゼロなら、最初からゲートにします。 落ちたら止まる。新しく増えることがなくなります。
負債があるなら、まず報告だけにします。 初日からブロックすると、回避の作法(phpcs:disable を撒く)を覚えます。ゼロまで減らしてから、ゲートに上げる。
- name: Run PHPCS
run: vendor/bin/phpcs
continue-on-error: true # 負債を返し終わるまでは報告のみ
減らし終わったら、この行を消します。
6. Plugin Check(WordPress.org の審査基準)
WordPress.org で配布するなら、これを入れておくと差し戻しが減ります。公式が出している、審査基準のチェックツールです。
ローカルで
wp plugin install plugin-check --activate
wp plugin check my-plugin
CI で
公式の GitHub Action があります。
plugin-check:
name: Plugin Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: wordpress/plugin-check-action@v1
with:
build-dir: './build'
何を拾ってくれるか
自分が実際に指摘されたものだと、こういう類です。
-
wp_enqueue_*を使わずに<script>や<style>を出力している - ファイルの場所を
ABSPATH前提で判定している(移設された構成で壊れる) - 実行可能な PHP を書き出している
- readme.txt の形式不備、ライセンス表記漏れ
申請前に一度通しておくだけで、往復が1回減ります。 審査は1往復で数日かかることがあるので、ここは効きます。
7. i18n(翻訳)
後回しにすると、あとで全ファイルを直すことになります。最初から入れておきます。
# .pot を生成
wp i18n make-pot . languages/my-plugin.pot --domain=my-plugin
# .po から .mo を作る
wp i18n make-mo languages/
# JSON(ブロックエディタ向け)
wp i18n make-json languages/ --no-purge
PHPCS の WordPress.WP.I18n を有効にしてあるので、テキストドメインの書き間違いは lint で落ちます。 ここも機械に判定させる部分です。
8. 全部入れたあとの、日常の流れ
# 作業前
git switch -c feature/xxx
# 作業中(AIに書かせる)
# 作業後
composer lint # PHPCS
composer test # PHPUnit
npm run build # アセット
git diff -- tests/ # テストが緩んでいないか
git add -A && git commit
git push
# → CI が回る
ここまで用意すると、diff を全部読まなくても、壊れたら止まる状態になります。読まなくていい、という意味ではありません。読めなかったときに、代わりに止めてくれるものがある、という意味です。
早見でまとめ
- 用意するものは3種類。AIに渡す規約(言葉)/機械が判定するもの(lint・テスト)/最後の関門(CI・Plugin Check)
- 言葉で書けることは、なるべく機械が判定できる形に変換する
- 入れる順番は、.gitignore → PHPCS → CLAUDE.md → PHPUnit → CI → Plugin Check
- PHPCS は
Generic.Metrics.*で関数の長さと複雑さも縛る。AIは文句を言わない - 規約を緩めるなら、コードに
phpcs:disableを撒かず、ruleset に理由付きで書く - AIに
phpcs:disableを使わせない。エラーを消す最短経路として選ばれる - ルールは pure 関数に切り出す。時刻・ファイル・HTTP・DB は引数で渡す
- PHPUnit は
failOnWarning/failOnRiskyを true に。検証していないテストを緑にしない - 新しいテストは、対象を壊して赤くなるのを見てから完成とする
- 緑になったら、実装よりテストの差分を先に見る
- CI の PHP は matrix で複数バージョン。手元にない環境を持つ方法は CI しかない
- 既存の負債があるなら、まず報告だけ。ゼロにしてからゲートに上げる
-
.gitignoreに入れたものは git から戻らない。AIから見ると管理下も管理外も同じに見える
自分は、この環境を「AIに任せるため」に作ったつもりでしたが、実際には自分が読まなくても済むようにするためでした。読める量には限りがあって、その限界を超えたときに、代わりに止めてくれるものがあるかどうかで、半年後が変わります。
役に立ったらストックして、新しいプラグインを始めるとき見返してください。
ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。