2026年8月6日

2026年8月6日

WordPressでフィーチャーフラグを実装する方法(定数・オプション・フィルターで段階公開)

WordPress のフィーチャーフラグは、判定をひとつの関数に集め、オプション(get_option)・wp-config.php の定数・フィルターの順で値を決める形にすると、プラグインを追加せずに小さく始められます。この記事では、その基本の関数から、管理者やベータ参加者だけに見せる方法、ユーザーIDのハッシュを使った割合での段階公開、役目を終えたフラグの片付けまでを順に説明します。

フィーチャーフラグで解決できること

フィーチャーフラグは「コードはデプロイ済みだが、機能をオンにするかどうかは設定で決める」ための仕組みです。WordPress のサイト運用では、次のような場面で役に立ちます。

  • 新しいヘッダーや購入フローを、まず管理者だけに表示して本番データで確認したい
  • 外部 API 連携に障害が出たとき、デプロイし直さずにその機能だけ止めたい(キルスイッチ)
  • 一部のログインユーザーから順に公開し、問い合わせやエラーの増え方を見ながら広げたい
  • ステージングでは有効、本番では無効のように、環境ごとに切り替えたい

フラグを使わずに「ブランチを分けて、準備ができたらマージ」で進めると、マージとリリースが同時になり、問題が出たときの戻し方がデプロイのやり直ししかありません。フラグがあれば、公開と停止を設定の変更だけで行えます。

判定関数をひとつに集約する

最初に、フラグの一覧と判定関数を用意します。テーマの functions.php に書くこともできますが、テーマを切り替えても残るように、自作プラグインや mu-plugins に置くのがおすすめです。

<?php
/**
 * Plugin Name: My Feature Flags
 */

// フラグの一覧。既定値と、片付けの目安を一緒に記録しておく
function my_feature_registry(): array {
	return array(
		'new_header'   => array( 'default' => false, 'owner' => 'web-team', 'remove_by' => '2026-12' ),
		'new_checkout' => array( 'default' => false, 'owner' => 'ec-team',  'remove_by' => '2027-01' ),
	);
}

function my_feature_enabled( string $flag, ?int $user_id = null ): bool {
	$registry = my_feature_registry();

	if ( ! isset( $registry[ $flag ] ) ) {
		// 登録されていないフラグは無効扱い。開発中に気づけるよう通知する
		_doing_it_wrong( __FUNCTION__, esc_html( "Unknown feature flag: {$flag}" ), '1.0.0' );
		return false;
	}

	// 1. 既定値
	$enabled = (bool) $registry[ $flag ]['default'];

	// 2. 管理画面や WP-CLI で保存した値(オプション)
	$saved = get_option( 'my_feature_flags', array() );
	if ( is_array( $saved ) && array_key_exists( $flag, $saved ) ) {
		$enabled = (bool) $saved[ $flag ];
	}

	// 3. wp-config.php の定数による強制上書き(例: MY_FLAG_NEW_HEADER)
	$const = 'MY_FLAG_' . strtoupper( $flag );
	if ( defined( $const ) ) {
		$enabled = (bool) constant( $const );
	}

	if ( null === $user_id ) {
		$user_id = get_current_user_id();
	}

	// 4. 最後にフィルターで調整できるようにする(ユーザー別・割合公開など)
	return (bool) apply_filters( 'my_feature_enabled', $enabled, $flag, $user_id );
}

優先順位を「既定値 → オプション → 定数 → フィルター」にしているのには理由があります。オプションは管理画面から気軽に変えられる値、定数はサーバー上の設定ファイルでしか変えられない値です。障害時に「この環境では管理画面の操作に関係なく止めておきたい」という強い停止をかけたいときは定数を使い、フィルターはユーザーごとの条件を足すために使います。

フラグ名は英小文字とアンダースコアだけにしておくと、定数名との対応がずれません。ハイフンを使うと MY_FLAG_NEW-HEADER のような定義できない名前になってしまいます。

使う側のコード

判定はテンプレートやフックの登録時に呼び出します。

