2026年7月27日
2026年7月27日
WordPressプラグイン開発の基礎|ヘッダーから有効化・削除処理、安全な設定画面まで
WordPressのプラグインは、プラグインヘッダーを書いたPHPファイル1つから始められます。実用に耐えるものにするには、直接アクセスの防止、有効化・無効化・削除時の後始末、そして入力のサニタイズ・出力のエスケープ・nonceと権限チェックをはじめから組み込むことが大切です。この記事では、管理画面で設定したお知らせ文を投稿の末尾に表示する小さなプラグインを作りながら、開発の基本を一通り身につけられるように説明します。
ファイル構成とプラグインヘッダー
プラグインは wp-content/plugins/ の下に専用のフォルダーを作って置きます。今回は次の構成にします。
wp-content/plugins/hello-notice/
├── hello-notice.php ← メインファイル(ヘッダーを書く)
└── uninstall.php ← 削除時に実行される後始末
WordPressはメインファイル先頭のコメントを読み取ってプラグインとして認識します。必須なのは Plugin Name だけですが、動作要件やテキストドメインを書いておくと、要件を満たさない環境での有効化を防げたり、翻訳の準備になったりします。
<?php
/**
* Plugin Name: Hello Notice
* Description: 管理画面で設定したお知らせ文を投稿の末尾に表示します。
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: Your Name
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: hello-notice
*/
defined( 'ABSPATH' ) || exit;
最後の1行は、プラグインのPHPファイルにブラウザーから直接アクセスされたときに処理を止めるためのものです。ABSPATH はWordPressの読み込み時に定義される定数なので、WordPressを経由せずに実行された場合は未定義になります。直接実行されて困る処理がなくても、エラーメッセージからサーバーのパスが漏れることを防げるため、すべてのPHPファイルの先頭に置くのが一般的です。
関数名・オプション名・フック名には、プラグイン固有の接頭辞(ここでは hn_)を付けます。WordPressではすべてのプラグインとテーマが同じ名前空間で動くため、get_settings のような一般的な名前は他のコードと衝突し、致命的エラーの原因になります。PHPの名前空間やクラスで囲む方法もありますが、まずは接頭辞を徹底するだけでも事故は大きく減ります。
有効化・無効化フック
有効化したときに一度だけ実行したい処理は register_activation_hook() で登録します。初期設定値の保存や、独自のテーブル作成が典型です。
register_activation_hook( __FILE__, 'hn_activate' );
function hn_activate() {
// 既に値がある場合は上書きしない(再有効化で設定が消えないように)
add_option( 'hn_settings', array(
'message' => '',
'enabled' => 0,
) );
}
register_deactivation_hook( __FILE__, 'hn_deactivate' );
function hn_deactivate() {
// 今回は消すものがない。WP-Cron の予定やリライトルールを登録した場合はここで解除する
}
つまずきやすいのは登録する場所です。register_activation_hook() はメインファイルの直下で呼び出してください。plugins_loaded や init の中で登録すると、有効化の処理が走る時点ではまだ登録されておらず、何も起きません。第1引数の __FILE__ も、メインファイル以外から呼ぶ場合はメインファイルのパスを渡す必要があります。
無効化は「一時的に止める」操作なので、利用者の設定データは消さないのが基本です。WP-Cronに wp_schedule_event() で予定を入れたなら wp_clear_scheduled_hook() で解除し、カスタム投稿タイプを登録して flush_rewrite_rules() を呼んだなら、無効化時にも再度呼んでパーマリンクを元に戻します。
設定画面をSettings APIで作る
設定画面は、自前でフォーム処理を書くよりも、WordPressのSettings APIを使うほうが安全で手数も少なくなります。settings_fields() がnonceを出力し、保存先の options.php がnonceと権限(既定は manage_options)を検証してくれるためです。開発者が担当するのは、保存する値のサニタイズと、表示するときのエスケープです。
add_action( 'admin_menu', 'hn_add_menu' );
function hn_add_menu() {
add_options_page(
'Hello Notice',
'Hello Notice',
'manage_options',
'hello-notice',
'hn_render_page'
);
}
add_action( 'admin_init', 'hn_register_settings' );
function hn_register_settings() {
register_setting( 'hn_group', 'hn_settings', array(
'type' => 'array',
'sanitize_callback' => 'hn_sanitize',
'default' => array( 'message' => '', 'enabled' => 0 ),
) );
}
function hn_sanitize( $input ) {
$input = is_array( $input ) ? $input : array();
return array(
'message' => isset( $input['message'] ) ? sanitize_textarea_field( $input['message'] ) : '',
'enabled' => empty( $input['enabled'] ) ? 0 : 1,
);
}
function hn_render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$opts = get_option( 'hn_settings', array() );
$message = isset( $opts['message'] ) ? $opts['message'] : '';
$enabled = ! empty( $opts['enabled'] );
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form method="post" action="options.php">
<?php settings_fields( 'hn_group' ); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="hn-message"><?php esc_html_e( 'お知らせ文', 'hello-notice' ); ?></label></th>
<td><textarea id="hn-message" name="hn_settings[message]" rows="4" class="large-text"><?php echo esc_textarea( $message ); ?></textarea></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( '表示', 'hello-notice' ); ?></th>
<td>
<label>
<input type="checkbox" name="hn_settings[enabled]" value="1" <?php checked( $enabled ); ?>>
<?php esc_html_e( '投稿の末尾に表示する', 'hello-notice' ); ?>
</label>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
サニタイズ関数では、受け取った配列をそのまま返さず、想定したキーだけを取り出して型をそろえています。こうしておくと、フォームに想定外の項目を足して送信されても、余計な値がデータベースに入りません。チェックボックスはオフのとき値が送られてこないため、empty() で0か1に正規化します。オプションがまだ存在しない状態で初めて保存すると、サニタイズ関数が2回呼ばれることがあります。同じ値を2回通しても結果が変わらない書き方にしておけば問題になりません。
表示側では、出力する場所に合ったエスケープ関数を使い分けます。HTMLの本文には esc_html()、属性値には esc_attr()、URLには esc_url()、テキストエリアの中身には esc_textarea() です。保存時にサニタイズしていても、表示時のエスケープは省きません。データベースの値が別の経路で書き換えられる可能性があるためです。
投稿の末尾にお知らせを出す
設定した文章を投稿本文の後ろに付け足すには、the_content フィルターを使います。
add_filter( 'the_content', 'hn_append_notice' );
function hn_append_notice( $content ) {
if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
$opts = get_option( 'hn_settings', array() );
if ( empty( $opts['enabled'] ) || empty( $opts['message'] ) ) {
return $content;
}
$notice = '<div class="hn-notice">' . nl2br( esc_html( $opts['message'] ) ) . '</div>';
return $content . $notice;
}
条件分岐を入れているのは、the_content が抜粋の生成やウィジェット、関連記事の表示など、メインの本文以外でも呼ばれるためです。条件がないと、一覧ページや関連記事の中にまでお知らせが重複して出ることがあります。改行は esc_html() でエスケープした後に nl2br() で <br> に変える順番にします。逆にすると、付け足した <br> までエスケープされて文字として表示されます。
独自のフォーム処理にはnonceと権限チェックを
Settings APIで扱えない処理、たとえば「設定を初期値に戻す」ボタンのような操作は、admin-post.php に送信して自分で処理します。このとき、nonceの検証と権限チェックの両方を行います。nonceは「本人が自分の画面から送信したか」を確かめるもので、権限の確認の代わりにはなりません。
// 設定画面の </div> の直前など、上の form の外に置く
function hn_render_reset_form() {
?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="hn_reset">
<?php wp_nonce_field( 'hn_reset', 'hn_reset_nonce' ); ?>
<?php submit_button( __( '初期値に戻す', 'hello-notice' ), 'delete', 'submit', false ); ?>
</form>
<?php
}
add_action( 'admin_post_hn_reset', 'hn_handle_reset' );
function hn_handle_reset() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'この操作を行う権限がありません。', 'hello-notice' ), '', array( 'response' => 403 ) );
}
check_admin_referer( 'hn_reset', 'hn_reset_nonce' );
update_option( 'hn_settings', array( 'message' => '', 'enabled' => 0 ) );
wp_safe_redirect( admin_url( 'options-general.php?page=hello-notice' ) );
exit;
}
hn_render_reset_form() は、設定画面の関数 hn_render_page() の中で、保存用フォームの閉じタグより後ろから呼び出します。HTMLではフォームを入れ子にできないため、保存用のフォームの中に置くと正しく動きません。admin_post_ に続く名前は、フォームの hidden 項目 action の値と一致させます。ログインしていない利用者からの送信を受けたい場合は admin_post_nopriv_ を使いますが、その場合も入力の検証は同じように行います。リダイレクト後は exit で処理を終えることを忘れないでください。
削除時の後始末 uninstall.php
プラグインを管理画面から削除したとき、WordPressはプラグインフォルダー内の uninstall.php を実行します。ここで、プラグインが作ったオプションやテーブルを消します。
<?php
// WordPress の削除処理以外から呼ばれたら何もしない
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
delete_option( 'hn_settings' );
WP_UNINSTALL_PLUGIN の確認は、このファイルにURLで直接アクセスされてデータが消されるのを防ぐためのものです。マルチサイトでネットワーク有効化していた場合は、get_sites() で各サイトを回り、switch_to_blog() で切り替えて削除し、restore_current_blog() で戻す処理が必要になります。register_uninstall_hook() を使う方法もありますが、コールバックに無名関数やインスタンスメソッドは使えないため、初めてなら uninstall.php のほうが分かりやすいでしょう。
開発中の確認方法
開発はローカル環境やステージングで行い、wp-config.php で WP_DEBUG と WP_DEBUG_LOG を true にして、wp-content/debug.log に警告が出ていないかを見ながら進めます。動作確認では次の点を見ておくと、公開後の不具合を減らせます。
- 有効化・無効化・再有効化を繰り返しても設定が消えない、エラーが出ない
- 管理者以外のユーザー(編集者・購読者)で設定画面のURLを直接開いても操作できない
- お知らせ文に
<script>などのタグを入れて保存しても、そのまま実行されず文字として表示される - プラグインを削除した後、データベースの
wp_optionsにhn_settingsが残っていない
コーディング規約に沿っているかは、PHP_CodeSnifferとWordPress Coding Standardsを使うと機械的に確かめられます。エスケープ漏れやnonce検証の抜けも警告されるため、レビューの補助として役立ちます(各ツールの対応バージョンは2026年10月現在のものを公式リポジトリで確認してください)。
まとめ
- プラグインはヘッダー付きのPHPファイル1つで動く。全ファイルの先頭に
ABSPATHの確認を置き、名前には固有の接頭辞を付ける - 有効化・無効化フックはメインファイル直下で登録する。無効化では利用者の設定を消さず、削除時の
uninstall.phpで後始末する - 設定画面はSettings APIを使うとnonceと権限の検証を任せられる。保存値は想定したキーだけをサニタイズして残す
- 出力は場所に合わせて
esc_html()・esc_attr()・esc_url()などでエスケープする - 独自のフォーム処理では、nonceの検証と
current_user_can()による権限チェックを両方行う