TL;DR
- 10,000行超の
functions.phpを抱えるレガシーWordPressテーマで、DEV検証中の機能が別案件のPROD公開作業に巻き込まれて漏洩する事故が発生した。 - 根本対策は「ファイル分割 + ブランチ運用整備」だが、依存解析コストが高くデグレリスクも大きいため、まず動作レイヤでの防御=フィーチャーフラグ機構を最小実装で先行投入した。
- 実体はわずか2関数 + 設定ファイル1枚。環境判定と機能ON/OFFを独立させ、両方を同時に間違えないと事故が起きない二重防御を構造的に持たせている。
1. きっかけになった事故
ある月、コードレビュー時点では問題のなかった改修案件をPROD公開した直後、「画面に表示されてはいけないはずの管理機能が本番に出ている」という指摘が運用チームから入った。
調べると次の構図だった。
- 別チームが、別案件として新しい管理画面をDEV環境で検証中だった
- 検証コードは
functions.phpの中段あたりに 環境分岐なしで直書き されていた - 公開対象の案件をDEV→PROD同期する際に、巻き添えで一緒に公開された
つまり「コード混在型」の典型である。DEV検証中のコードと、PROD公開予定のコードが、同じファイル・同じブランチに同居していたため、デプロイ単位を絞れなかった。
functions.php をざっと数えると 10,513行 / トップレベル宣言が378。もはやファイル単位で「これは公開していい/いけない」を判別できる状態ではなかった。
2. 「ファイルを分割すれば解決」では済まない理由
最初に検討したのは当然「機能単位でファイル分割しよう」だった。が、現実的には以下の壁がある。
- 暗黙の依存が多い: グローバル定数、ヘルパー関数、フィルタフック、アクション登録順への依存があちこちに散らばっている
- 読み込み順序が崩れると即500: 関数定義より先にフックが走ると致命的
- テスト網が薄い: 機能ごとの回帰確認手段が限られているため、一括分割のデグレ検知が難しい
- そもそも公開タイミングが揃わない: 「分割PRが大きくなりすぎてマージできない」問題に必ずぶつかる
結論として、分割そのものはストラングラーパターンで時間をかけて進めるしかない。だが、それを待つ間にも同じ事故は再発し得る。
そこで、「分割は徐々にやるが、その前に、まず動作レイヤで事故を防ぐ層を入れる」 という順序を選んだ。これがフィーチャーフラグ機構である。
3. 設計目標
導入時に置いた制約は以下。
- 最小実装: 既存コードに大規模パッチを当てない。新規ファイル数も最小。
- 失敗時は安全側に倒す: 環境判定が不能なら「本番扱い」とし、検証中の機能は出さない。
- 二重で間違えない限り事故が起きない: 環境判定とフラグ定義は別の場所・別のレイヤに置く。
- フラグは一覧性のある単一ファイル: 散らかさない。レビュー可能にする。
- wp-cli / cronなどHTTPコンテキスト外でも動く: 環境判定がHTTPヘッダ依存だと夜間バッチで誤動作する。
4. 実装
実体は以下の3点。便宜上、関数プレフィックスを app_ としてマスクしている(実コードでは独自プレフィックス)。
4.1 環境判定 app_current_env()
戻り値は 'local' / 'dev' / 'prod' の3値。判定優先順位は以下の通り。
function app_current_env() {
// (1) wp-config.php で明示指定された定数を最優先
if ( defined( 'APP_ENV' ) ) {
$env = APP_ENV;
if ( in_array( $env, [ 'local', 'dev', 'prod' ], true ) ) {
return $env;
}
}
// (2) WordPress 標準の wp_get_environment_type()
if ( function_exists( 'wp_get_environment_type' ) ) {
switch ( wp_get_environment_type() ) {
case 'local': return 'local';
case 'development': return 'dev';
case 'staging': return 'dev';
case 'production': return 'prod';
}
}
// (3) ホスト名フォールバック (dev.example.com 系を 'dev' 扱い)
if ( isset( $_SERVER['HTTP_HOST'] ) && strpos( $_SERVER['HTTP_HOST'], 'dev.' ) === 0 ) {
return 'dev';
}
// (4) 判定不能 → 安全側に倒して 'prod'
return 'prod';
}
ポイントは (4) のフォールバック。判定根拠が一つも当たらなかった場合に 'local' や 'dev' を返してしまうと、未知の実行コンテキスト(wp-cli、cron、CLIスクリプト等)で検証中機能が動いてしまう。「分からないなら本番として扱う」を徹底する。
4.2 フラグ判定 app_feature_enabled($key)
function app_feature_enabled( $key ) {
static $flags = null;
if ( $flags === null ) {
$flags = include __DIR__ . '/feature-flags.php';
}
if ( ! isset( $flags[ $key ] ) ) {
return false; // 未定義キーはOFF
}
return in_array( app_current_env(), $flags[ $key ], true );
}
未定義キーを false で返すのも安全側設計。タイポしたフラグキーで意図せず有効化されることを避ける。
4.3 単一フラグ定義ファイル feature-flags.php
<?php
return [
'redirect_management' => [ 'local', 'dev' ],
// 公開承認後にここへ 'prod' を追加する
];
全フラグをここ一枚に集約する。ファイルが散らかると「どの機能がどの環境で動くか」が把握不能になり、結局元の問題に戻る。レビュー時にこのファイルだけ見れば、現在の環境別有効機能の全貌が掴める状態を保つ。
4.4 利用側
新規機能のエントリポイント(例: functions/admin/redirects.php)の先頭行でガードする。
<?php
if ( ! app_feature_enabled( 'redirect_management' ) ) {
return;
}
// 以下、検証中機能のコード本体
return で即抜けることで、フックや管理メニューの登録自体が走らない。「公開前機能は存在ごと無かったことになる」のがゴール。
5. なぜ「二重防御」と呼べるか
PROD環境で検証中機能が万が一動くには、独立した二つの間違いが同時に起きる必要がある。
- 間違い①:
wp-config.phpのAPP_ENVがPROD側で誤って'dev'等になっている - 間違い②:
feature-flags.phpで該当機能の許可配列に'prod'が誤って追加されている
| 状態 | 環境定数 | フラグ定義 | 結果 |
|---|---|---|---|
| 正常 | 'prod' | ['local','dev'] | OFF |
| 環境定数だけ間違える | 'dev' | ['local','dev'] | OFF(フラグ側で守られる) |
| フラグだけ間違える | 'prod' | ['local','dev','prod'] | OFF(環境定数側で守られる) |
| 両方間違える | 'dev' | ['local','dev','prod'] | ON(事故) |
変更場所が wp-config.php とテーマ内ファイルで物理的に分かれている点が重要。レビュアー・承認者も別になりやすく、二重間違いを構造的に起こしにくい。
6. 公開ワークフローへの組み込み
運用ルールとして以下を明文化した。
- 新規機能・検証中改修は 必ず
feature-flags.phpにキーを定義し、エントリポイントでapp_feature_enabled()ガードを入れる - フラグ初期値は
['local']または['local', 'dev']。'prod'は最初から書かない 'prod'の追加は、公開承認を得た後の独立PRとして行う(コード変更と公開フラグを別コミットに分ける)- 公開後、しばらく経過観察したらフラグごと撤去 → コードを「常時ON」のレギュラー扱いに昇格
特に 3 が肝で、「機能を作るPR」と「機能を本番公開するPR」を物理的に分けることで、公開判断のレビューを独立させられる。
7. ストラングラーパターンへの接続
このフラグ機構は単独でも機能するが、本来の到達点は functions.php のストラングラーパターンによる解体である。
想定している今後の動き方は以下。
- 新規機能は最初から
functions/features/以下に独立ファイルとして作る - 検証中のものは
functions/features-dev/に置き、フラグでガードする - 既存の
functions.php内の機能を、機能単位で順次features/配下に移植していく - 移植時はフラグで一時的に旧/新を切り替えられるようにし、リスクを下げる
- 最終的に
functions.phpは「読み込みオーケストレーション専用」まで痩せる
フィーチャーフラグはこの長期戦における 「いつでも切り戻せる」という保険 として機能する。分割の過程で問題が出ても、フラグを ['local'] に戻すだけで本番影響を即時遮断できる。
8. やらなかったこと、やらない理由
- GUIでのフラグ切替(管理画面化): やらない。フラグ状態の変更は PRレビューを必ず通すべき重要操作 なので、コード(Git履歴)に残す方が監査性が高い。
- フラグ単位のロールアウト%制御: BtoCのA/Bテスト基盤ではなく、デプロイ事故防止が目的なので不要。
- ユーザー属性ベースのフラグ: 同上。
- 外部サービス(LaunchDarkly等)導入: 月額コスト・依存追加に見合う規模ではない。PHP配列で十分。
「フィーチャーフラグ」という言葉は華やかな機能を想像させるが、ここで欲しいのは事故防止のためのスイッチ一枚だけだった。要件に対して過剰な道具を入れない判断は重要。
9. まとめ
- レガシー巨大ファイルを抱えるプロジェクトで、コード混在による公開事故が起きた
- 根本対策(分割)は時間がかかるので、その前に 動作レイヤでの防御層 を最小実装で入れた
- 実装は環境判定関数1つ・フラグ判定関数1つ・設定ファイル1枚のみ
- 判定不能なら本番扱い と 二か所同時に間違えないと事故にならない という2つの設計原則が肝
- これを土台にしてストラングラーパターンでの段階的解体に進む
「巨大化したコードを一気に直す勇気は出ないが、放置するともう一度事故が起きる」――同じ状況にいる方の参考になれば幸いです。


コメント