// テンプレートでの出し分け
if ( my_feature_enabled( 'new_header' ) ) {
	get_template_part( 'template-parts/header', 'new' );
} else {
	get_template_part( 'template-parts/header' );
}

// 機能ごとフックを登録するかどうかを決める
add_action( 'init', function () {
	if ( my_feature_enabled( 'new_checkout' ) ) {
		require_once __DIR__ . '/includes/new-checkout.php';
	}
} );

ユーザーに関わる判定は、ログイン中のユーザーが確定してから呼び出してください。get_current_user_id() や current_user_can() はプラグインファイルが読み込まれた直後には使えないため、init 以降のフックの中で判定するのが安全です。

フラグの値を切り替える

WP-CLI で切り替える

運用中にもっとも手軽なのは WP-CLI です。オプションは連想配列として保存されているので、JSON で丸ごと更新するか、wp option patch で一部だけ書き換えます。

# 現在の値を確認
wp option get my_feature_flags --format=json

# 初回: まとめて保存
wp option update my_feature_flags '{"new_header":true,"new_checkout":false}' --format=json

# 2回目以降: 1つのキーだけ更新
wp option patch update my_feature_flags new_header 0

オブジェクトキャッシュを使っている環境でも、WP-CLI 経由の update_option はキャッシュを更新するため、通常はすぐ反映されます。ただし、ページキャッシュや CDN にキャッシュされた HTML は古いまま残るので、ログアウト状態の表示に関わるフラグを切り替えたら、ページキャッシュも削除してください。

管理画面に設定を用意する

技術担当以外も切り替える場合は、Settings API で簡単な設定画面を作ります。settings_fields() が nonce を出力し、options.php 側で権限と nonce が検証されるので、自前で保存処理を書くより安全です。

add_action( 'admin_init', function () {
	register_setting( 'my_feature_flags_group', 'my_feature_flags', array(
		'type'              => 'array',
		'default'           => array(),
		'sanitize_callback' => function ( $input ) {
			$clean = array();
			foreach ( array_keys( my_feature_registry() ) as $flag ) {
				$clean[ $flag ] = ! empty( $input[ $flag ] );
			}
			return $clean;
		},
	) );
} );

add_action( 'admin_menu', function () {
	add_options_page( 'Feature Flags', 'Feature Flags', 'manage_options', 'my-feature-flags', function () {
		$saved = (array) get_option( 'my_feature_flags', array() );
		echo '<div class="wrap"><h1>Feature Flags</h1><form method="post" action="options.php">';
		settings_fields( 'my_feature_flags_group' );
		foreach ( my_feature_registry() as $flag => $meta ) {
			printf(
				'<p><label><input type="checkbox" name="my_feature_flags[%1$s]" value="1" %2$s> %1$s(担当: %3$s / 削除目安: %4$s)</label></p>',
				esc_attr( $flag ),
				checked( ! empty( $saved[ $flag ] ), true, false ),
				esc_html( $meta['owner'] ),
				esc_html( $meta['remove_by'] )
			);
		}
		submit_button();
		echo '</form></div>';
	} );
} );

サニタイズ処理では、一覧に登録されたフラグだけを受け付けて真偽値に変換しています。フォームから想定外のキーが送られても保存されません。

ユーザーや権限ごとに有効にする

「まず管理者だけ」「ベータ参加を希望したユーザーだけ」といった条件は、my_feature_enabled フィルターで足します。

add_filter( 'my_feature_enabled', function ( bool $enabled, string $flag, int $user_id ): bool {
	if ( 'new_header' !== $flag || $enabled || ! $user_id ) {
		return $enabled;
	}

	// 管理者には先行して有効にする
	if ( user_can( $user_id, 'manage_options' ) ) {
		return true;
	}

	// ベータ参加を選んだユーザー(ユーザーメタに保存)
	$beta = (array) get_user_meta( $user_id, 'my_beta_features', true );
	return in_array( $flag, $beta, true );
}, 10, 3 );

