wordpress-plugin-core について
WordPress プラグイン開発者向けの開発スキルで、実装方法とセキュリティ上の注意点を扱います。プラグイン構成、WordPress の規約、入力処理、権限、データベースクエリ、出力の保護を案内します。
主な機能
- 01プラグイン構成とアーキテクチャパターン
- 02一意なプレフィックスと ABSPATH チェック
- 03権限チェックと nonce 検証
- 04入力のサニタイズと出力のエスケープ
- 05プリペアドクエリとライフサイクルの指針
活用シーン
- 01WordPress プラグインのひな形作成
- 02プラグインコードの一般的なセキュリティリスク確認
- 03フォーム、AJAX、REST エンドポイントの実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill wordpress-plugin-coreインストール後、Claude Code で/wordpress-plugin-coreを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonassets/example-template.txtreferences/common-hooks.mdexample-reference.mdgithub-auto-updates.mdsecurity-checklist.mdscripts/example-script.shscaffold-plugin.shtemplates/plugin-oop/README.mdtemplates/plugin-psr4/README.mdcomposer.jsontemplates/plugin-simple/README.md詳細
WordPress プラグイン開発(コア)
最終更新日:2026-01-21 最新バージョン:WordPress 6.9+(2025年12月2日)、PHP 8.0+ を推奨、PHP 8.5 に対応 依存関係:なし(WordPress 5.9+、PHP 7.4+ が最小要件)
クイックスタート
アーキテクチャパターン:シンプル(関数のみ、5 個未満) | OOP(中規模プラグイン) | PSR-4(モダン/大規模、2025 年以降推奨)
プラグインヘッダー(必須なのは Plugin Name のみ):
<?php
/**
* Plugin Name: My Plugin
* Version: 1.0.0
* Requires at least: 5.9
* Requires PHP: 7.4
* Text Domain: my-plugin
*/
if ( ! defined( 'ABSPATH' ) ) exit;
セキュリティの基本(機能を実装する前に必須の 5 項目):
// 1. Unique Prefix
define( 'MYPL_VERSION', '1.0.0' );
function mypl_init() { /* code */ }
add_action( 'init', 'mypl_init' );
// 2. ABSPATH Check (every PHP file)
if ( ! defined( 'ABSPATH' ) ) exit;
// 3. Nonces
wp_nonce_field( 'mypl_action', 'mypl_nonce' );
wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' );
// 4. Sanitize Input, Escape Output
$clean = sanitize_text_field( $_POST['input'] );
echo esc_html( $output );
// 5. Prepared Statements
global $wpdb;
$wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}table WHERE id = %d", $id ) );
セキュリティの基本(詳細)
一意のプレフィックス(最低 4~5 文字)
対象:関数、クラス、定数、オプション、トランジェント、メタキー。使用しないもの:wp_、__、_。
function mypl_function() {} // ✅
class MyPL_Class {} // ✅
function init() {} // ❌ Will conflict
ケイパビリティの確認(is_admin() ではなく)
// ❌ WRONG - Security hole
if ( is_admin() ) { /* delete data */ }
// ✅ CORRECT
if ( current_user_can( 'manage_options' ) ) { /* delete data */ }
一般的なもの:manage_options(管理者)、edit_posts(編集者/投稿者)、read(購読者)
セキュリティの三原則(入力 → 処理 → 出力)
// Sanitize INPUT
$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
$html = wp_kses_post( $_POST['content'] ); // Allow safe HTML
$ids = array_map( 'absint', $_POST['ids'] );
// Validate LOGIC
if ( ! is_email( $email ) ) wp_die( 'Invalid' );
// Escape OUTPUT
echo esc_html( $name );
echo '<a href="' . esc_url( $url ) . '">';
echo '<div class="' . esc_attr( $class ) . '">';
ノンス(CSRF 対策)
// Form
<?php wp_nonce_field( 'mypl_action', 'mypl_nonce' ); ?>
if ( ! wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' ) ) wp_die( 'Failed' );
// AJAX
check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );
wp_localize_script( 'mypl-script', 'mypl_ajax_object', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mypl-ajax-nonce' ),
) );
プリペアドステートメント
// ❌ SQL Injection
$wpdb->get_results( "SELECT * FROM table WHERE id = {$_GET['id']}" );
// ✅ Prepared (%s=String, %d=Integer, %f=Float)
$wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}table WHERE id = %d", $_GET['id'] ) );
// LIKE Queries
$search = '%' . $wpdb->esc_like( $term ) . '%';
$wpdb->get_results( $wpdb->prepare( "... WHERE title LIKE %s", $search ) );
重要なルール
必ず行うこと
✅ 一意のプレフィックスを使用する(4~5 文字)。すべてのグローバルコード(関数、クラス、オプション、トランジェント)に適用します
✅ すべての PHP ファイルに ABSPATH チェックを追加する:if ( ! defined( 'ABSPATH' ) ) exit;
✅ is_admin() だけでなく、ケイパビリティ(current_user_can())を確認する
✅ すべてのフォームと AJAX リクエストでノンスを検証する
✅ ユーザー入力を含むすべてのデータベースクエリで $wpdb->prepare() を使用する
✅ 保存前に sanitize_*() 関数で入力をサニタイズする
✅ 表示前に esc_*() 関数で出力をエスケープする
✅ カスタム投稿タイプを登録する場合は、有効化時にリライトルールをフラッシュする
✅ 恒久的なクリーンアップには uninstall.php を使用する(無効化フックは使用しない)
✅ WordPress コーディング標準に従う(インデントにはタブを使用し、ヨーダ条件を適用する)
絶対に行わないこと
❌ extract() は使用しない - セキュリティ上の脆弱性を引き起こします
❌ サニタイズせずに $_POST/$_GET を信頼しない
❌ ユーザー入力を SQL に連結しない - 必ず prepare() を使用します
❌ 権限チェックに is_admin() だけを使用しない
❌ サニタイズされていないデータを出力しない - 必ずエスケープします
❌ 汎用的な関数名やクラス名を使用しない - 必ずプレフィックスを付けます
❌ 短縮 PHP タグ <? や <?= を使用しない - <?php のみを使用します
❌ 無効化時にユーザーデータを削除しない - アンインストール時にのみ削除します
❌ アンインストールフックを繰り返し登録しない - 有効化時に一度だけ登録します
❌ メイン処理で register_uninstall_hook() を使用しない - 代わりに uninstall.php を使用します
既知の問題の予防
このスキルでは、記録されている 29 件の問題を予防します:
問題 #1:SQL インジェクション
エラー:エスケープされていないユーザー入力によりデータベースが侵害される
出典:https://patchstack.com/articles/sql-injection/ (全脆弱性の 15%)
発生する理由:ユーザー入力を SQL クエリに直接連結している
予防策:必ずプレースホルダーを使って $wpdb->prepare() を使用する
// VULNERABLE
$wpdb->query( "DELETE FROM {$wpdb->prefix}table WHERE id = {$_GET['id']}" );
// SECURE
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}table WHERE id = %d", $_GET['id'] ) );
問題 #2:XSS(クロスサイトスクリプティング)
エラー:悪意のある JavaScript がユーザーのブラウザーで実行される 出典:https://patchstack.com (全脆弱性の 35%) 発生する理由:サニタイズされていないユーザーデータを HTML に出力している 予防策:状況に適した関数を使って、必ず出力をエスケープする
// VULNERABLE
echo $_POST['name'];
echo '<div class="' . $_POST['class'] . '">';
// SECURE
echo esc_html( $_POST['name'] );
echo '<div class="' . esc_attr( $_POST['class'] ) . '">';
問題 #3:CSRF(クロスサイトリクエストフォージェリ)
エラー:ユーザーに代わって不正な操作が実行される
出典:https://blog.nintechnet.com/25-wordpress-plugins-vulnerable-to-csrf-attacks/
発生する理由:リクエストが自サイトから発生したことを検証していない
予防策:wp_nonce_field() と wp_verify_nonce() を使ってノンスを使用する
// VULNERABLE
if ( $_POST['action'] == 'delete' ) {
delete_user( $_POST['user_id'] );
}
// SECURE
if ( ! wp_verify_nonce( $_POST['nonce'], 'mypl_delete_user' ) ) {
wp_die( 'Security check failed' );
}
delete_user( absint( $_POST['user_id'] ) );
問題 #4:ケイパビリティの確認漏れ
エラー:一般ユーザーが管理者向け機能にアクセスできる
出典:WordPress セキュリティレビュ―ガイドライン
発生する理由:current_user_can() ではなく is_admin() を使用している
予防策:管理者コンテキストかどうかだけでなく、必ずケイパビリティを確認する
// VULNERABLE
if ( is_admin() ) {
// Any logged-in user can trigger this
}
// SECURE
if ( current_user_can( 'manage_options' ) ) {
// Only administrators can trigger this
}
問題 #5:ファイルへの直接アクセス
エラー:WordPress のコンテキスト外で PHP ファイルが実行される 出典:WordPress プラグインハンドブック 発生する理由:ファイルの先頭に ABSPATH チェックがない 予防策:すべての PHP ファイルに ABSPATH チェックを追加する
// Add to top of EVERY PHP file
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
Issue #6: プレフィックスの衝突
エラー: 関数やクラスが他のプラグインと競合します 出典: WordPress Coding Standards 発生する理由: 一意なプレフィックスのない汎用的な名前を使用しているためです 防止策: すべてのグローバルコードに4~5文字のプレフィックスを使用します
// CAUSES CONFLICTS
function init() {}
class Settings {}
add_option( 'api_key', $value );
// SAFE
function mypl_init() {}
class MyPL_Settings {}
add_option( 'mypl_api_key', $value );
Issue #7: リライトルールがフラッシュされない(およびパフォーマンス)
エラー: カスタム投稿タイプで404エラーが発生する、またはフラッシュを繰り返すことでデータベースに過剰な負荷がかかります 出典: WordPress Plugin Handbook, Permalink Manager Pro 発生する理由: CPT の登録後にリライトルールをフラッシュし忘れている、またはページの読み込みごとにフラッシュを呼び出しているためです 防止策: フラッシュするのは有効化時/無効化時のみとし、ページの読み込みごとには決して実行しません
// ✅ CORRECT - Only flush on activation
function mypl_activate() {
mypl_register_cpt();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_deactivate() {
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
// ❌ WRONG - Causes database overload on EVERY page load
add_action( 'init', 'mypl_register_cpt' );
add_action( 'init', 'flush_rewrite_rules' ); // BAD! Performance killer!
// ❌ WRONG - In functions.php
function mypl_register_cpt() {
register_post_type( 'book', ... );
flush_rewrite_rules(); // BAD! Runs every time
}
ユーザー向けの解決方法: CPT で404エラーが表示される場合は、Settings → Permalinks → Save Changes に移動して手動でフラッシュします。
Issue #8: Transient がクリーンアップされない
エラー: 期限切れの transient がデータベースに蓄積します 出典: WordPress Transients API Documentation 発生する理由: アンインストール時にクリーンアップしていないためです 防止策: uninstall.php で transient を削除します
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
global $wpdb;
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mypl_%'" );
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mypl_%'" );
Issue #9: スクリプトがあらゆる場所で読み込まれる
エラー: 不要なアセットの読み込みによりパフォーマンスが低下します 出典: WordPress Performance Best Practices 発生する理由: 条件を確認せずにスクリプト/スタイルをエンキューしているためです 防止策: 必要な場所でのみアセットを読み込みます
// BAD - Loads on every page
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_script( 'mypl-script', $url );
} );
// GOOD - Only loads on specific page
add_action( 'wp_enqueue_scripts', function() {
if ( is_page( 'my-page' ) ) {
wp_enqueue_script( 'mypl-script', $url, array( 'jquery' ), '1.0', true );
}
} );
Issue #10: 保存時のサニタイズ漏れ
エラー: 悪意のあるデータがデータベースに保存されます 出典: WordPress Data Validation 発生する理由: $_POST のデータをサニタイズせずに保存しているためです 防止策: 保存前に必ずサニタイズします
// VULNERABLE
update_option( 'mypl_setting', $_POST['value'] );
// SECURE
update_option( 'mypl_setting', sanitize_text_field( $_POST['value'] ) );
Issue #11: LIKE クエリの誤り
エラー: SQL 構文エラーまたはインジェクションの脆弱性が発生します
出典: WordPress $wpdb Documentation
発生する理由: LIKE のワイルドカードが適切にエスケープされていないためです
防止策: $wpdb->esc_like() を使用します
// WRONG
$search = '%' . $term . '%';
// CORRECT
$search = '%' . $wpdb->esc_like( $term ) . '%';
$results = $wpdb->get_results( $wpdb->prepare( "... WHERE title LIKE %s", $search ) );
Issue #12: extract() の使用
エラー: 変数の衝突やセキュリティ上の脆弱性が発生します 出典: WordPress Coding Standards 発生する理由: extract() は配列のキーから変数を作成するためです 防止策: extract() は使用せず、配列の要素に直接アクセスします
// DANGEROUS
extract( $_POST );
// Now $any_array_key becomes a variable
// SAFE
$name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : '';
Issue #13: REST API の権限コールバック漏れ
エラー: エンドポイントに誰でもアクセスでき、不正アクセスや権限昇格につながる可能性があります
出典: WordPress REST API Handbook, Patchstack CVE Database
発生する理由: permission_callback が指定されていない、または機密性の高いエンドポイントで show_in_index => false が設定されていないためです
防止策: 必ず permission_callback を追加し、機密性の高いエンドポイントを REST インデックスから隠します
実際に発生した2025~2026年の脆弱性:
- All in One SEO(300万以上のサイト): 権限チェックが欠けていたため、コントリビューターレベルのユーザーがグローバル AI アクセストークンを閲覧できました
- AI Engine Plugin(CVE-2025-11749、CVSS 9.8 Critical):
show_in_index => falseの設定漏れにより、/wp-json/ インデックスでベアラートークンが露出し、認証されていない攻撃者に管理者の全権限が与えられました - SureTriggers: 脆弱性の公表から4時間以内に、不十分な認可チェックが悪用されました
- Worker for Elementor(CVE-2025-66144): サブスクライバーレベルの権限で、制限された機能を呼び出すことができました
// ❌ VULNERABLE - Missing permission_callback (WordPress 5.5+ requires it!)
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => 'GET',
'callback' => 'my_callback',
) );
// ✅ SECURE - Basic protection
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => 'GET',
'callback' => 'my_callback',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
) );
// ✅ SECURE - Hide sensitive endpoints from REST index
register_rest_route( 'myplugin/v1', '/admin', array(
'methods' => 'POST',
'callback' => 'my_admin_callback',
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
'show_in_index' => false, // Don't expose in /wp-json/
) );
2025~2026年の統計: 追跡された脆弱性は合計64,782件で、1週間に333件が新たに確認され、236件にはパッチが適用されないままでした。REST API の認証に関する問題は、脆弱性のかなりの割合を占めています。
Issue #14: アンインストールフックが繰り返し登録される
エラー: ページの読み込みごとにオプションが書き込まれます 出典: WordPress Plugin Handbook 発生する理由: メインの実行フローで register_uninstall_hook() を呼び出しているためです 防止策: 代わりに uninstall.php ファイルを使用します
// BAD - Runs on every page load
register_uninstall_hook( __FILE__, 'mypl_uninstall' );
// GOOD - Use uninstall.php file (preferred method)
// Create uninstall.php in plugin root
Issue #15: 無効化時のデータ削除
エラー: プラグインを一時的に無効化すると、ユーザーのデータが失われます 出典: WordPress プラグイン開発のベストプラクティス 原因: 無効化とアンインストールの混同 防止策: データを削除するのは uninstall.php 内だけにし、無効化時には削除しないでください
// WRONG - Deletes user data on deactivation
register_deactivation_hook( __FILE__, function() {
delete_option( 'mypl_user_settings' );
} );
// CORRECT - Only clear temporary data on deactivation
register_deactivation_hook( __FILE__, function() {
delete_transient( 'mypl_cache' );
} );
// CORRECT - Delete all data in uninstall.php
Issue #16: 非推奨関数の使用
エラー: WordPress のアップデート後にプラグインが動作しなくなります 出典: WordPress 非推奨関数リスト 原因: 新しい WordPress バージョンで削除された関数を使用しています 防止策: 開発中は WP_DEBUG を有効にしてください
// In wp-config.php (development only)
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
Issue #17: テキストドメインの不一致
エラー: 翻訳が読み込まれません 出典: WordPress 国際化 原因: テキストドメインがプラグインのスラッグと一致していません 防止策: あらゆる箇所でプラグインのスラッグを正確に使用してください
// Plugin header
// Text Domain: my-plugin
// In code - MUST MATCH EXACTLY
__( 'Text', 'my-plugin' );
_e( 'Text', 'my-plugin' );
Issue #18: プラグイン依存関係の不足
エラー: 必須プラグインが無効な場合に致命的なエラーが発生します 出典: WordPress プラグインの依存関係 原因: 必須プラグインを確認していません 防止策: plugins_loaded で依存関係を確認してください
add_action( 'plugins_loaded', function() {
if ( ! class_exists( 'WooCommerce' ) ) {
add_action( 'admin_notices', function() {
echo '<div class="error"><p>My Plugin requires WooCommerce.</p></div>';
} );
return;
}
// Initialize plugin
} );
Issue #19: 自動保存によるメタデータの保存
エラー: メタデータが複数回保存され、パフォーマンスに問題が発生します 出典: WordPress 投稿メタデータ 原因: save_post フックに自動保存のチェックがありません 防止策: DOING_AUTOSAVE 定数を確認してください
add_action( 'save_post', function( $post_id ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Safe to save meta
} );
Issue #20: admin-ajax.php のパフォーマンス
エラー: AJAX の応答が遅くなります 出典: https://deliciousbrains.com/comparing-wordpress-rest-api-performance-admin-ajax-php/ 原因: admin-ajax.php は WordPress コア全体を読み込みます 防止策: 新規プロジェクトでは REST API を使用してください(10 倍高速です)
// OLD: admin-ajax.php (still works but slower)
add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' );
// NEW: REST API (10x faster, recommended)
add_action( 'rest_api_init', function() {
register_rest_route( 'myplugin/v1', '/endpoint', array(
'methods' => 'POST',
'callback' => 'mypl_rest_handler',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
) );
} );
Issue #21: ブロックエディター用 show_in_rest の設定漏れ
エラー: カスタム投稿タイプで Gutenberg ブロックエディターではなくクラシックエディターが表示されます
出典: WordPress VIP ドキュメント、GitHub Issue #7595
原因: カスタム投稿タイプの登録時に show_in_rest => true の設定を忘れています
防止策: ブロックエディターが必要なカスタム投稿タイプには、必ず show_in_rest を指定してください
// ❌ WRONG - Block editor won't work
register_post_type( 'book', array(
'public' => true,
'supports' => array('editor'),
// Missing show_in_rest!
) );
// ✅ CORRECT
register_post_type( 'book', array(
'public' => true,
'show_in_rest' => true, // Required for block editor
'supports' => array('editor'),
) );
重要なルール: ブロックエディターと互換性があるのは、'show_in_rest' => true を指定して登録された投稿タイプだけです。ブロックエディターは WordPress REST API に依存しています。ブロックエディターと互換性のない投稿タイプ、または show_in_rest => false が指定された投稿タイプでは、代わりにクラシックエディターが読み込まれます。
Issue #22: wpdb::prepare() におけるテーブル名のエスケープ
エラー: 引用符で囲まれたテーブル名による SQL 構文エラー、またはプレフィックスのハードコードによる別環境での動作不良 出典: WordPress コーディング規約の Issue #2442 原因: テーブル名をプレースホルダーとして使用すると、テーブル名が引用符で囲まれます 防止策: テーブル名を prepare() のプレースホルダーに含めないでください
// ❌ WRONG - Adds quotes around table name
$table = $wpdb->prefix . 'my_table';
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM %s WHERE id = %d",
$table, $id
) );
// Result: SELECT * FROM 'wp_my_table' WHERE id = 1
// FAILS - table name is quoted
// ❌ WRONG - Hardcoded prefix
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM wp_my_table WHERE id = %d",
$id
) );
// FAILS if user changed table prefix
// ✅ CORRECT - Table name NOT in prepare()
$table = $wpdb->prefix . 'my_table';
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
$id
) );
// ✅ CORRECT - Using wpdb->prefix for built-in tables
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID = %d",
$id
) );
Issue #23: Nonce 検証のエッジケース
エラー:Nonce の失敗によってユーザー体験が分かりにくくなる、または誤った安心感を与える 出典:MalCare: wp_verify_nonce()、Pressidium: Understanding Nonces 発生する理由:Nonce の動作と制限についての誤解 予防策:Nonce のエッジケースを理解し、必ず権限チェックと組み合わせます
エッジケース:
- 時間ベースの戻り値:
$result = wp_verify_nonce( $nonce, 'action' );
// Returns 1: Valid, generated 0-12 hours ago
// Returns 2: Valid, generated 12-24 hours ago
// Returns false: Invalid or expired
-
Nonce の再利用:WordPress は nonce が使用済みかどうかを追跡しません。12~24 時間の有効期間内であれば、複数回使用できます。
-
セッションの無効化:Nonce は、有効なセッションに紐付いている場合にのみ有効です。ユーザーがログアウトすると、そのユーザーのすべての nonce が無効になり、フォームを開いたままにしていた場合、分かりにくいユーザー体験につながります。
-
キャッシュの問題:キャッシュプラグインが古い nonce を提供すると、不整合が発生することがあります。
-
認可の代替にはなりません:
// ❌ INSUFFICIENT - Only checks origin, not permission
if ( wp_verify_nonce( $_POST['nonce'], 'delete_user' ) ) {
delete_user( $_POST['user_id'] );
}
// ✅ CORRECT - Combine with capability check
if ( wp_verify_nonce( $_POST['nonce'], 'delete_user' ) &&
current_user_can( 'delete_users' ) ) {
delete_user( absint( $_POST['user_id'] ) );
}
重要な原則(2025 年):Nonce を認証や認可に頼ってはなりません。Nonce は侵害される可能性があると常に想定してください。current_user_can() を使用して関数を保護します。
Issue #24: フックの優先度と引数の数
エラー:フックのコールバックが想定した引数を受け取らない、または誤った順序で実行される 出典:Kinsta: WordPress Hooks Bootcamp 発生する理由:デフォルトでは引数は 1 つのみで、優先度は 10 です 予防策:必要に応じて、引数の数と優先度を明示的に指定します
// ❌ WRONG - Only receives $post_id
add_action( 'save_post', 'my_save_function' );
function my_save_function( $post_id, $post, $update ) {
// $post and $update are NULL!
}
// ✅ CORRECT - Specify argument count
add_action( 'save_post', 'my_save_function', 10, 3 );
function my_save_function( $post_id, $post, $update ) {
// Now all 3 arguments are available
}
// Priority matters (lower number = runs earlier)
add_action( 'init', 'first_function', 5 ); // Runs first
add_action( 'init', 'second_function', 10 ); // Default priority
add_action( 'init', 'third_function', 15 ); // Runs last
ベストプラクティス:
- 名前の衝突を避けるため、カスタムフック名には常にプレフィックスを付けます:
do_action( 'mypl_data_processed' )とし、do_action( 'data_processed' )は使用しません - フィルターは変更したデータを echo するのではなく、RETURN する必要があります
- フックの配置は後方互換性に影響するため、慎重に選択します
Issue #25: カスタム投稿タイプの URL の競合
エラー:パーマリンクをフラッシュしても、個別の CPT 投稿で 404 エラーが返される 出典:Permalink Manager Pro: URL Conflicts 発生する理由:CPT のスラッグが固定ページのスラッグと一致し、URL の競合が発生する 予防策:CPT に別のスラッグを使用するか、固定ページの名前を変更します
// ❌ CONFLICT - Page and CPT use same slug
// Page URL: example.com/portfolio/
register_post_type( 'portfolio', array(
'rewrite' => array( 'slug' => 'portfolio' ),
) );
// Individual posts 404: example.com/portfolio/my-project/
// ✅ SOLUTION 1 - Use different slug for CPT
register_post_type( 'portfolio', array(
'rewrite' => array( 'slug' => 'projects' ),
) );
// Posts: example.com/projects/my-project/
// Page: example.com/portfolio/
// ✅ SOLUTION 2 - Use hierarchical slug
register_post_type( 'portfolio', array(
'rewrite' => array( 'slug' => 'work/portfolio' ),
) );
// Posts: example.com/work/portfolio/my-project/
// ✅ SOLUTION 3 - Rename the page slug
// Change page from /portfolio/ to /our-portfolio/
Issue #26: WordPress 6.8 の bcrypt パスワードハッシュ化への移行
エラー:WordPress 6.8 へのアップグレード後、カスタムのパスワードハッシュ処理が機能しなくなる 出典:WordPress Core Make、GitHub Issue #21022 発生する理由:WordPress 6.8 以降では、パスワードハッシュ化方式が phpass から bcrypt に変更されました 予防策:WordPress のパスワード関数を使用し、ハッシュを直接処理しないでください
変更点(WordPress 6.8、2025 年 4 月):
- デフォルトのパスワードハッシュ化アルゴリズムが phpass から bcrypt に変更
- 新しいハッシュプレフィックス:
$wp$2y$(SHA-384 で事前ハッシュ化した bcrypt) - 既存のパスワードは、次回のログイン時に自動的に再ハッシュ化
- 一般的な bcrypt プラグイン(roots/wp-password-bcrypt)は不要に
// ✅ SAFE - These functions continue to work without changes
wp_hash_password( $password );
wp_check_password( $password, $hash );
// ⚠️ NEEDS UPDATE - Direct phpass hash handling
if ( strpos( $hash, '$P$' ) === 0 ) {
// Custom phpass logic - needs update for bcrypt
}
// ✅ NEW - Detect hash type
if ( strpos( $hash, '$wp$2y$' ) === 0 ) {
// bcrypt hash (WordPress 6.8+)
} elseif ( strpos( $hash, '$P$' ) === 0 ) {
// phpass hash (WordPress <6.8)
}
必要な対応:
- パスワードハッシュを直接処理するプラグインを確認します
- 6.8 以降にアップグレードする際は、bcrypt プラグインを削除します
- 標準的な wp_hash_password/wp_check_password の使用であれば、対応は不要です
Issue #27: WordPress 6.9 WP_Dependencies の非推奨化
エラー: "Deprecated: Function WP_Dependencies->add_data() was called with an argument that is deprecated" 情報源: WordPress 6.9 ドキュメント、WordPress サポートフォーラム 発生理由: WordPress 6.9(2025年12月2日)で、WP_Dependencies オブジェクトのメソッドが非推奨になりました 予防策: WordPress 6.9 で WP_DEBUG を有効にしてプラグインをテストし、非推奨のメソッドを置き換えます
影響を受けるプラグイン(確認済み):
- WooCommerce(10.4.2 で修正済み)
- Yoast SEO(26.6 で修正済み)
- Elementor(3.24 以降が必要)
破壊的変更: WordPress 6.9 では、古いテーマやプラグインが依存していた複数の非推奨関数が削除または変更され、カスタムメニューウォーカー、クラシックウィジェット、メディアモーダル、カスタマイザー機能が動作しなくなりました。
必要な対応:
- WordPress 6.9 で WP_DEBUG を有効にしてプラグインをテストします
- 非推奨の WP_Dependencies メソッドを置き換えます
- debug.log 内の非推奨通知を確認します
- 上位 1,000 件のプラグインは数時間以内に修正されましたが、メンテナンスされていないプラグインは対応が遅れることがよくあります
Issue #28: WordPress 6.7 での翻訳読み込みの変更
エラー: 翻訳が読み込まれない、またはデバッグ通知が表示される 情報源: WooCommerce 開発者ブログ、WordPress 6.7 フィールドガイド 発生理由: WordPress 6.7 以降で、翻訳の読み込みタイミングや方法が変更されました 予防策: 優先度 10 の 'init' の後に翻訳を読み込み、テキストドメインがプラグインのスラッグと一致していることを確認します
// ❌ WRONG - Loading too early
add_action( 'init', 'load_plugin_textdomain' );
// ✅ CORRECT - Load after 'init' priority 10
add_action( 'init', 'load_plugin_textdomain', 11 );
// Ensure text domain matches plugin slug EXACTLY
// Plugin header: Text Domain: my-plugin
__( 'Text', 'my-plugin' ); // Must match exactly
必要な対応:
- load_plugin_textdomain() の呼び出しタイミングを確認します
- テキストドメインがプラグインのスラッグと完全に一致していることを確認します
- WP_DEBUG を有効にしてテストします
Issue #29: wpdb::prepare() のプレースホルダー不足エラー
エラー: "The query argument of wpdb::prepare() must have a placeholder" 情報源: WordPress $wpdb ドキュメント、SitePoint: データベースの操作 発生理由: プレースホルダーを使わずに prepare() を使用しています 予防策: 動的データがない場合は prepare() を使用しないでください
// ❌ WRONG
$wpdb->prepare( "SELECT * FROM {$wpdb->posts}" );
// Error: The query argument of wpdb::prepare() must have a placeholder
// ✅ CORRECT - Don't use prepare() if no dynamic data
$wpdb->get_results( "SELECT * FROM {$wpdb->posts}" );
// ✅ CORRECT - Use prepare() for dynamic data
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID = %d",
$post_id
) );
wpdb::prepare() のその他の誤り:
- パーセント記号の扱い:
// ❌ WRONG
$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE '%test%'" );
// ✅ CORRECT
$search = '%' . $wpdb->esc_like( $term ) . '%';
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
$search
) );
- 引数形式の混在:
// ❌ WRONG - Can't mix individual args and array
$wpdb->prepare( "... WHERE id = %d AND name = %s", $id, array( $name ) );
// ✅ CORRECT - Pick one format
$wpdb->prepare( "... WHERE id = %d AND name = %s", $id, $name );
// OR
$wpdb->prepare( "... WHERE id = %d AND name = %s", array( $id, $name ) );
プラグインのアーキテクチャパターン
シンプル(関数のみ)
小規模なプラグイン(関数が 5 個未満):
function mypl_init() { /* code */ }
add_action( 'init', 'mypl_init' );
OOP(シングルトン)
中規模のプラグイン:
class MyPL_Plugin {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) self::$instance = new self();
return self::$instance;
}
private function __construct() {
add_action( 'init', array( $this, 'init' ) );
}
}
MyPL_Plugin::get_instance();
PSR-4(モダン、2025 年以降推奨)
大規模またはチーム開発のプラグイン:
my-plugin/
├── my-plugin.php
├── composer.json → "psr-4": { "MyPlugin\\": "src/" }
└── src/Admin.php
// my-plugin.php
require_once __DIR__ . '/vendor/autoload.php';
use MyPlugin\Admin;
new Admin();
よく使われるパターン
カスタム投稿タイプ(重要: 有効化時に rewrite rules をフラッシュし、ブロックエディター用に show_in_rest を設定します):
// show_in_rest => true REQUIRED for Gutenberg block editor
register_post_type( 'book', array(
'public' => true,
'show_in_rest' => true, // Without this, block editor won't work!
'supports' => array( 'editor', 'title' ),
) );
register_activation_hook( __FILE__, function() {
mypl_register_cpt();
flush_rewrite_rules(); // NEVER call on every page load
} );
カスタムタクソノミー:
register_taxonomy( 'genre', 'book', array( 'hierarchical' => true, 'show_in_rest' => true ) );
メタボックス:
add_meta_box( 'book_details', 'Book Details', 'mypl_meta_box_html', 'book' );
// Save: Check nonce, DOING_AUTOSAVE, current_user_can('edit_post')
update_post_meta( $post_id, '_book_isbn', sanitize_text_field( $_POST['book_isbn'] ) );
Settings API:
register_setting( 'mypl_options', 'mypl_api_key', array( 'sanitize_callback' => 'sanitize_text_field' ) );
add_settings_section( 'mypl_section', 'API Settings', 'callback', 'my-plugin' );
add_settings_field( 'mypl_api_key', 'API Key', 'field_callback', 'my-plugin', 'mypl_section' );
REST API(admin-ajax.php より 10 倍高速):
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => 'POST',
'callback' => 'mypl_rest_callback',
'permission_callback' => fn() => current_user_can( 'edit_posts' ),
) );
AJAX(レガシー。新規プロジェクトでは REST API を使用します):
add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' );
check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );
wp_send_json_success( array( 'message' => 'Success' ) );
カスタムテーブル:
global $wpdb;
$sql = "CREATE TABLE {$wpdb->prefix}mypl_data (id bigint AUTO_INCREMENT PRIMARY KEY, ...)";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
Transients(キャッシュ):
$data = get_transient( 'mypl_data' );
if ( false === $data ) {
$data = expensive_operation();
set_transient( 'mypl_data', $data, 12 * HOUR_IN_SECONDS );
}
同梱リソース
テンプレート: plugin-simple/、plugin-oop/、plugin-psr4/、examples/meta-box.php、examples/settings-page.php、examples/custom-post-type.php、examples/rest-endpoint.php、examples/ajax-handler.php
スクリプト: scaffold-plugin.sh、check-security.sh、validate-headers.sh
参考資料: security-checklist.md、hooks-reference.md、sanitization-guide.md、wpdb-patterns.md、common-errors.md
高度なトピック
i18n(国際化):
load_plugin_textdomain( 'my-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
__( 'Text', 'my-plugin' ); // Return translated
_e( 'Text', 'my-plugin' ); // Echo translated
esc_html__( 'Text', 'my-plugin' ); // Translate + escape
WP-CLI:
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'mypl', 'MyPL_CLI_Command' );
}
Cron イベント:
register_activation_hook( __FILE__, fn() => wp_schedule_event( time(), 'daily', 'mypl_daily_task' ) );
register_deactivation_hook( __FILE__, fn() => wp_clear_scheduled_hook( 'mypl_daily_task' ) );
add_action( 'mypl_daily_task', 'mypl_do_daily_task' );
プラグインの依存関係:
if ( ! class_exists( 'WooCommerce' ) ) {
deactivate_plugins( plugin_basename( __FILE__ ) );
add_action( 'admin_notices', fn() => echo '<div class="error"><p>Requires WooCommerce</p></div>' );
}
配布と自動更新
GitHub 自動更新(YahnisElsts による Plugin Update Checker):
// 1. Install: git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
// 2. Add to main plugin file
require plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
$updateChecker = PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin-slug'
);
$updateChecker->getVcsApi()->enableReleaseAssets(); // Use GitHub Releases
// Private repos: Define token in wp-config.php
if ( defined( 'YOUR_PLUGIN_GITHUB_TOKEN' ) ) {
$updateChecker->setAuthentication( YOUR_PLUGIN_GITHUB_TOKEN );
}
デプロイ:
git tag 1.0.1 && git push origin main && git push origin 1.0.1
# Create GitHub Release with ZIP (exclude .git, tests)
代替手段:Git Updater(コーディング不要)、Custom Update Server(完全な制御)、Freemius(商用)
セキュリティ:HTTPS を使用し、トークンをハードコードせず、ライセンスを検証し、更新チェックをレート制限してください。
重要:ZIP にはプラグインフォルダーを含める必要があります:plugin.zip/my-plugin/my-plugin.php
リソース:references/github-auto-updates.md、examples/github-updater.php を参照してください。
依存関係
必須:
- WordPress 5.9+(6.7+ を推奨)
- PHP 7.4+(8.0+ を推奨)
任意:
- Composer 2.0+ - PSR-4 オートローディング用
- WP-CLI 2.0+ - コマンドラインでのプラグイン管理用
- Query Monitor - デバッグとパフォーマンス分析用
公式ドキュメント
- WordPress プラグインハンドブック:https://developer.wordpress.org/plugins/
- WordPress コーディング規約:https://developer.wordpress.org/coding-standards/
- WordPress REST API:https://developer.wordpress.org/rest-api/
- WordPress データベースクラス($wpdb):https://developer.wordpress.org/reference/classes/wpdb/
- WordPress セキュリティ:https://developer.wordpress.org/apis/security/
- Settings API:https://developer.wordpress.org/plugins/settings/settings-api/
- カスタム投稿タイプ:https://developer.wordpress.org/plugins/post-types/
- Transients API:https://developer.wordpress.org/apis/transients/
- Context7 ライブラリ ID:/websites/developer_wordpress
トラブルシューティング
致命的なエラー:WP_DEBUG を有効にし、wp-content/debug.log を確認し、プレフィックス付きの名前を検証し、依存関係を確認してください。
CPT の 404 エラー:設定 → パーマリンク → 保存からリライトルールをフラッシュしてください。
Nonce の失敗:Nonce 名とアクションが一致しているか確認し、有効期限が切れていないことを確認してください(デフォルトは 24 時間)。
AJAX が 0/-1 を返す:アクション名が wp_ajax_{action} と一致しているか確認し、Nonce が送信・検証されていることを確認してください。
HTML が削除される:安全な HTML には sanitize_text_field() ではなく wp_kses_post() を使用してください。
クエリの失敗:$wpdb->prepare() を使用し、$wpdb->prefix を確認し、構文を検証してください。
セットアップ完了チェックリスト
このチェックリストを使用して、プラグインを確認してください。
- プラグインヘッダーにすべての項目が記載されている
- すべての PHP ファイルの先頭に ABSPATH チェックがある
- すべての関数/クラスで一意のプレフィックスを使用している
- すべてのフォームで Nonce 検証を行っている
- すべてのユーザー入力をサニタイズしている
- すべての出力をエスケープしている
- すべてのデータベースクエリで $wpdb->prepare() を使用している
- 権限をチェックしている(is_admin() だけではない)
- カスタム投稿タイプの有効化時にリライトルールをフラッシュしている
- 無効化フックでは一時データのみを消去している
- uninstall.php で永続データをクリーンアップしている
- テキストドメインがプラグインのスラッグと一致している
- スクリプト/スタイルを必要な場所でのみ読み込んでいる
- 開発中に WP_DEBUG を有効にしている
- Query Monitor でパフォーマンスをテストしている
- 非推奨関数の警告がない
- 最新の WordPress バージョンで動作する
質問や問題がある場合
- 詳細なトラブルシューティングについては
references/common-errors.mdを確認してください。 - セキュリティ基盤のすべての手順を確認してください。
- 公式ドキュメントを確認してください:https://developer.wordpress.org/plugins/
- WP_DEBUG を有効にし、debug.log を確認してください。
- Query Monitor プラグインを使用して、フックとクエリをデバッグしてください。
最終確認日:2026-01-21 | スキルのバージョン:2.0.0 | 変更点:WordPress 6.7~6.9 の調査から新たに 9 件の問題を追加しました(bcrypt の移行、WP_Dependencies の非推奨化、翻訳の読み込み、REST API の CVE 2025-2026、wpdb::prepare() のエッジケース、Nonce の制限、フックに関する注意点、CPT の URL 競合)。文書化して防止するエラーを 20 件から 29 件に更新しました。エラー数:20 → 29。バージョン:1.x → 2.0.0(WordPress のバージョン固有のコンテンツを大幅に追加したため、メジャーバージョンを更新)。