以前、PHP 8.2+ 環境で WP-CLI が吐く Deprecated ノイズを 3 段防御で吸収する方法を書きました。WP_CLI_PHP_ARGS 環境変数を前置して error_reporting を抑制するアプローチで、多くの環境では機能します。ところが「同じ設定をしているのに Deprecated が消えない」という状況が実際に発生しました。調べると、環境変数がそもそも届かない構造的な理由がありました。本稿はその原因の整理と、v1.6.8 で実装した 3 柱の修正を記録します。
環境変数が効かない理由 — phar + shebang の実行経路
あるエージェンシーから Xserver 上の複数サイト(本稿では「サイト A / B」と表記)で、プラグイン一覧の取得が大量の Deprecated メッセージで失敗するという報告を受けました。弊社実機(Xserver / PHP 8.2.30 / WP-CLI 2.7.1)で同様の症状を再現し、実行経路を追いました。
Xserver の /usr/bin/wp は典型的な phar 形式のバイナリです。内部は #!/usr/bin/env php の shebang から始まる PHP スクリプトで、実際の起動経路はこうなっています。
shell → /usr/bin/wp(shebang: #!/usr/bin/env php)
↓
env が php を探して起動
↓
php が phar を読み込んで WP-CLI 実行
この経路では、WP_CLI_PHP_ARGS 環境変数が PHP の起動オプションとして読まれません。WP_CLI_PHP_ARGS は WP-CLI が内部で -d フラグを php に渡すための変数ですが、php 自体を shebang 経由で起動している場合、WP-CLI が php に -d を注入する段階まで制御が届かないのが原因です。
# 効かない(Xserver の /usr/bin/wp は shebang 経由の phar 起動)
WP_CLI_PHP_ARGS="-d error_reporting='E_ALL & ~E_DEPRECATED'" wp plugin list --format=json
# 効く(php に直接 -d を渡す)
php -d error_reporting='E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED' /tmp/wp-cli-2.7.1.phar plugin list --format=json
弊社実機で確認した結果、前者では Deprecated が 407 行残り、後者では 0 行になりました。
柱 A — php 直叩き経路の検出と -d フラグ注入
修正の方向性は「wp_cli_path の先頭を見て、php 直叩き形式なら変換し、そこに -d を直接注入する」です。
core/wpcli_json.py に追加した inject_php_d_flag(wp_cli_path) は、wp_cli_path の先頭バイナリ名を確認して判断します。
- php / php8.2 / php82 等が先頭にあれば、php 直後に
-d error_reporting='E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED'を注入する - bare
wp/ phar 直呼び出しの場合は変換せず元のまま(別の経路の対処に委ねる)
def inject_php_d_flag(wp_cli_path: str) -> str:
parts = shlex.split(wp_cli_path)
if not parts:
return wp_cli_path
binary = os.path.basename(parts[0]).lower().rstrip(".exe")
if not (binary == "php" or binary.startswith("php")):
return wp_cli_path # bare wp / phar はそのまま
flag = "-d error_reporting='E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED'"
return shlex.join([parts[0], flag] + parts[1:])
os.path.basename を使うことで、Windows の \ と Linux の / 両方のパス区切りに対して basename が正しく取れるようにしています。
柱 B — JSON locate でノイズを通り抜ける
「-d 注入で Deprecated は消えたが、Notice / Warning 等の別のノイズが stdout に混入した場合にも対応したい」という判断から、出力の JSON 部分だけを取り出すヘルパーも追加しました。
extract_json_from_wpcli_output(stdout) は、stdout の中から最初の [ または { と、最後の ] または } を locate して、その範囲だけを json.loads に渡します。
def extract_json_from_wpcli_output(stdout: str) -> Any:
start = min(
(stdout.find(c) for c in ("[", "{") if c in stdout),
default=-1,
)
end = max(
(stdout.rfind(c) for c in ("]", "}") if c in stdout),
default=-1,
)
if start == -1 or end == -1 or start > end:
return None
return json.loads(stdout[start : end + 1])
PHP Fatal error / Parse error はブラケットを含まないか、含んでも JSON として invalid なため json.loads で例外になり None を返します。誤検出の余地が減ります。
柱 C — json.loads 9 箇所のタプル返却化
既存のコードには json.loads(stdout) を直接呼んでいる箇所が 9 つありました。これらをパース結果と生 stdout のタプルを返す形に統一し、パース失敗時も生の出力をエラーハンドリングに渡せるようにしました。
def parse_wpcli_json(stdout: str) -> tuple[Any, str]:
try:
return extract_json_from_wpcli_output(stdout), stdout
except (json.JSONDecodeError, TypeError):
return None, stdout
タプルで raw を保持することにより、「JSON として読めなかったが stdout には情報がある」場面でも、ログやデバッグ表示に生の出力を活用できます。
AST 静的解析テスト 46 件で回帰を防ぐ
3 柱の実装と並行して、tests/test_wpcli_deprecation_noise.py(46 件)を追加しました。
- php 検出境界テスト —
php/php8.2/php82は注入対象・wp/.phar直呼び出しは注入しないことを境界値で確認 - -d 注入の位置テスト — 注入後のコマンド文字列が正しい位置に
-dを持つことを検証 - JSON locate 7+ ケース — Deprecated ノイズ混入 / Fatal error のみ / 正常 JSON / 空文字列 等のパターン
- 構造的静的検証 — AST を使って既存の
json.loads呼び出し箇所がタプル返却パターンに移行されていることを確認
V40 では「防御パターンが正しく動くか」のテストでしたが、今回はさらに AST 静的解析テストを組み合わせ、「コード自体が意図した構造を保っているか」まで機械的に検証できる形にしました。テスト総数は 942 件から 993 件に増えました。
まとめ
環境変数ベースの Deprecated 抑制は、php コマンドを直接制御できる経路では機能しますが、/usr/bin/wp のような phar + shebang 経路では届きません。v1.6.8 の修正は 3 層で対応しています。
- 柱 A: wp_cli_path を解析して php 直叩き形式なら
-dを直接注入 - 柱 B: stdout を locate して JSON 範囲だけ抽出(残留ノイズへの耐性)
- 柱 C:
json.loads呼び出しをタプル返却に統一(raw 保持・堅牢化)
「環境変数が効かない」という現象に出会ったとき、それが「ツールの仕様上、その経路では効かない」という構造的な理由から来ているかどうかを確認するのが、見立てを絞り込む最初の一手になります。Deprecated の症状と v1.6.6 時点の防御設計については、PHP 8.2+ × 古い WP-CLI で出る Deprecated ノイズを 3 段防御で吸収する方法を合わせて読むと、対症療法と今回の構造修正の対比が整理されます。