WordPress プラグインをゼロから書く
WordPressプラグインをゼロから作成し、フック、有効化と無効化、設定ページ、サニタイズ、エスケープ、安全な保存まで学べます。
WordPress プラグインとは、最も単純に言えば wp-content/plugins の中にあるフォルダーで、プラグイン名を記したコメントヘッダーで始まる 1 つの PHP ファイルを含むものです。そのファイルを保存すれば、WordPress はプラグイン画面にそのプラグインを一覧表示します。
テーマの functions.php にスニペットを追加してきた方なら、その落とし穴はすでにご存じでしょう。テーマを切り替えると、そのコードも一緒に消えてしまいます。そのコードをプラグインに移せば、テーマを切り替えても機能が生き残ります。これこそが、サイトの挙動はプラグインに、見た目はテーマに属するべき主な理由です。
この記事では、小さなプラグインを最初から最後まで作り上げます。フッターに表示されるサイト通知で、安全に保存できる設定ページから編集可能なものです。すべてのコードブロックはプラグインとして有効な状態になっているため、どのセクションで手を止めても、そこまでの内容を有効化できます。
要点
- プラグインとは
wp-content/plugins内のフォルダーで、プラグイン名を記したコメントヘッダーを持つ 1 つの PHP ファイルを含むもの。Plugin Nameが唯一の必須ヘッダーフィールドです。 - プラグインのコードは、
add_action()またはadd_filter()で関数をフックに紐付けるまで何も実行されません。 - フィルターのコールバックは、受け取った値を必ず返さなければなりません。何も返さなければ、フィルター対象の値は空になります。
- 安全な設定ページは 4 つの層で構成されます。権限チェック、nonce、入力時のサニタイズ、出力時のエスケープです。
register_activation_hook()はデフォルト値の設定用、無効化は一時的なクリーンアップ用、恒久的なデータ削除はアンインストールの役割です。
WordPress プラグインのファイルとヘッダーを作成する方法
wp-content/plugins/site-notice/ というフォルダーを作成し、その中に site-notice.php というファイルを作ります。WordPress はプラグインフォルダー内の PHP ファイルを読み取り、プラグインヘッダーで始まるものを選び出すことで、プラグイン画面を構築します。ヘッダーを持つファイルはそれぞれ別個のプラグインとして扱われるため、ヘッダーは 1 つのファイルにのみ記述してください。
<?php
/**
* Plugin Name: Site Notice
* Description: Shows a short notice in the site footer, editable from Settings.
* Version: 1.0.0
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: OpenReplay Team
* License: GPL-2.0-or-later
* Text Domain: site-notice
*/
ヘッダー要件のページには、Update URI や Requires Plugins を含め、認識されるすべてのフィールドが記載されています。最もよく使うものは以下のとおりです。
| フィールド | WordPress における役割 | 必須 |
|---|---|---|
| Plugin Name | プラグイン一覧に表示される | はい |
| Description | 名前の下に表示される | いいえ |
| Version | 表示されるほか、更新の比較に使われる | いいえ |
| Requires at least / Requires PHP | 古い環境での有効化をブロックする | いいえ |
| Author, License, Text Domain | 帰属表示、ライセンス、翻訳用スラッグ | いいえ |
ファイルを保存すると、プラグイン画面にプラグインが表示されます。問題なく有効化できますが、まだ何もしません。
有効化フックと無効化フックは何をするのか
register_activation_hook() は、誰かがプラグインをオンにした瞬間に一度だけコールバックを実行します。そのため、初期オプション値をデータベースに書き込むのに適した場所です。register_deactivation_hook() は、プラグインが動作中にのみ必要だったものを破棄するためのもので、典型的な例はキャッシュです。オプションやカスタムテーブルを含め、恒久的に削除する作業は代わりにアンインストールの役割です。というのも、ユーザーは後で再び有効にするつもりでプラグインをオフにすることがよくあるからです。
ヘッダーの下に次を追加します。
function orp_activate() {
add_option( 'orp_notice_text', 'Welcome to the site.' );
}
register_activation_hook( __FILE__, 'orp_activate' );
function orp_deactivate() {
delete_transient( 'orp_notice_cache' );
}
register_deactivation_hook( __FILE__, 'orp_deactivate' );
第 1 引数の __FILE__ は、メインのプラグインファイルを指します。このプラグインはまだ何もキャッシュしていませんが、無効化コールバックはその型を示しています。一時的な生成物を片付け、保存済みのオプションはそのまま残す、ということです。すべての関数名とオプション名に付けた orp_ プレフィックスは、コアや他のプラグインとの衝突を防ぎます。
なぜプラグインのコードが実行されないのか
プラグインファイル内のコードは、それ自体では実行されません。WordPress は関数をフックに紐付けて初めてそれを実行し、それまでファイルは何も動きません。add_action() はフック名と callable を受け取り、任意で優先度を指定できます(デフォルトは 10)。
function orp_print_notice() {
$notice = get_option( 'orp_notice_text' );
if ( $notice ) {
echo '<p class="orp-notice">' . esc_html( $notice ) . '</p>';
}
}
add_action( 'wp_footer', 'orp_print_notice' );
テーマが wp_footer を発火させると、WordPress はそれに紐付いたすべての関数、つまり私たちの関数も含めて呼び出します。フロントエンドのページを読み込むと、閉じ body タグの直前に通知が表示されます。
フィルターを追加する: 値を変更して返す
アクションは WordPress のライフサイクルの特定の時点で関数を実行させるものですが、フィルターは関数に値を渡し、変更された値が返ってくることを期待します。フィルターのコールバックは必ず値を返さなければなりません。 何も返さなければ PHP は null を返し、WordPress はその null をそのまま引き継ぐため、フィルター対象だったものはサイトから消えてしまいます。body_class のコールバックで return を忘れれば body 要素からクラスが失われ、the_content で忘れればすべての投稿が空で描画されます。
function orp_body_class( $classes ) {
if ( get_option( 'orp_notice_text' ) ) {
$classes[] = 'orp-has-notice';
}
return $classes;
}
add_filter( 'body_class', 'orp_body_class' );
これは body クラスの配列を受け取り、通知が設定されているときに 1 つ追加し、その配列を返します。これにより、通知が有効な間はテーマ側でページのスタイルを変えることができます。
WordPress の設定ページはどう作るのか
設定ページは 3 つの登録から成ります。admin_menu フックでのメニューページの登録、そして admin_init フックでの設定(setting)とそのセクション・フィールドの登録です。これは設定 API の章で説明されているとおりです。add_options_page() はページを「設定」の下に配置し、register_setting() はオプション名と、そして何より重要な sanitize_callback を指定します。
function orp_settings_menu() {
add_options_page( 'Site Notice', 'Site Notice', 'manage_options',
'orp-site-notice', 'orp_settings_page_html' );
}
add_action( 'admin_menu', 'orp_settings_menu' );
function orp_settings_init() {
register_setting( 'orp_settings', 'orp_notice_text', array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
) );
add_settings_section( 'orp_main', 'Notice', '__return_false', 'orp-site-notice' );
add_settings_field( 'orp_notice_text', 'Notice text',
'orp_notice_field_html', 'orp-site-notice', 'orp_main' );
}
add_action( 'admin_init', 'orp_settings_init' );
function orp_notice_field_html() {
$value = get_option( 'orp_notice_text', '' );
echo '<input type="text" name="orp_notice_text" value="'
. esc_attr( $value ) . '" class="regular-text">';
}
function orp_settings_page_html() {
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( 'orp_settings' );
do_settings_sections( 'orp-site-notice' );
submit_button( 'Save Notice' );
?>
</form>
</div>
<?php
}
フォームは options.php に POST され、保存処理はコアが担当します。なお、register_setting() は default 引数も受け付けますが、ここでは有効化フックでデフォルト値を設定しています。どちらか一方の仕組みを選び、一貫して使ってください。
送信された値を安全に扱うには
権限チェックは「このユーザーは設定を保存してよいのか」に答えるものであり、nonce は「このユーザーは本当にこのフォームを送信するつもりだったのか」に答えるものです。クライアントのサイトに残す設定ページには両方が必要であり、nonce が権限チェックの代わりになることは決してありません。
ここでは、設定 API がほとんどを引き受けてくれます。settings_fields() が nonce を出力し、フォームが戻ってきたときにコアがそれを検証します。また、現在のユーザーが manage_options 権限を持っていない限り、コアは保存をブロックします。これは wp-admin/options.php が設定ページに対してデフォルトで適用する権限です。もし独自のフォームハンドラーを書く場合は、wp_nonce_field() で nonce を出力し、check_admin_referer() で検証してください。ハンドブック自身の例にならって、ページのコールバックに明示的なガードを追加します。
function orp_settings_page_html() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// ... form as above ...
}
チェックするのは権限(capability)であり、administrator のようなロール名ではありません。残る 2 つの層はすでに配置済みです。データベースへ入る際のサニタイズ(sanitize_callback としての sanitize_text_field)と、出力時のエスケープ(フィールドでの esc_attr()、フッターでの esc_html())です。この 2 つの操作は互いに置き換えられるものではなく、どちらかを省略することが、保存されたオプションをインジェクションの経路に変えてしまう原因になります。
有効化して動作を確認する
プラグイン画面で Site Notice を有効化し、フロントエンドを読み込んで、フッターに通知が表示されているか確認します。「設定」→「Site Notice」でテキストを変更し、再読み込みしてみましょう。何も起きない場合は、次のリストを順に確認してください。
- プラグインは実際に有効化されていますか(単に存在するだけではありませんか)?
add_action()やadd_filter()に渡した文字列は、定義済みの関数名と完全に一致していますか?<?phpの前に空白や出力がありませんか? ある場合、WordPress は有効化時に「予期しない出力(unexpected output)」を報告します。wp-config.phpのWP_DEBUGをオンにし、WP_DEBUG_LOGでエラーをwp-content/debug.logに書き出して、推測ではなく実際の PHP エラーを読んでください。
これで、デフォルトオプション、アクション、値を返すフィルター、そして nonce・権限チェック・サニタイザーを通して保存される設定ページを備えたプラグインができました。これは、まだ functions.php に住んでいるすべてのスニペットにコピーする価値のある基本形です。移して、プレフィックスを付け、フックし、「入力時サニタイズ・出力時エスケープ」のペアを保ったままにしましょう。
FAQ
wp_verify_nonce と check_admin_referer の違いは何ですか?
check_admin_referer() は、管理画面のフォームや URL に対して nonce とリファラーの両方を検証し、検証に失敗した場合は 403 でリクエストを停止します。wp_verify_nonce() は nonce のみをチェックし、結果を返すので自分で処理する必要があります。こちらは Ajax ハンドラーやその他のカスタムな文脈に適しています。どちらも権限チェックの代わりにはなりません。nonce はユーザーがその操作を意図したことを確認するものであり、current_user_can() はユーザーにその操作が許可されていることを確認するものです。
プラグインを無効化すると、保存されたオプションはどうなりますか?
何も起きません。無効化してもオプションはデータベースに残るため、ユーザーがプラグインを再度有効にしたときに設定はそのまま保たれます。無効化コールバックでは、transient やキャッシュファイルなど、短命なものだけを削除すべきです。恒久的なクリーンアップはアンインストールの役割で、register_uninstall_hook() を使うか、プラグインフォルダー内の uninstall.php ファイルで実装します。uninstall.php を使う場合は、何かを削除する前に WP_UNINSTALL_PLUGIN 定数が定義されていることを必ず確認しなければなりません。
WordPress プラグインは複数の PHP ファイルを持てますか?
はい、ただしプラグインヘッダーのコメントを含めてよいのはメインファイルだけです。WordPress はプラグインディレクトリ内の PHP ファイルからそのヘッダーを読み取ってプラグインを見つけるため、ヘッダーを持つファイルはそれぞれ独立したプラグインとして表示されます。追加のファイルはメインファイルから require_once で読み込み、register_activation_hook() のような呼び出しはメインのプラグインファイルを指すようにしてください。これらの第 1 引数はそのファイルを参照する必要があるためです。
デフォルトのオプション値は register_setting と有効化フックのどちらで設定すべきですか?
どちらか一方の仕組みを使い、両方は使わないでください。register_setting() は、データベースに値が存在しないときに返される default 引数を受け付けます。一方、有効化フック内の add_option() は、有効化時に一度だけ実際の行を書き込みます。有効化フックによる方法は、フロントエンドのリクエストも含め、すべてのリクエストで値が存在することを保証します。register_setting() の default は、その登録コードが実行される場所でのみ適用されます。両方を混在させると、同じオプションに対して真実の情報源が 2 つできてしまいます。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k