ここでは current_user_can() ではなく、ユーザーIDを受け取る user_can() を使っています。判定関数に別のユーザーIDを渡したとき(たとえばメール送信の処理で宛先ユーザーを判定するとき)にも正しく動くようにするためです。

ユーザーIDのハッシュで割合ずつ公開する

ログインユーザーの10%、次に30%と段階的に広げたい場合は、ユーザーIDから安定したハッシュ値を作り、0〜99の番号に振り分けます。乱数を使うと、同じ人がページごとに新旧の画面を行き来してしまうため、同じユーザーが毎回同じ番号になる計算にするのがポイントです。

function my_feature_bucket( string $flag, int $user_id ): int {
	// フラグ名を混ぜることで、フラグごとに対象ユーザーの組み合わせが変わる
	return hexdec( substr( md5( $flag . '|' . $user_id ), 0, 7 ) ) % 100;
}

add_filter( 'my_feature_enabled', function ( bool $enabled, string $flag, int $user_id ): bool {
	$rollout = array( 'new_checkout' => 10 ); // 公開する割合(%)

	if ( $enabled || ! $user_id || ! isset( $rollout[ $flag ] ) ) {
		return $enabled;
	}
	return my_feature_bucket( $flag, $user_id ) < $rollout[ $flag ];
}, 20, 3 );

割合を 10 から 30 に増やしても、最初の10%に入っていたユーザーはそのまま対象に残ります。割合はコードに書く代わりにオプションに保存しておくと、デプロイせずに広げられます。

ログアウト状態の訪問者を割合で振り分ける場合は注意が必要です。ページキャッシュが有効なサイトでは、PHP で出し分けた HTML が最初の訪問者の内容のままキャッシュされ、他の人にも配信されます。未ログインの訪問者が対象なら、キャッシュの対象外にするか、出し分けを JavaScript 側で行う設計にしてください。

役目を終えたフラグを片付ける

フラグは増やすのは簡単ですが、放っておくと「どれが本番で有効なのか誰も分からない」状態になります。一覧に担当者と削除目安を書いているのはそのためです。全ユーザーへの公開が済み、しばらく問題がなければ、次の手順で取り除きます。

  1. コードから my_feature_enabled( 'フラグ名' ) の分岐を消し、新しい側の処理だけを残す
  2. my_feature_registry() から該当フラグを削除する
  3. 保存済みの値を消す(wp option patch delete my_feature_flags new_header)
  4. wp-config.php に定数を書いていた環境があれば、その行も削除する

オプションを書き換える前には wp option get my_feature_flags --format=json の結果を控えておくと、誤って別のフラグを消したときにすぐ戻せます。定期的に remove_by を過ぎたフラグを一覧で確認する運用にしておくと、片付け忘れを防げます。

外部のフラグ管理サービスを検討する目安

ここまでの実装で、多くの WordPress サイトには十分です。一方で、複数のアプリやモバイルアプリと同じフラグを共有したい、属性に応じた細かい対象指定や変更履歴の記録、承認フローが欲しい、といった要件が出てきたら、フィーチャーフラグ専用の SaaS やオープンソースのフラグ管理サーバーを検討する段階です。その場合も、WordPress 側の呼び出しを my_feature_enabled() に集約しておけば、関数の中身を外部サービスの SDK や API 呼び出しに差し替えるだけで移行できます。サービスの機能や提供条件は変わりやすいため、導入時に公式ドキュメントで確認してください(2026年10月現在)。

まとめ

  • フラグの判定は my_feature_enabled() のような関数ひとつに集め、既定値・オプション・定数・フィルターの順で値を決める
  • 切り替えは WP-CLI の wp option update / wp option patch、または Settings API の設定画面で行う
  • ユーザー別の判定は init 以降に行い、user_can() とユーザーメタで条件を足す
  • 割合公開はユーザーIDとフラグ名の安定したハッシュで振り分ける。未ログインの出し分けはページキャッシュとの衝突に注意する
  • 担当者と削除目安を記録し、公開が済んだフラグはコード・一覧・保存値から取り除く

お気軽にご相談ください

お見積りへ お問い合わせへ
▲