- 1. プラグイン開発の基本構造|フックとファイル配置
- 2. 有効化・無効化フック|activation hooks
- 3. 設定値の保存|Options API
- 4. 設定画面を正攻法で作る|Settings API
- 5. 管理メニュー|add_menu_page / add_submenu_page
- 6. 投稿編集画面の拡張|メタボックスとPost Meta
- 7. カスタム投稿タイプとタクソノミー
- 8. 管理画面のCSS/JS読み込み|admin_enqueue_scripts
- 9. 権限とロール|capability設計
- 10. admin_post_|フォーム送信の受け口
- 11. WP_List_Table|投稿一覧風のテーブルUIを作る
- 12. 独自アップデート機能|WordPress.org非公開プラグインの自動更新
- 13. $wpdb深掘り|安全なSQL・独自テーブル設計
- 14. プラグインファイルのパス・URL関数
- 15. nonceフォーム処理|wp_nonce_field / check_admin_referer
- 16. User Meta API|ユーザーごとのデータ保存
- 17. ユーザー取得|get_users / WP_User_Query
- 18. 管理画面通知|admin_notices / add_settings_error
- 19. ロール・ケイパビリティ管理|add_role / get_role
- 20. メール送信|wp_mail
- 21. Object Cache|wp_cache_get / wp_cache_set
- 22. URLリダイレクト・操作|wp_redirect / add_query_arg
- 23. メディアアップロード|wp_handle_upload / wp_insert_attachment
- 24. タクソノミーCRUD|wp_insert_term / get_terms / term_exists
- 25. ダッシュボードウィジェット・プラグインアクションリンク
- まとめ:プラグイン開発で守るべき5つの原則
この記事でわかること
- プラグイン開発で使う関数を25カテゴリ・フック設計からメディアアップロードまで体系的に解説
- 「動けばOK」から脱却:Settings APIによるnonce・サニタイズ・権限の自動処理
- メタボックスの保存処理「黄金パターン」(nonce検証→権限チェック→サニタイズ→保存)を完全習得
- User Meta・Object Cache・wp_mail・タクソノミーCRUDなど、解説書に載りにくい実装パターンも収録
- WordPress.org非公開プラグインへの自動更新機能の実装方法まで収録
プラグイン開発は、ひと言でいえば「WordPressのルールに沿って機能を拡張する」仕事です。しかし「動けばOK」で組んでしまうと、後から必ず問題が発生します。
プラグインには「どのテーマでも正しく動く」「管理画面から設定変更できる」「他プラグインとの競合に耐える」「セキュリティ対策が必須」という要件があります。つまりプラグインは長期運用を前提としたソフトウェアなのです。
この記事はWordPress関数解説シリーズのPart2(プラグイン開発編)です。テーマ制作関数はPart1、Ajax・REST API・セキュリティ設計はPart3で解説しています。
1. プラグイン開発の基本構造|フックとファイル配置
結論:WordPressプラグインの設計は「どのフックで何をするか」を最初に決めることから始まります。コードを書く前にフックマップを整理する習慣をつけるだけで、後から「どこに何を書けばいいかわからない」という迷子状態を防げます。
WordPressでは「何かしたい」=「どこかのフックで処理を登録する」という考え方が基本です。プラグインは必ずフックを通じてWordPressの動作に介入します。
プラグインファイルの冒頭には必ずプラグインヘッダーコメントを記述します。これがないとWordPressはプラグインとして認識しません。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php /** * Plugin Name: My Plugin * Plugin URI: https://example.com/my-plugin * Description: プラグインの説明文をここに記述します。 * Version: 1.0.0 * Author: Your Name * Author URI: https://example.com * License: GPL v2 or later * Text Domain: my-plugin */ // 直接アクセス禁止(必須) defined( 'ABSPATH' ) || exit; // 初期化処理 add_action( 'init', function() { // CPT登録、ショートコード登録などはここで }); |
Text Domainは翻訳ファイルの識別子です。WordPress.orgでプラグインを公開する場合、translate.wordpress.orgで翻訳を管理できます。詳しくは「自作WordPressプラグインでPTEを取るまでの実体験」をご覧ください。
add_action()|特定のタイミングで処理を実行する
WordPressは処理の各段階でアクションフックと呼ばれるタイミングポイントを提供しています。add_action()でそのタイミングに自分の処理を挿入します。
第3引数の$priorityは実行順序を制御します。数値が小さいほど早く実行されます(デフォルト:10)。
|
1 2 3 4 5 6 7 8 9 10 |
// 優先度デフォルト(10)で登録 add_action( 'init', 'myplugin_init' ); // 他のプラグインより先に実行したい場合 add_action( 'init', 'myplugin_early_init', 5 ); // 抜粋の長さをフィルター(値を返す必要がある) add_filter( 'excerpt_length', function( $length ) { return 40; }); |
主要フックポイント一覧
| フック名 | 発火タイミング | 主な用途 |
|---|---|---|
plugins_loaded |
全プラグイン読み込み後 | 他プラグイン依存の処理 |
init |
WordPress初期化完了後 | CPT・ショートコード登録 |
admin_init |
管理画面の初期化 | Settings API登録 |
admin_menu |
管理メニュー構築時 | メニュー項目の追加 |
wp_enqueue_scripts |
フロントCS/JS読み込み時 | スタイル・スクリプト登録 |
admin_enqueue_scripts |
管理画面CS/JS読み込み時 | 管理画面用アセット登録 |
save_post |
投稿保存時 | メタボックスデータの保存 |
remove_action() / remove_filter()|フックを解除する
他のプラグインやテーマが登録した処理を無効化したい場合に使います。解除には登録時と同じコールバック関数名・同じ優先度の指定が必要です。無名関数(クロージャ)で登録された処理は解除できません。
|
1 2 3 |
// 登録された処理を解除(優先度も一致させる必要がある) remove_action( 'wp_head', 'some_plugin_add_styles', 10 ); remove_filter( 'the_content', 'other_plugin_filter', 20 ); |
2. 有効化・無効化フック|activation hooks
結論:ユーザーデータの削除を無効化フックで行うのは絶対に避けてください。「プラグインを無効化したらデータが全部消えた」はユーザーの信頼を一瞬で失います。データ削除はアンインストール時のみ、無効化時はcronの解除と一時キャッシュのクリアだけに留めてください。
register_activation_hook()|有効化時に1回だけ実行
プラグインを有効化したとき(または更新後に再有効化したとき)に実行されます。初期オプション設定・独自テーブル作成・リライトルールのフラッシュなどに使います。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
register_activation_hook( __FILE__, function() { // 初期オプションを設定(既存値は上書きしない) add_option( 'myplugin_version', '1.0.0' ); add_option( 'myplugin_settings', array( 'enabled' => true, 'api_key' => '', ) ); // CPT登録後にリライトルールをフラッシュ myplugin_register_post_types(); flush_rewrite_rules(); // 有効化時の1回だけ実行すること }); |
register_deactivation_hook()|無効化時に1回だけ実行
| タイミング | ✅ やること | ❌ やってはいけないこと |
|---|---|---|
| 無効化時 | cronジョブの解除、一時キャッシュのクリア | ユーザーデータの削除 |
| アンインストール時 | オプション・テーブルなどのデータ削除 | – |
|
1 2 3 4 5 6 7 8 |
register_deactivation_hook( __FILE__, function() { // cronジョブを解除 wp_clear_scheduled_hook( 'myplugin_daily_cleanup' ); // 一時キャッシュをクリア delete_transient( 'myplugin_cache' ); // ← ユーザーデータは絶対に削除しない }); |
アンインストール時のデータ削除はuninstall.phpを用意するか、register_uninstall_hook()で登録します。
3. 設定値の保存|Options API
結論:Options APIはプラグインの設定保存の基本手段ですが、「なんでもoptionsに入れる」のは禁物です。投稿ごとのデータはPost Meta、頻繁に変わる一時データはTransients、大量データは独自テーブルと使い分けることで、データベース負荷を抑えられます。
Options APIはwp_optionsテーブルにキーと値のペアを保存します。サイト全体に関わる設定値の管理に使います。
Options API 関数一覧
| 関数 | 動作 | 使いどころ |
|---|---|---|
get_option( $key, $default ) |
値を取得(なければデフォルト) | 設定の読み込みに常用 |
add_option( $key, $value ) |
存在しない場合のみ作成 | 有効化時の初期値設定 |
update_option( $key, $value ) |
上書き(なければ作成) | 設定画面からの保存 |
delete_option( $key ) |
削除 | アンインストール時のクリーンアップ |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 設定の取得(デフォルト値付き) $settings = get_option( 'myplugin_settings', array( 'enabled' => false, 'api_key' => '', 'limit' => 10, ) ); $api_key = $settings['api_key']; // 設定の保存(上書き) update_option( 'myplugin_settings', array( 'enabled' => true, 'api_key' => 'new_key_here', 'limit' => 20, ) ); // アンインストール時に全削除 delete_option( 'myplugin_settings' ); delete_option( 'myplugin_version' ); |
データ保存先の使い分け
| データの種類 | 推奨保存先 |
|---|---|
| サイト全体の設定値 | Options API(wp_options) |
| 投稿ごとのデータ | Post Meta(postmeta) |
| ユーザーごとのデータ | User Meta(usermeta) |
| 有効期限付きキャッシュ | Transients API |
| 大量・複雑な構造データ | 独自テーブル($wpdb) |
4. 設定画面を正攻法で作る|Settings API
結論:$_POSTを直接受け取ってupdate_option()で保存するコードは書かないでください。Settings APIを使えば、nonce検証・権限チェック・サニタイズコールバックがすべて自動化され、セキュリティ対策の書き忘れがなくなります。
Settings APIを使うメリットを整理します。
| メリット | 詳細 |
|---|---|
| nonce検証が自動 | CSRF攻撃を自動防御 |
| サニタイズが確実 | コールバックに全入力値を通せる |
| 権限チェックが自動 | 不正アクセスを自動拒否 |
| UIが統一される | WordPress標準の設定画面デザイン |
register_setting()|設定項目を登録する
Settings APIの核です。sanitize_callbackを必ず設定してください。ユーザーの入力値はすべてこのコールバックを通過してからデータベースに保存されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
add_action( 'admin_init', function() { register_setting( 'myplugin_group', 'myplugin_settings', array( 'type' => 'array', 'sanitize_callback' => function( $input ) { return array( 'enabled' => isset( $input['enabled'] ) ? '1' : '0', 'api_key' => sanitize_text_field( $input['api_key'] ?? '' ), 'limit' => absint( $input['limit'] ?? 10 ), ); }, 'default' => array( 'enabled' => '0', 'api_key' => '', 'limit' => 10, ), ) ); }); |
add_settings_section()|設定をグループ分けする
設定項目が多い場合にセクションで区切ります。コールバックで説明文を出力できます。
|
1 2 3 4 5 6 7 8 9 10 |
add_action( 'admin_init', function() { add_settings_section( 'myplugin_main_section', // セクションID '基本設定', // セクションタイトル function() { // セクション説明文 echo '<p>プラグインの基本動作を設定します。</p>'; }, 'myplugin' // ページスラッグ ); }); |
add_settings_field()|入力フィールドを追加する
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
add_action( 'admin_init', function() { // チェックボックス add_settings_field( 'myplugin_enabled', 'プラグインを有効化', function() { $options = get_option( 'myplugin_settings', array() ); $checked = ! empty( $options['enabled'] ) ? 'checked' : ''; echo '<label><input type="checkbox" name="myplugin_settings[enabled]" ' . $checked . '> 有効にする</label>'; }, 'myplugin', 'myplugin_main_section' ); // テキスト入力 add_settings_field( 'myplugin_api_key', 'APIキー', function() { $options = get_option( 'myplugin_settings', array() ); $value = esc_attr( $options['api_key'] ?? '' ); echo '<input type="text" name="myplugin_settings[api_key]" value="' . $value . '" class="regular-text">'; echo '<p class="description">外部サービスのAPIキーを入力してください。</p>'; }, 'myplugin', 'myplugin_main_section' ); }); |
設定フォームの出力|settings_fields / do_settings_sections / submit_button
formのactionがoptions.phpであることに注目してください。WordPressのoptions.phpが保存処理を引き受け、nonce検証・権限チェック・サニタイズコールバックを自動実行してくれます。
|
1 2 3 4 5 6 7 8 9 10 11 |
<div class="wrap"> <h1><?php echo esc_html( get_admin_page_title() ); ?></h1> <form method="post" action="options.php"> <?php settings_fields( 'myplugin_group' ); // nonceとhiddenフィールドを出力 do_settings_sections( 'myplugin' ); // セクションとフィールドを出力 submit_button(); // 送信ボタンを出力 ?> </form> </div> |
5. 管理メニュー|add_menu_page / add_submenu_page
結論:独立したメニューを追加するかWordPress標準の「設定」に追加するかは、プラグインの規模で判断します。設定項目が1〜2個なら「設定」サブメニューへの追加で十分で、大きなメニューを増やしすぎると管理画面が使いにくくなります。
add_menu_page()|トップレベルメニューを追加
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
add_action( 'admin_menu', function() { add_menu_page( 'My Plugin Settings', // ページタイトル(<title>に表示) 'My Plugin', // メニュータイトル(サイドバー表示) 'manage_options', // 必要な権限 'myplugin', // メニュースラッグ 'myplugin_render_page', // コンテンツ出力関数 'dashicons-admin-generic', // アイコン(Dashicons名またはURL) 80 // 表示位置 ); }); function myplugin_render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; // 権限チェック(二重確認として推奨) } echo '<div class="wrap">'; echo '<h1>' . esc_html( get_admin_page_title() ) . '</h1>'; echo '</div>'; } |
add_submenu_page()|既存メニューの下に追加
|
1 2 3 4 5 6 7 8 9 10 11 |
add_action( 'admin_menu', function() { // 「設定」の下に追加(シンプルなプラグインに最適) add_submenu_page( 'options-general.php', // 親メニューのスラッグ 'My Plugin Settings', 'My Plugin', 'manage_options', 'myplugin-settings', 'myplugin_render_settings' ); }); |
メニュー位置の目安
| 位置(数値) | 表示位置 |
|---|---|
| 2 | ダッシュボードの下 |
| 20 | 固定ページの下 |
| 60 | 「外観」の下 |
| 65 | 「プラグイン」の下 |
| 80 | 「設定」の下(推奨) |
6. 投稿編集画面の拡張|メタボックスとPost Meta
結論:メタボックスの保存処理では「nonce検証→権限チェック→サニタイズ→保存」の4ステップを必ず守ってください。この黄金パターンのうち1つでも省略すると、セキュリティホールになります。特にnonce検証の省略はCSRF攻撃への無防備な入口になります。
投稿ごとに保存したいデータは、オプションではなくPost Meta(カスタムフィールド)で管理します。
add_meta_box()|入力エリアを追加する
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
add_action( 'add_meta_boxes', function() { add_meta_box( 'myplugin_meta_box', // メタボックスID '追加情報', // タイトル 'myplugin_meta_box_html', // 出力関数 'post', // 投稿タイプ(配列で複数指定可) 'side', // 位置:'normal', 'side', 'advanced' 'high' // 優先度:'high', 'low', 'default' ); }); function myplugin_meta_box_html( $post ) { $value = get_post_meta( $post->ID, '_myplugin_custom_field', true ); wp_nonce_field( 'myplugin_meta_box', 'myplugin_meta_box_nonce' ); // CSRF対策 echo '<label for="myplugin_custom_field">カスタム値:</label>'; echo '<input type="text" id="myplugin_custom_field" name="myplugin_custom_field"'; echo ' value="' . esc_attr( $value ) . '" style="width:100%;">'; } |
save_postフック|保存処理の黄金パターン
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
add_action( 'save_post', function( $post_id ) { // Step1: 自動保存時は処理しない if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } // Step2: nonce検証(CSRF対策) if ( ! isset( $_POST['myplugin_meta_box_nonce'] ) ) { return; } if ( ! wp_verify_nonce( $_POST['myplugin_meta_box_nonce'], 'myplugin_meta_box' ) ) { return; } // Step3: 権限チェック if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } // Step4: サニタイズして保存 if ( isset( $_POST['myplugin_custom_field'] ) ) { $value = sanitize_text_field( $_POST['myplugin_custom_field'] ); update_post_meta( $post_id, '_myplugin_custom_field', $value ); } }); |
Post Meta 関連関数
| 関数 | 動作 |
|---|---|
get_post_meta( $id, $key, $single ) |
メタ値を取得($single=trueで単一値) |
update_post_meta( $id, $key, $value ) |
更新(なければ作成) |
add_post_meta( $id, $key, $value, $unique ) |
追加($unique=trueで重複防止) |
delete_post_meta( $id, $key ) |
削除 |
キー名の先頭にアンダースコア(_)を付けると、標準のカスタムフィールドUIに表示されなくなります。プラグイン専用フィールドには_myplugin_のようなプレフィックスを付けることで、他プラグインとの衝突も防げます。
7. カスタム投稿タイプとタクソノミー
結論:カスタム投稿タイプはテーマではなくプラグインで登録してください。テーマで登録すると、テーマを変更したときにCPTが消えてコンテンツにアクセスできなくなります。データ構造はテーマに依存させないことが長期運用の原則です。
register_post_type()|カスタム投稿タイプを登録
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
add_action( 'init', function() { register_post_type( 'book', array( 'label' => '書籍', 'labels' => array( 'name' => '書籍', 'singular_name' => '書籍', 'add_new_item' => '新規書籍を追加', 'edit_item' => '書籍を編集', ), 'public' => true, 'has_archive' => true, 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), 'show_in_rest' => true, // ブロックエディタとREST API対応に必須 'menu_icon' => 'dashicons-book', ) ); }); |
register_taxonomy()|カスタムタクソノミーを登録
|
1 2 3 4 5 6 7 8 |
add_action( 'init', function() { register_taxonomy( 'genre', 'book', array( 'label' => 'ジャンル', 'hierarchical' => true, // true=カテゴリ形式 / false=タグ形式 'show_in_rest' => true, 'show_admin_column' => true, // 投稿一覧にカラム表示 ) ); }); |
flush_rewrite_rules()|404エラーを防ぐリライトルール更新
CPTを登録するとパーマリンク構造が変わりますが、リライトルールのキャッシュが残っているため、そのままでは404エラーになります。有効化時に1回だけflush_rewrite_rules()を実行してください。毎リクエストで実行するのは絶対に避けてください(データベースへの重い処理のため)。
|
1 2 3 4 |
register_activation_hook( __FILE__, function() { myplugin_register_post_types(); // まずCPTを登録 flush_rewrite_rules(); // その後にフラッシュ(順序が重要) }); |
8. 管理画面のCSS/JS読み込み|admin_enqueue_scripts
結論:管理画面用のCSS/JSは$hook_suffixで条件分岐し、自分のページでのみ読み込んでください。全管理画面でアセットを読み込むプラグインは「重いプラグイン」の烙印を押される最大の原因です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
add_action( 'admin_enqueue_scripts', function( $hook_suffix ) { // 自分のプラグイン設定ページでのみ読み込む if ( $hook_suffix !== 'toplevel_page_myplugin' ) { return; } wp_enqueue_style( 'myplugin-admin', plugin_dir_url( __FILE__ ) . 'assets/css/admin.css', array(), '1.0.0' ); wp_enqueue_script( 'myplugin-admin', plugin_dir_url( __FILE__ ) . 'assets/js/admin.js', array( 'jquery' ), '1.0.0', true ); }); |
$hook_suffixの形式はメニュー構造によって変わります。
| メニュー種別 | $hook_suffixの形式 |
|---|---|
| トップレベルメニュー | toplevel_page_{menu_slug} |
| サブメニュー | {parent_slug}_page_{menu_slug} |
不明な場合はvar_dump( $hook_suffix )で確認してください。
9. 権限とロール|capability設計
結論:「誰がこの機能を使えるべきか?」をコードを書く前に決めてください。権限の粒度が細かすぎると複雑になり、粗すぎると権限昇格のリスクが生まれます。Settings API経由の操作にはmanage_options、投稿の操作にはedit_postsが標準的な判断基準です。
current_user_can()|権限チェックの基本
|
1 2 3 4 5 6 |
function myplugin_render_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'この操作を行う権限がありません。' ); } // 設定画面を出力 } |
よく使うケイパビリティ一覧
| ケイパビリティ | 使用場面 | 最低ロール |
|---|---|---|
manage_options |
プラグイン設定画面 | 管理者 |
edit_posts |
投稿の編集 | 投稿者以上 |
edit_post |
特定の投稿の編集(IDを第2引数で指定) | 投稿者以上 |
upload_files |
ファイルのアップロード | 投稿者以上 |
edit_users |
ユーザーの編集 | 管理者 |
10. admin_post_|フォーム送信の受け口
結論:CSVエクスポートやファイルダウンロードなど、Settings APIでは対応できない処理はadmin_post_{action}フックを使います。処理完了後は必ずwp_safe_redirect()でリダイレクトしてください。リダイレクトしないと同じフォームの二重送信が発生します。
|
1 2 3 4 5 6 |
<!-- フォーム側 --> <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"> <?php wp_nonce_field( 'myplugin_export', 'myplugin_nonce' ); ?> <input type="hidden" name="action" value="myplugin_export"> <button type="submit" class="button button-primary">CSVエクスポート</button> </form> |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 受け口 add_action( 'admin_post_myplugin_export', function() { // 権限チェック if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Permission denied' ); } // nonce検証 check_admin_referer( 'myplugin_export', 'myplugin_nonce' ); // 処理を実行 // ... // 処理後はリダイレクト(二重送信防止) wp_safe_redirect( add_query_arg( array( 'page' => 'myplugin', 'message' => 'exported' ), admin_url( 'admin.php' ) ) ); exit; // ← exitは必須 }); |
11. WP_List_Table|投稿一覧風のテーブルUIを作る
結論:WP_List_Tableは公式の公開APIとして保証されていませんが、実務では広く使われています。WordPressの管理画面と統一されたUI(ソート・ページネーション・一括操作)を短期間で実装できるため、現実的な選択肢です。バージョンアップで挙動が変わる可能性があることだけ念頭に置いてください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } class MyPlugin_List_Table extends WP_List_Table { // カラム定義 public function get_columns() { return array( 'cb' => '<input type="checkbox" />', 'title' => 'タイトル', 'date' => '日付', ); } // チェックボックスカラム protected function column_cb( $item ) { return sprintf( '<input type="checkbox" name="ids[]" value="%d" />', absint( $item['id'] ) ); } // タイトルカラム(行アクション付き) protected function column_title( $item ) { $actions = array( 'edit' => sprintf( '<a href="%s">編集</a>', esc_url( $item['edit_url'] ) ), 'delete' => sprintf( '<a href="%s">削除</a>', esc_url( $item['delete_url'] ) ), ); return sprintf( '%s %s', esc_html( $item['title'] ), $this->row_actions( $actions ) ); } // その他カラムのデフォルト出力 protected function column_default( $item, $column_name ) { return esc_html( $item[ $column_name ] ?? '' ); } // データ取得とページネーション設定 public function prepare_items() { $per_page = 20; $page = max( 1, absint( $_GET['paged'] ?? 1 ) ); $data = myplugin_get_items( $page, $per_page ); $total = myplugin_count_items(); $this->items = $data; $this->set_pagination_args( array( 'total_items' => $total, 'per_page' => $per_page, 'total_pages' => ceil( $total / $per_page ), ) ); } } |
12. 独自アップデート機能|WordPress.org非公開プラグインの自動更新
結論:WordPress.org以外で配布する有料プラグインなどに自動更新を実装するには、pre_set_site_transient_update_pluginsフィルターを使います。自前サーバーから最新バージョン情報を取得し、update情報に追加するだけで、標準のWordPress更新UIから更新できるようになります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
add_filter( 'pre_set_site_transient_update_plugins', function( $transient ) { if ( empty( $transient->checked ) ) { return $transient; } $plugin_file = 'myplugin/myplugin.php'; $current = $transient->checked[ $plugin_file ] ?? null; if ( ! $current ) { return $transient; } // 自前サーバーから最新バージョン情報を取得 $response = wp_remote_get( 'https://example.com/api/update-check.json' ); if ( is_wp_error( $response ) ) { return $transient; } $info = json_decode( wp_remote_retrieve_body( $response ), true ); // 新しいバージョンがあれば更新情報を追加 if ( $info && version_compare( $info['version'], $current, '>' ) ) { $transient->response[ $plugin_file ] = (object) array( 'slug' => 'myplugin', 'plugin' => $plugin_file, 'new_version' => $info['version'], 'package' => $info['download_url'], 'tested' => $info['tested'] ?? '', ); } return $transient; }); |
13. $wpdb深掘り|安全なSQL・独自テーブル設計
結論:ユーザー入力を含むSQLは必ず$wpdb->prepare()を使ってください。プレースホルダを使わずに変数を直接文字列結合してSQLを組み立てるコードは、SQLインジェクション攻撃への完全な無防備状態です。Options APIやMeta APIで対応できない大量・複雑なデータを扱う場合にのみ$wpdbを使います。
$wpdb->prepare()|SQLインジェクション対策の基本
| プレースホルダ | 型 | 使用例 |
|---|---|---|
%d |
整数(decimal) | post_id, user_id など |
%s |
文字列(string) | meta_key, status など |
%f |
浮動小数点数(float) | 価格、評価スコアなど |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
global $wpdb; // 安全なSQL:prepare()でプレースホルダを使う $sql = $wpdb->prepare( "SELECT * FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s", $post_id, '_myplugin_field' ); $results = $wpdb->get_results( $sql, ARRAY_A ); // 1件だけ取得 $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", $post_id ) ); // 単一の値を取得 $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = %s", 'publish' ) ); |
dbDelta()|独自テーブルの作成・更新
プラグイン有効化時にテーブルを作成します。dbDelta()は既存テーブルとの差分を確認し、必要なカラム追加だけを行うため、アップデート時の実行も安全です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
register_activation_hook( __FILE__, function() { global $wpdb; $table = $wpdb->prefix . 'myplugin_items'; $charset_collate = $wpdb->get_charset_collate(); // 文字コードを既存テーブルと揃える $sql = "CREATE TABLE {$table} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT(20) UNSIGNED NOT NULL, title VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY user_id (user_id) ) {$charset_collate};"; // ↑ PRIMARY KEYの後の2スペースは dbDelta() の要件 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $sql ); }); |
14. プラグインファイルのパス・URL関数
結論:プラグインのファイルパスやURLを取得するには、plugin_dir_path()とplugin_dir_url()を必ず使ってください。絶対パスをハードコードすると、サーバー移転やサブディレクトリ構成への変更で一斉に壊れます。
プラグイン開発で最も頻繁に使うパス関数を整理します。
| 関数 | 返す値 | 末尾スラッシュ |
|---|---|---|
plugin_dir_path( __FILE__ ) |
プラグインのフルパス(サーバー絶対パス) | あり |
plugin_dir_url( __FILE__ ) |
プラグインのURL | あり |
plugins_url( $path, __FILE__ ) |
サブパスを含んだURL | なし |
plugin_basename( __FILE__ ) |
myplugin/myplugin.php 形式の相対パス |
なし |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?php // ファイルの読み込み(絶対パス) require_once plugin_dir_path( __FILE__ ) . 'includes/class-main.php'; // CSS/JSの読み込み(URL) wp_enqueue_style( 'myplugin-style', plugin_dir_url( __FILE__ ) . 'assets/css/style.css', array(), '1.0.0' ); // plugins_url:サブディレクトリから呼ぶ場合も安全 $icon_url = plugins_url( 'assets/images/icon.png', __FILE__ ); // plugin_basename:プラグインアクションリンクのフィルター登録などに使う $plugin_base = plugin_basename( __FILE__ ); // 結果例: 'myplugin/myplugin.php' // WP_PLUGIN_DIR / WP_PLUGIN_URL(定数) // プラグインディレクトリへの直接参照が必要な場合 $plugins_root = WP_PLUGIN_DIR; // 末尾スラッシュなし ?> |
15. nonceフォーム処理|wp_nonce_field / check_admin_referer
結論:Ajax以外の通常フォーム(管理画面のカスタムフォームなど)でも、nonceによるCSRF対策は必須です。wp_nonce_field()でフォームにhiddenフィールドを埋め込み、処理側でcheck_admin_referer()で検証するセットを必ず使ってください。
Part3ではAjax向けのcheck_ajax_referer()を解説しています。ここでは管理画面の通常フォーム向けのnonceパターンを解説します。
wp_nonce_field()|フォームにnonceを埋め込む
wp_nonce_field()は<input type="hidden">タグを2つ出力します。1つがnonceトークン、もう1つがリファラー用の_wp_http_refererです。
|
1 2 3 4 5 6 7 8 9 |
<form method="post"> <?php wp_nonce_field( 'myplugin_import_csv', 'myplugin_nonce' ); ?> <!-- 出力例: <input type="hidden" id="myplugin_nonce" name="myplugin_nonce" value="abc123xyz"> <input type="hidden" name="_wp_http_referer" value="/wp-admin/..."> --> <input type="file" name="csv_file"> <button type="submit" class="button button-primary">インポート</button> </form> |
check_admin_referer()|管理画面フォームの検証
検証失敗時は自動的にwp_die()を呼び出して処理を終了します。check_ajax_referer()の管理画面フォーム版です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php // フォーム処理側 if ( isset( $_POST['myplugin_nonce'] ) ) { // 検証失敗時は自動でwp_die() check_admin_referer( 'myplugin_import_csv', 'myplugin_nonce' ); // 権限チェック if ( ! current_user_can( 'manage_options' ) ) { wp_die( '権限がありません。' ); } // サニタイズして処理 // ... } ?> |
wp_verify_nonce()|戻り値で判定したい場合
wp_die()を使わずに自前でエラー処理をしたい場合はwp_verify_nonce()を直接使います。成功時は1(有効期限内の前半)または2(後半)を返し、失敗時はfalseを返します。
|
1 2 3 4 5 6 7 8 |
<?php $nonce = $_POST['myplugin_nonce'] ?? ''; if ( ! wp_verify_nonce( $nonce, 'myplugin_import_csv' ) ) { // 自前でエラーメッセージを表示 add_settings_error( 'myplugin', 'nonce_failed', 'セキュリティチェックに失敗しました。', 'error' ); return; } ?> |
16. User Meta API|ユーザーごとのデータ保存
結論:ユーザーごとに保存したいデータ(お気に入り、設定、プロファイル追加情報など)はUser Metaで管理します。Options APIに保存すると「Aさんの設定がBさんに影響する」という最悪のバグが起きます。ユーザーに紐づくデータは必ずUser Metaを使ってください。
User Meta 関数一覧
| 関数 | 動作 |
|---|---|
get_user_meta( $user_id, $key, $single ) |
メタ値を取得($single=trueで単一値) |
update_user_meta( $user_id, $key, $value ) |
更新(なければ作成) |
add_user_meta( $user_id, $key, $value, $unique ) |
追加($unique=trueで重複防止) |
delete_user_meta( $user_id, $key ) |
削除 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<?php $user_id = get_current_user_id(); // 値を取得(デフォルト値付き) $notifications = get_user_meta( $user_id, '_myplugin_notifications', true ); $notifications = $notifications ?: array(); // 未設定の場合は空配列 // 値を保存 update_user_meta( $user_id, '_myplugin_notifications', array( 'email' => true, 'digest' => 'weekly', ) ); // 複数値を持つメタ($unique=falseでaddを繰り返せる) add_user_meta( $user_id, '_myplugin_tag', 'premium', true ); // $unique=trueで重複防止 // 削除 delete_user_meta( $user_id, '_myplugin_notifications' ); // プロフィール画面に独自フィールドを追加する例 add_action( 'show_user_profile', 'myplugin_add_profile_fields' ); add_action( 'edit_user_profile', 'myplugin_add_profile_fields' ); function myplugin_add_profile_fields( $user ) { $bio_en = get_user_meta( $user->ID, '_myplugin_bio_en', true ); echo '<h3>英語プロフィール</h3>'; echo '<table class="form-table"><tr>'; echo '<th><label for="bio_en">English Bio</label></th>'; echo '<td><textarea name="myplugin_bio_en" id="bio_en">' . esc_textarea( $bio_en ) . '</textarea></td>'; echo '</tr></table>'; } // 保存処理 add_action( 'personal_options_update', 'myplugin_save_profile_fields' ); add_action( 'edit_user_profile_update', 'myplugin_save_profile_fields' ); function myplugin_save_profile_fields( $user_id ) { if ( ! current_user_can( 'edit_user', $user_id ) ) { return; } if ( isset( $_POST['myplugin_bio_en'] ) ) { update_user_meta( $user_id, '_myplugin_bio_en', sanitize_textarea_field( $_POST['myplugin_bio_en'] ) ); } } ?> |
17. ユーザー取得|get_users / WP_User_Query
結論:get_users()は引数にargsを渡すだけで柔軟なユーザー絞り込みができます。大量ユーザーを扱う場合やページネーションが必要な場合はWP_User_Queryを使いますが、単純な取得ならget_users()で十分です。
get_users()|ユーザー一覧を配列で取得
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php // 管理者一覧を取得 $admins = get_users( array( 'role' => 'administrator', 'orderby' => 'display_name', 'order' => 'ASC', 'fields' => array( 'ID', 'display_name', 'user_email' ), // 必要なフィールドのみ取得(パフォーマンス最適化) ) ); foreach ( $admins as $admin ) { echo esc_html( $admin->display_name ) . ' / ' . esc_html( $admin->user_email ); } // 特定のUser Metaで絞り込む $premium_users = get_users( array( 'meta_key' => '_myplugin_tag', 'meta_value' => 'premium', 'number' => 50, // 最大取得件数(必ず上限を設定する) ) ); ?> |
WP_User_Query|ページネーション対応の高度なクエリ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php // 総件数と現在ページのデータを同時に取得 $per_page = 20; $page = max( 1, absint( $_GET['paged'] ?? 1 ) ); $query = new WP_User_Query( array( 'role__in' => array( 'editor', 'author' ), 'number' => $per_page, 'offset' => ( $page - 1 ) * $per_page, 'orderby' => 'registered', 'order' => 'DESC', ) ); $users = $query->get_results(); $total_users = $query->get_total(); // 総件数(ページネーションに使用) ?> |
get_user_by()|IDやメールから特定ユーザーを取得
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php // IDで取得 $user = get_user_by( 'id', 42 ); // メールアドレスで取得 $user = get_user_by( 'email', 'user@example.com' ); // ログイン名で取得 $user = get_user_by( 'login', 'yoshiaki' ); if ( $user ) { echo esc_html( $user->display_name ); echo esc_html( $user->user_email ); } // 現在のログインユーザーを取得 $current_user = wp_get_current_user(); if ( $current_user->exists() ) { echo 'こんにちは、' . esc_html( $current_user->display_name ) . 'さん'; } ?> |
18. 管理画面通知|admin_notices / add_settings_error
結論:プラグインのユーザーへのフィードバックはadmin_noticesフックで出力します。Settings API経由の設定保存ならadd_settings_error()を使うと通知表示まで自動化できます。どちらの方法でも出力するHTMLはesc_html()でエスケープを忘れないでください。
admin_notices フック|管理画面に通知を表示する
WordPressの管理画面には4種類の通知スタイルがあります。
| CSSクラス | 用途 | 表示色 |
|---|---|---|
notice-success |
保存完了など成功時 | 緑 |
notice-error |
エラー発生時 | 赤 |
notice-warning |
注意・警告 | 黄 |
notice-info |
情報・案内 | 青 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?php // 管理画面全体に通知(is-dismissibleで✕ボタン付き) add_action( 'admin_notices', function() { // 自分のページのみ表示する場合はスクリーンIDで絞る $screen = get_current_screen(); if ( $screen->id !== 'toplevel_page_myplugin' ) { return; } // APIキー未設定の警告 $settings = get_option( 'myplugin_settings', array() ); if ( empty( $settings['api_key'] ) ) { echo '<div class="notice notice-warning is-dismissible">'; echo '<p><strong>My Plugin:</strong>'; printf( 'APIキーが設定されていません。<a href="%s">設定画面</a>から設定してください。', esc_url( admin_url( 'admin.php?page=myplugin' ) ) ); echo '</p></div>'; } }); // transientを使って「保存完了」通知を1回だけ表示するパターン // (wp_safe_redirect後のリダイレクト先でも表示できる) function myplugin_show_admin_notice() { $notice = get_transient( 'myplugin_admin_notice' ); if ( $notice ) { echo '<div class="notice notice-success is-dismissible">'; echo '<p>' . esc_html( $notice ) . '</p>'; echo '</div>'; delete_transient( 'myplugin_admin_notice' ); } } add_action( 'admin_notices', 'myplugin_show_admin_notice' ); // 保存処理完了後 set_transient( 'myplugin_admin_notice', '設定を保存しました。', 30 ); ?> |
add_settings_error() / get_settings_errors()|Settings API連携通知
Settings API経由の設定画面では、add_settings_error()を使うとsettings_errors()の出力位置で自動的に表示されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php // sanitize_callbackの中でエラーを追加 function myplugin_sanitize( $input ) { $clean = array(); $clean['api_key'] = sanitize_text_field( $input['api_key'] ?? '' ); // バリデーション:APIキーが短すぎる場合 if ( ! empty( $clean['api_key'] ) && strlen( $clean['api_key'] ) < 32 ) { add_settings_error( 'myplugin_settings', // スラッグ 'api_key_too_short', // エラーコード 'APIキーは32文字以上で入力してください。', // メッセージ 'error' // タイプ:'error' or 'success' or 'warning' or 'info' ); $clean['api_key'] = ''; // 不正な値はリセット } else { add_settings_error( 'myplugin_settings', 'settings_saved', '設定を保存しました。', 'success' ); } return $clean; } // 設定ページのHTMLでエラーを出力 echo '<div class="wrap">'; settings_errors( 'myplugin_settings' ); // ← ここに通知が出力される echo '<h1>' . esc_html( get_admin_page_title() ) . '</h1>'; ?> |
19. ロール・ケイパビリティ管理|add_role / get_role
結論:独自ロールの追加はadd_role()で行いますが、毎リクエストで実行するのは絶対に避けてください。データベースに書き込む重い処理のため、プラグインの有効化時に1回だけ実行し、無効化時にremove_role()で削除するのが正しいパターンです。
add_role()|独自ロールを追加する
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php // 有効化時に1回だけ実行 register_activation_hook( __FILE__, function() { // 「会員」ロールを追加(投稿の閲覧と自分の投稿の編集だけができる) add_role( 'member', // ロールスラッグ '会員', // 表示名 array( 'read' => true, // ダッシュボードへのアクセス 'edit_posts' => true, // 自分の投稿を編集 'delete_posts' => false, // 削除は不可 ) ); }); // 無効化時に削除 register_deactivation_hook( __FILE__, function() { remove_role( 'member' ); }); ?> |
get_role()|既存ロールにケイパビリティを追加/削除する
既存ロール(投稿者・編集者など)に独自機能のケイパビリティを追加したい場合に使います。こちらも有効化時に1回だけ実行してください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?php register_activation_hook( __FILE__, function() { // 編集者(editor)に独自のケイパビリティを追加 $editor_role = get_role( 'editor' ); if ( $editor_role ) { $editor_role->add_cap( 'myplugin_manage_items' ); $editor_role->add_cap( 'myplugin_export_data' ); } // 管理者にも追加(管理者はすべての権限を持つが、明示的に追加することで管理しやすくなる) $admin_role = get_role( 'administrator' ); if ( $admin_role ) { $admin_role->add_cap( 'myplugin_manage_items' ); $admin_role->add_cap( 'myplugin_export_data' ); $admin_role->add_cap( 'myplugin_manage_settings' ); } }); // 無効化時にケイパビリティを削除 register_deactivation_hook( __FILE__, function() { foreach ( array( 'editor', 'administrator' ) as $role_name ) { $role = get_role( $role_name ); if ( $role ) { $role->remove_cap( 'myplugin_manage_items' ); $role->remove_cap( 'myplugin_export_data' ); $role->remove_cap( 'myplugin_manage_settings' ); } } }); // 使用例:独自ケイパビリティで権限チェック if ( current_user_can( 'myplugin_manage_items' ) ) { // アイテム管理機能を表示 } ?> |
20. メール送信|wp_mail
結論:WordPressからメールを送る場合は必ずwp_mail()を使ってください。PHPのmail()を直接呼ぶと、WordPress側のメール設定(SMTPプラグインなど)が無視され、迷惑メールフォルダに入りやすくなります。wp_mail()はフィルターフックで差し込まれるSMTPプラグインとも自動的に連携します。
wp_mail()|基本的な送信
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php // 基本的な送信 $to = 'user@example.com'; $subject = '【My Plugin】処理が完了しました'; $message = "処理が正常に完了しました。\n\n詳細はダッシュボードをご確認ください。"; $headers = array( 'Content-Type: text/plain; charset=UTF-8', 'From: My Plugin <noreply@example.com>', ); $result = wp_mail( $to, $subject, $message, $headers ); if ( $result ) { // 送信成功 } else { // 送信失敗(wp_mail_failed フィルターでエラー詳細を取得できる) error_log( 'myplugin: wp_mail failed to ' . $to ); } ?> |
HTMLメールの送信
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?php // HTMLメールを送る add_filter( 'wp_mail_content_type', function() { return 'text/html'; }); $html_message = ' <html><body> <h2>処理完了のお知らせ</h2> <p>' . esc_html( $user_name ) . 'さんのデータ処理が完了しました。</p> <a href="' . esc_url( admin_url() ) . '">管理画面を確認する</a> </body></html> '; wp_mail( $to, $subject, $html_message, array( 'Content-Type: text/html; charset=UTF-8', 'From: My Plugin <noreply@example.com>', 'Reply-To: support@example.com', ) ); // Content-Typeを元に戻す(他のメール送信に影響しないよう必ず戻す) remove_filter( 'wp_mail_content_type', function() { return 'text/html'; }); ?> |
wp_mail_failed フィルター|送信失敗の詳細を取得
|
1 2 3 4 5 6 |
<?php add_action( 'wp_mail_failed', function( WP_Error $error ) { error_log( 'myplugin: Mail failed - ' . $error->get_error_message() ); // SMTP設定のデバッグ、ログ送信、管理者への警告メールなど }); ?> |
21. Object Cache|wp_cache_get / wp_cache_set
結論:TransientsはDBに保存する「永続キャッシュ」ですが、Object Cacheはリクエスト中のみ有効なメモリキャッシュです。Redis・Memcachedが導入されている環境ではTransientsの読み書きもObject Cacheが引き受けるため自動で高速化されます。ループ内で同じDBクエリを繰り返している場合はwp_cache_get/setでリクエスト単位のキャッシュを検討してください。
| 関数 | 動作 | 有効期間 |
|---|---|---|
wp_cache_get( $key, $group ) |
キャッシュを取得(なければfalse) | リクエスト中(外部キャッシュ有効時はそれに依存) |
wp_cache_set( $key, $data, $group, $expire ) |
キャッシュを保存 | $expire秒(0=リクエスト終了まで) |
wp_cache_delete( $key, $group ) |
キャッシュを削除 | 即時 |
wp_cache_flush() |
全キャッシュをクリア | 即時 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?php // グループ名を必ず指定して名前衝突を防ぐ $cache_group = 'myplugin_items'; $cache_key = 'user_' . get_current_user_id() . '_favorites'; $cached = wp_cache_get( $cache_key, $cache_group ); if ( false === $cached ) { // キャッシュなし:DBから取得 global $wpdb; $table = $wpdb->prefix . 'myplugin_favorites'; $result = $wpdb->get_results( $wpdb->prepare( "SELECT item_id FROM {$table} WHERE user_id = %d", get_current_user_id() ), ARRAY_A ); // キャッシュに保存(このリクエスト中はDB再読み込みしない) wp_cache_set( $cache_key, $result, $cache_group ); $cached = $result; } // お気に入り更新時はキャッシュを削除 function myplugin_clear_user_cache( $user_id ) { $key = 'user_' . $user_id . '_favorites'; wp_cache_delete( $key, 'myplugin_items' ); } ?> |
22. URLリダイレクト・操作|wp_redirect / add_query_arg
結論:管理画面内のリダイレクトにはwp_safe_redirect()を使ってください。wp_redirect()はURLを検証しないため、オープンリダイレクト攻撃の踏み台になり得ます。wp_safe_redirect()は同じサイトか許可リストのURLのみに制限するため、管理画面フォームの送信後リダイレクトには必ずこちらを使います。
wp_redirect() / wp_safe_redirect()|ページ遷移
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php // ✅ 管理画面内のリダイレクトはwp_safe_redirect() wp_safe_redirect( admin_url( 'admin.php?page=myplugin&status=saved' ) ); exit; // ← exitは必須(これがないとリダイレクト後もPHPが実行し続ける) // ✅ 外部URLへのリダイレクト(セキュリティが問題ない場合のみ) wp_redirect( 'https://external.example.com/' ); exit; // ❌ NG:$_GET['redirect_to'] をそのままwp_redirect()に渡す // → オープンリダイレクト脆弱性になる // wp_redirect( $_GET['redirect_to'] ); // 絶対にやってはいけない ?> |
add_query_arg()|URLにパラメータを付加する
URLにGETパラメータを追加/変更します。URLを文字列で直接組み立てるより安全で可読性が高くなります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php // 現在のURLにパラメータを追加 $url = add_query_arg( 'status', 'imported', admin_url( 'admin.php?page=myplugin' ) ); // 結果: /wp-admin/admin.php?page=myplugin&status=imported // 複数パラメータを一度に追加 $redirect_url = add_query_arg( array( 'page' => 'myplugin', 'status' => 'saved', 'updated' => '1', ), admin_url( 'admin.php' ) ); // 現在のURLにパラメータを追加(第3引数省略で現在のURLを使う) $current_with_sort = add_query_arg( 'orderby', 'date' ); // リダイレクト後の完結した例 wp_safe_redirect( add_query_arg( 'message', 'exported', wp_get_referer() ) ); exit; ?> |
remove_query_arg()|URLからパラメータを削除する
|
1 2 3 4 5 6 7 8 |
<?php // 特定パラメータを除去したURLを取得 $clean_url = remove_query_arg( 'status', admin_url( 'admin.php?page=myplugin&status=saved' ) ); // 結果: /wp-admin/admin.php?page=myplugin // 複数パラメータを一度に削除 $clean_url = remove_query_arg( array( 'status', 'updated', 'message' ) ); ?> |
23. メディアアップロード|wp_handle_upload / wp_insert_attachment
結論:プラグインでファイルアップロードを実装する場合、move_uploaded_file()を直接使ってはいけません。wp_handle_upload()を使うことで、ファイルタイプの検証・サイズチェック・保存先の決定・セキュリティチェックが一括して行われます。
wp_handle_upload()|ファイルを安全に受け取る
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?php // フォームのname属性が'myplugin_file'の場合 if ( ! empty( $_FILES['myplugin_file']['name'] ) ) { // wp_handle_upload用のオーバーライド設定 $overrides = array( 'test_form' => false, // フォームnonce検証を自前で行う場合はfalse 'test_type' => true, // MIMEタイプチェックを有効化(推奨) 'mimes' => array( // 許可するMIMEタイプ 'csv' => 'text/csv', 'txt' => 'text/plain', ), ); $uploaded = wp_handle_upload( $_FILES['myplugin_file'], $overrides ); if ( isset( $uploaded['error'] ) ) { // アップロード失敗 error_log( 'myplugin upload error: ' . $uploaded['error'] ); } else { // 成功:$uploaded['file'] = サーバー絶対パス // $uploaded['url'] = アクセスURL // $uploaded['type'] = MIMEタイプ $file_path = $uploaded['file']; $file_url = $uploaded['url']; } } ?> |
wp_insert_attachment()|メディアライブラリに登録する
アップロードしたファイルをWordPressのメディアライブラリに登録します。登録することでサムネイル自動生成などの恩恵を受けられます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?php // ファイルをアップロード後、メディアライブラリに登録する完全な手順 require_once ABSPATH . 'wp-admin/includes/image.php'; // wp_generate_attachment_metadataに必要 $file_path = '/path/to/uploaded/image.jpg'; $post_id = 0; // 親投稿ID(紐づける投稿がなければ0) // メタデータを準備 $attachment = array( 'guid' => wp_upload_dir()['url'] . '/' . basename( $file_path ), 'post_mime_type' => 'image/jpeg', 'post_title' => sanitize_file_name( basename( $file_path ) ), 'post_status' => 'inherit', ); // メディアライブラリに登録 $attachment_id = wp_insert_attachment( $attachment, $file_path, $post_id ); if ( ! is_wp_error( $attachment_id ) ) { // 各サイズのサムネイルを生成 $meta = wp_generate_attachment_metadata( $attachment_id, $file_path ); wp_update_attachment_metadata( $attachment_id, $meta ); // 投稿のアイキャッチとして設定する場合 // set_post_thumbnail( $post_id, $attachment_id ); } ?> |
media_handle_upload()|アップロードとライブラリ登録を一発で行う
wp_handle_upload()とwp_insert_attachment()を組み合わせた処理をまとめて行います。画像を投稿に添付してメディアライブラリに登録するケースに最適です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; // 第1引数:$_FILESのキー名、第2引数:紐づける投稿ID $attachment_id = media_handle_upload( 'myplugin_file', $post_id ); if ( is_wp_error( $attachment_id ) ) { echo esc_html( $attachment_id->get_error_message() ); } else { // 成功:$attachment_idがメディアのID $url = wp_get_attachment_url( $attachment_id ); } ?> |
24. タクソノミーCRUD|wp_insert_term / get_terms / term_exists
結論:タームを追加する前にterm_exists()で重複確認をしてください。確認なしにwp_insert_term()を呼ぶと、同名タームが複数生成されてアーカイブページが重複表示される原因になります。
wp_insert_term()|タームを追加する
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?php // タームを追加する前に存在確認(重複防止) $term_name = 'フィクション'; $taxonomy = 'genre'; $existing = term_exists( $term_name, $taxonomy ); if ( ! $existing ) { $result = wp_insert_term( $term_name, // ターム名 $taxonomy, // タクソノミー array( 'slug' => sanitize_title( $term_name ), // スラッグ 'description' => '創作物・小説・フィクション全般', 'parent' => 0, // 親タームID(0=トップレベル) ) ); if ( is_wp_error( $result ) ) { error_log( 'wp_insert_term error: ' . $result->get_error_message() ); } else { $new_term_id = $result['term_id']; } } ?> |
get_terms()|タームの一覧を取得する
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php // タームの一覧取得 $terms = get_terms( array( 'taxonomy' => 'genre', 'hide_empty' => false, // 投稿が0件のタームも含める 'orderby' => 'name', 'order' => 'ASC', ) ); if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) { foreach ( $terms as $term ) { echo esc_html( $term->name ) . ' (' . absint( $term->count ) . '件)<br>'; } } // 特定の親ターム配下のみ取得 $children = get_terms( array( 'taxonomy' => 'genre', 'parent' => 5, // 親タームID ) ); ?> |
wp_update_term() / wp_delete_term()|更新・削除
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php // タームを更新 $result = wp_update_term( $term_id, // 更新対象のターム ID $taxonomy, array( 'name' => '新しい名前', 'slug' => 'new-slug', 'description' => '更新後の説明', ) ); // タームを削除(紐づいている投稿からも除去される) $result = wp_delete_term( $term_id, $taxonomy ); if ( is_wp_error( $result ) ) { error_log( 'term delete error: ' . $result->get_error_message() ); } // 投稿にタームをセット wp_set_object_terms( $post_id, array( 'fiction', 'fantasy' ), 'genre' ); // 現在のタームに追加(既存タームを残す) wp_set_object_terms( $post_id, array( 'new-tag' ), 'post_tag', true ); ?> |
25. ダッシュボードウィジェット・プラグインアクションリンク
結論:プラグインの設定ページへの導線は、プラグイン一覧の「設定」リンクとして追加するのが最もユーザーに親切な方法です。plugin_action_links_{plugin_file}フィルターを使えば、WordPress標準のUIに自然に溶け込んだリンクを追加できます。
wp_add_dashboard_widget()|ダッシュボードにウィジェットを追加
WordPressのダッシュボード(管理画面トップ)に独自のウィジェットを追加します。統計情報やクイックステータスの表示に使います。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?php add_action( 'wp_dashboard_setup', function() { wp_add_dashboard_widget( 'myplugin_dashboard_widget', // ウィジェットID 'My Plugin|ステータス', // タイトル 'myplugin_render_dashboard_widget', // コンテンツ出力関数 null, // コントロール用コールバック(不要な場合null) null, // コールバック引数 'normal', // 位置:'normal', 'side', 'column3', 'column4' 'high' // 優先度:'high', 'core', 'default', 'low' ); }); function myplugin_render_dashboard_widget() { $count = myplugin_get_total_count(); $last_run = get_option( 'myplugin_last_run' ); echo '<ul>'; echo '<li>処理件数:<strong>' . number_format_i18n( $count ) . ' 件</strong></li>'; if ( $last_run ) { echo '<li>最終実行:' . esc_html( date_i18n( 'Y年n月j日 H:i', strtotime( $last_run ) ) ) . '</li>'; } echo '</ul>'; printf( '<p><a href="%s" class="button">設定画面を開く</a></p>', esc_url( admin_url( 'admin.php?page=myplugin' ) ) ); } ?> |
plugin_action_links_{plugin_file}|プラグイン一覧に「設定」リンクを追加
プラグイン一覧ページの「有効化 | 削除」の横に独自リンクを追加します。ユーザーが設定ページを見つけやすくなるため、設定画面があるプラグインには必ず実装してください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), function( $links ) { // 設定リンクをリストの先頭に追加 $settings_link = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'admin.php?page=myplugin' ) ), esc_html__( '設定', 'myplugin' ) ); array_unshift( $links, $settings_link ); // ドキュメントリンクも追加 $links[] = sprintf( '<a href="%s" target="_blank">%s</a>', esc_url( 'https://raplsworks.com/myplugin-docs/' ), esc_html__( 'ドキュメント', 'myplugin' ) ); return $links; }); ?> |
plugin_row_meta|プラグイン一覧の説明文下にリンクを追加
プラグイン一覧の説明文の下に表示されるメタリンク(作者・バージョン等の横)に独自リンクを追加します。サポートフォーラムやレビューページへの誘導に使います。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php add_filter( 'plugin_row_meta', function( $links, $file ) { // 自分のプラグインのみ対象 if ( plugin_basename( __FILE__ ) !== $file ) { return $links; } $links[] = sprintf( '<a href="%s" target="_blank">%s</a>', esc_url( 'https://wordpress.org/support/plugin/myplugin/' ), esc_html__( 'サポートフォーラム', 'myplugin' ) ); $links[] = sprintf( '<a href="%s" target="_blank">%s</a>', esc_url( 'https://wordpress.org/plugins/myplugin/#reviews' ), esc_html__( '★レビューを書く', 'myplugin' ) ); return $links; }, 10, 2 ); ?> |
まとめ:プラグイン開発で守るべき5つの原則
この記事では、プラグイン開発者が使うWordPress関数を25カテゴリにわたって解説しました。実務で役立てられるよう、最重要ポイントを整理します。
| 原則 | 要点 |
|---|---|
| ✅ フック設計を先に決める | 「どのフックで何をするか」をコードを書く前にマップする |
| ✅ Settings APIを使う | nonce・権限・サニタイズを自動化し、$_POST直受けを避ける |
| ✅ 保存処理の黄金パターン | nonce検証→権限チェック→サニタイズ→保存の4ステップを必ず守る |
| ✅ データ保存先の使い分け | 設定=Options、投稿ごと=PostMeta、一時=Transients、大量=独自テーブル |
| ✅ CPTはプラグインで登録 | テーマ変更でデータが消えない構造にする |
次回のPart3では、Ajax・REST API・外部API連携・Transientsキャッシュ戦略・WP-Cronについて詳しく解説します。













コメント