wp-plugin-development
WordPressプラグインのアーキテクチャレビューとWordPress.org投稿規格に対応します。プラグインコードのレビュー、プラグインアーキテクチャの監査、WordPress.org プラグイン投稿の準備、プラグインのベストプラクティス確認、カスタム投稿タイプ・タクソノミー・Settings API・フックシステム・多言語化の分析、または「プラグインレビュー」「プラグイン開発」「プラグインのベストプラクティス」「WordPress.org投稿」「Plugin Check」「プラグインヘッダー」「アクティベーションフック」「ディアクティベーションフック」「アンインストール」「カスタム投稿タイプ」「タクソノミー」「Settings API」「フック」「アクション」「フィルター」「i18n」「テキストドメイン」「プレフィックス」「ネームスペーシング」などのキーワードが含まれる場合に利用できます。プラグインアーキテクチャ、ライフサイクルフック、WordPress.org準拠性、API統合の問題を検出します。
description の原文を見る
WordPress plugin architecture review and WordPress.org submission standards. Use when reviewing WordPress plugin code, auditing plugin architecture, preparing WordPress.org plugin submission, checking plugin best practices, analyzing custom post types, taxonomies, Settings API, hooks system, internationalization, or when user mentions "plugin review", "plugin development", "plugin best practices", "WordPress.org submission", "Plugin Check", "plugin headers", "activation hook", "deactivation hook", "uninstall", "custom post type", "taxonomy", "Settings API", "hooks", "actions", "filters", "i18n", "text domain", "prefixing", "namespacing". Detects issues in plugin architecture, lifecycle hooks, WordPress.org compliance, and API integration.
SKILL.md 本文
WordPress プラグイン開発レビュースキル
概要
WordPress 6.x 以上対応のプラグインに向けた体系的なプラグインアーキテクチャレビュー。中核原則: WordPress プラグインは標準化された API エコシステム(ライフサイクルフック(アクティベーション/ディアクティベーション/アンインストール)、Settings API、フックシステム(アクション/フィルター)、カスタム投稿タイプ/タクソノミー、国際化)を通じて機能を拡張します。レビューはプラグインアーキテクチャ、命名規則、WordPress.org 適合基準を検証し、セキュリティ固有のパターン(nonce、ケーパビリティ、サニタイゼーション)について wp-security-review を参照します。調査結果はファイル単位で行番号、重大度ラベル(CRITICAL/WARNING/INFO)、適切な実装を示す BAD/GOOD コードペアをグループ化して報告します。
使用すべき場合
以下の場合に使用します:
- プラグインコードアーキテクチャレビュー
- WordPress.org プラグインディレクトリへの投稿前チェック
- プラグインライフサイクルフック(アクティベーション/ディアクティベーション/アンインストール)の監査
- カスタム投稿タイプまたはタクソノミー登録のレビュー
- Settings API 実装の検証
- フックシステムの使用法チェック(アクション/フィルター、優先度、削除)
- 国際化(i18n/l10n)監査
- WordPress Plugin Check(PCP)適合性検証
- プリフィックスとネームスペース衝突検出
以下の場合には使用しません:
- テーマ開発(利用可能な場合は wp-theme-development を使用)
- Gutenberg ブロック開発(利用可能な場合は wp-block-development を使用)
- WooCommerce 拡張開発(利用可能な場合は wp-woocommerce-dev を使用)
- セキュリティ専用監査(包括的なセキュリティ分析には wp-security-review を使用)
- パフォーマンス専用監査(wp-performance-review を使用)
コードレビューワークフロー
体系的なプラグインレビューのため、以下の 6 段階ワークフローに従います:
-
プラグインの種類とコンテキストを特定する
- WordPress.org 投稿 → 最も厳しい基準を適用(Plugin Check 適合)
- プライベート/エンタープライズプラグイン → 命名規則の柔軟性が高い、readme.txt は不要
- 必須プラグイン(mu-plugin) → 異なるロード方法(アクティベーションフック不要)
- ドロップインプラグイン → 特別なファイル(object-cache.php、db.php)と異なるパターン
-
プラグインヘッダーの完全性とメタデータを確認する
- 必須:
Plugin Nameフィールドが存在 - 推奨: Version、Description、Author、Text Domain、Requires at least、Requires PHP、License
- テキストドメインがプラグインスラッグと完全に一致すること(小文字、ダッシュはアンダースコアではない)を確認
- 必須:
-
CRITICAL パターンをスキャンする(機能を破壊するか、WordPress.org 拒否を引き起こす)
- PHP ファイルに
defined( 'ABSPATH' ) || exit;チェックがない - グローバルスコープにプリフィックスのない関数/クラス(ネームスペース衝突リスク)
- アクティベーション/ディアクティベーションフック(セットアップ/クリーンアップ用)がない
- ヘッダーと i18n 関数呼び出しの間のテキストドメイン不一致
- アンインストールクリーンアップがない(uninstall.php または register_uninstall_hook がない)
initフック上で呼び出されるflush_rewrite_rules()(ページ読み込みごとにコストの高い操作)$wpdb->query()を使用した直接的なデータベーステーブル作成(dbDelta()ではなく)- フィルターコールバックに戻り値がない
- REST API ルートに
permission_callbackがない(WordPress 5.5 以降の要件) - 投稿タイプキーが 20 文字を超える
- タクソノミー名が 32 文字を超える
- PHP ファイルに
-
WARNING パターンを確認する(標準的ではないが機能する)
- ユーザー向け文字列の国際化(i18n)がない
- テキストドメインパラメータなしの
__()または_e()呼び出し - ハードコードされた WordPress パス(
/wp-content/plugins/) register_setting()にsanitize_callbackがない- Gutenberg 互換性の
show_in_restがない - 管理者コードがフロントエンドで読み込まれている(
is_admin()チェック欠落)
-
INFO 改善を記録する(ベストプラクティスと最適化)
- プリフィックスの代わりに PHP ネームスペースを使用できる
- プラグインヘッダーにバージョン要件がない(
Requires at least、Requires PHP) - Block Editor サポート用に
show_in_restを追加できる - 管理者アセット読み込みが全体的に行われている(条件付きロードが必要)
- ヘッダーに
Domain Pathがない
-
コンテキスト対応の重大度を適用して報告
- WordPress.org コンテキスト: 最も厳しい実施(Plugin Check 失敗 = CRITICAL)
- プライベートプラグインコンテキスト: より柔軟(プリフィックスは重要、readme.txt はオプション)
- 必須プラグイン: 期待を調整(アクティベーションフックは適用されない)
- 以下の出力形式を使用して報告
- セキュリティ上の懸念が見つかった場合、「セキュリティの問題が検出されました。包括的なセキュリティ分析については
/wp-sec-reviewを実行してください。」というメモを追加
ファイルタイプ固有のチェック
メインプラグインファイル(例:my-plugin.php)
プラグインヘッダー(PLG-02):
- CRITICAL:
Plugin Nameフィールドがない → WordPress はプラグインを認識しない - WARNING:
Text Domainがない → 国際化を破壊 - WARNING: テキストドメインがプラグインスラッグと一致しない → WordPress.org 拒否
- INFO:
Requires at leastまたはRequires PHPがない → バージョン要件を実施できない
ABSPATH チェック(PLG-03):
- CRITICAL: 最初の 10 行に
defined( 'ABSPATH' ) || exit;がない → 直接ファイルアクセス可能 - 検出パターン: ABSPATH チェックなしの PHP ファイル
プリフィックス(PLG-04):
- CRITICAL: グローバルスコープのプリフィックスのない関数 → 他のプラグインとのネームスペース衝突
- CRITICAL: グローバルスコープのプリフィックスのないクラス → 別のプラグインが同じ名前を使用している場合、致命的エラー
- CRITICAL: プリフィックスのないグローバル変数 → 変数衝突リスク
- WARNING: プリフィックスのないオプション/トランジェント → データベースキー衝突リスク
- パターン: プラグインプリフィックスのない
function [a-z_]+\((最小 4-5 文字) - 代替案: PHP ネームスペースはプリフィックスの必要性を排除
アクティベーション/ディアクティベーションフック(PLG-04):
- WARNING: セットアップ要件があるのに
register_activation_hook()がない → 初期化方法がない - WARNING: 一時的なデータがあるのに
register_deactivation_hook()がない → ディアクティベーション時のクリーンアップがない - パターン:
register_activation_hook( __FILE__, 'callback' )を探す
アンインストールファイル(uninstall.php)
WP_UNINSTALL_PLUGIN 定数チェック(PLG-05):
- CRITICAL:
WP_UNINSTALL_PLUGINチェックのないuninstall.php→ 直接実行可能 - パターン:
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { exit; }
クリーンアップの完全性(PLG-05):
- WARNING: オプションクリーンアップがない(
delete_option()) → データベースにプラグインデータが残る - WARNING: トランジェントクリーンアップがない(
delete_transient()) → データベース肥大化 - WARNING: カスタムテーブルクリーンアップがない(
DROP TABLE) → 孤立したテーブル - WARNING: ユーザーメタクリーンアップがない(
delete_metadata( 'user', ... )) → ユーザーデータが残る - コンテキスト: これらのデータタイプがプラグインで作成される場合のみ適用
カスタム投稿タイプファイル
register_post_type() 使用(PLG-06):
- CRITICAL: 投稿タイプキーが 20 文字を超える → トリミング問題、パーマリンク問題
- CRITICAL: 投稿タイプキーがプリフィックスなし → WordPress コアまたは他のプラグインとのクエリ変数衝突
- WARNING:
show_in_rest => trueがない → Block Editor でアクセス不可 - WARNING:
initでflush_rewrite_rules()が呼び出される → パフォーマンス問題(ページロードごとの DB 書き込み) - パターン:
register_post_type( 'key', array( ... ) )からinitへのフック - ベストプラクティス: 投稿タイプキーは最大 20 文字、小文字英数字 + ダッシュ/アンダースコア
書き込みフラッシュのタイミング(PLG-06):
- CRITICAL:
initフック上のflush_rewrite_rules()→ ページロードごとの DB 書き込み - 正しいパターン: 投稿タイプ登録後、アクティベーションフックでのみ呼び出し
タクソノミーファイル
register_taxonomy() 使用(PLG-07):
- CRITICAL: タクソノミー名が 32 文字を超える → データベースエラー
- CRITICAL: タクソノミー名がプリフィックスなし → コアタクソノミーまたは他のプラグインとの衝突
- WARNING:
show_in_rest => trueがない → Block Editor でアクセス不可 - パターン:
register_taxonomy( 'name', 'post_type', array( ... ) )からinitへのフック - ベストプラクティス: タクソノミー名は最大 32 文字、小文字文字 + アンダースコアのみ
Settings API ファイル
register_setting() 使用(PLG-08):
- CRITICAL: 対応する
register_setting()のないadd_settings_field()→ フィールドは保存されない - WARNING:
sanitize_callbackのないregister_setting()→ サニタイズされていないデータが保存される - WARNING:
typeパラメータのないregister_setting()→ 型推論が不安定 - パターン:
register_setting( 'option_group', 'option_name', array( ... ) )onadmin_init
Settings API ワークフロー(PLG-09):
- WARNING:
add_settings_section()ページスラッグがdo_settings_sections()呼び出しと一致しない → セクションはレンダリングされない - WARNING:
add_settings_field()セクション ID がadd_settings_section()ID と一致しない → フィールドはレンダリングされない - WARNING: フォームに
settings_fields()呼び出しがない → nonce 保護なし、オプションが保存されない - WARNING: フォームに
do_settings_sections()呼び出しがない → フィールドはレンダリングされない - パターン: 完全なワークフローには
register_setting()→add_settings_section()→add_settings_field()→settings_fields()とdo_settings_sections()を含むフォーム HTML が必要
フック使用ファイル
add_action/add_filter パターン(PLG-10):
- WARNING: 順序が重要な場合、フック優先度が指定されていない → 誤ったシーケンスで実行される可能性
- WARNING: コールバックが複数のパラメータが必要な場合、
accepted_argsが指定されていない → パラメータが切り詰められる - INFO: 明確にするため優先度を明示的に指定できる(デフォルト 10 を使用している場合でも)
- パターン:
add_action( 'hook_name', 'callback', $priority, $accepted_args )
フィルター戻り値要件(PLG-10):
- CRITICAL: フィルターコールバックが値を返さない → フィルターチェーンを破壊、致命的エラーを引き起こす
- パターン:
add_filter( 'hook', 'callback' )で、コールバックに戻り文がない - チェック: すべてのフィルターコールバックは必ず値を返す必要がある
フック削除(PLG-10):
- WARNING:
remove_action()またはremove_filter()のパラメータが登録と一致しない → フックが削除されない - パターン: 優先度と accepted_args は、元の
add_action()/add_filter()呼び出しと完全に一致する必要がある - 例:
add_action( 'init', 'callback', 15 )にはremove_action( 'init', 'callback', 15 )が必要
拡張性のためのカスタムフック(PLG-10):
- INFO: プラグインは拡張性のためにカスタムフック
do_action()またはapply_filters()を提供できる - パターン:
do_action( 'myplugin_after_save', $data )またはapply_filters( 'myplugin_modify_output', $output )
国際化(i18n)ファイル
テキストドメイン一致(PLG-11):
- CRITICAL: プラグインヘッダーのテキストドメインが i18n 呼び出しのテキストドメインと一致しない → 翻訳読み込みが失敗
- CRITICAL: テキストドメインがダッシュの代わりにアンダースコアを使用 → WordPress.org 拒否
- CRITICAL: テキストドメインがプラグインスラッグと一致しない → Plugin Check 失敗
- パターン:
Text Domain: my-pluginはプラグインフォルダ/ファイル名と完全に一致する必要がある
i18n 関数使用(PLG-11):
- WARNING: テキストドメインパラメータなしの
__()または_e()→ コア翻訳へのフォールバック(不正確) - WARNING: 翻訳可能な文字列に埋め込まれた変数 → 翻訳抽出を破壊
- パターン: 常に
__( 'Text', 'text-domain' )、決して__( 'Text' )ではない - パターン:
sprintf( __( 'Text %s', 'domain' ), $var )を使用、__( "Text $var", 'domain' )ではない
複数形化(PLG-11):
- WARNING: if/else で複数形を処理(
_n()の代わりに) → 複雑な複数形規則を持つ言語を破壊 - パターン: カウント依存文字列には
_n( 'singular', 'plural', $count, 'domain' )を使用
コンテキスト対応翻訳(PLG-11):
- INFO: 同じ英単語が異なる翻訳を持つ場合、
_x()を使用してあいまいさを解消できる - パターン:
_x( 'Post', 'noun', 'domain' )vs_x( 'Post', 'verb', 'domain' )
エスケープされた翻訳バリアント(PLG-11):
- INFO: 翻訳 + エスケープを組み合わせるには
esc_html__()またはesc_attr__()を使用 - パターン:
echo esc_html__( 'Text', 'domain' )をecho esc_html( __( 'Text', 'domain' ) )の代わりに使用
翻訳読み込み(PLG-11):
- INFO: WordPress 4.6 以降の場合、
load_plugin_textdomain()はオプション(translate.wordpress.org から自動読み込み) - パターン: 呼び出される場合は
initへのフック:add_action( 'init', 'load_textdomain_callback' )
REST API エンドポイント(PLG-13、表面レベル)
register_rest_route() 使用:
- CRITICAL:
permission_callbackがない → WordPress 5.5 以降のエラー、エンドポイントがブロック - CRITICAL: 書き込み操作時に
permission_callback => '__return_true'→ 認証されていないアクセスが許可される - パターン:
register_rest_route( 'namespace/v1', '/route', array( ... ) )onrest_api_initフック - 注:
__return_trueは公開読み取り専用エンドポイントに正しい
ネームスペースとバージョン管理:
- WARNING: ネームスペースにバージョンがない → API 進化パスがない
- パターン: 単なる
mypluginではなくmyplugin/v1を使用
パラメータ検証:
- WARNING: args に
sanitize_callbackがない → サニタイズされていない入力 - WARNING: args に
validate_callbackがない → 無効な入力が受け入れられる - セキュリティの深掘り: wp-security-review スキルを参照
AJAX ハンドラ(PLG-14、表面レベル)
フック登録:
- WARNING: 高頻度呼び出しで
admin-ajax.phpを使用 → REST API の検討(パフォーマンス向上) - パターン: 認証用
add_action( 'wp_ajax_action_name', 'callback' ) - パターン: パブリック用
add_action( 'wp_ajax_nopriv_action_name', 'callback' )
セキュリティチェック:
- 簡単なリマインダー: AJAX ハンドラは nonce 検証(
check_ajax_referer())とケーパビリティチェックが必要 - セキュリティの深掘り: wp-security-review スキルを参照
管理メニュー(PLG-15、表面レベル)
add_menu_page/add_submenu_page:
- WARNING: ケーパビリティパラメータがない → アクセス制御がない
- WARNING: ページコールバックが
current_user_can()を確認しない → ケーパビリティパラメータだけでは不十分 - パターン:
add_menu_page( $title, $menu_title, 'manage_options', $slug, $callback )
データベーステーブル(PLG-16、表面レベル)
テーブル作成:
- CRITICAL:
dbDelta()の代わりに$wpdb->query( "CREATE TABLE..." )を使用 → アップグレードパスなし、文字セット問題 - パターン:
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta( $sql ); - パターン:
$wpdb->prefixでテーブル名を指定(wp_ハードコーディングではない) - パターン:
$wpdb->get_charset_collate()で文字セット/照合を指定
dbDelta SQL フォーマット要件:
- WARNING: dbDelta 用に SQL がフォーマットされていない → テーブル作成が無音で失敗
- 要件: PRIMARY KEY の後に 2 つのスペース、= の周りにスペースなし、各フィールドが独立した行
Cron ジョブ(PLG-17、表面レベル)
wp_schedule_event() 使用:
- WARNING:
wp_next_scheduled()をチェックせずにwp_schedule_event()→ 重複スケジュール - WARNING: ディアクティベーション時にスケジュール済みイベントがクリアされない → プラグイン無効化後も Cron ジョブが続行
- パターン:
wp_schedule_event()の前にif ( ! wp_next_scheduled( 'hook' ) )をチェック - パターン: ディアクティベーションフックで
wp_clear_scheduled_hook( 'hook' )
トランジェント(PLG-18、表面レベル)
set_transient() 使用:
- WARNING: トランジェントキーがプリフィックスなし → 他のプラグインとのキー衝突
- WARNING: 有効期限のないトランジェント → 永続的なオプションのように動作
- パターン:
set_transient( 'mypl_key', $value, HOUR_IN_SECONDS )
クイック検出用の検索パターン(PLG-21)
クイックプラグインスキャン用に rg コマンドを使用します。重大度別に整理されています。
CRITICAL パターン
# メインプラグインファイルには「Plugin Name」ヘッダーが含まれるべき
# これが何も返さない場合、プラグインは有効なメインヘッダーがない可能性がある。
rg -n "Plugin Name:" . -g '*.php'
# ABSPATH チェックがない PHP ファイル(直接ファイルアクセス可能)
rg -L --iglob '*.php' "defined\s*\(\s*['\"]ABSPATH['\"]\s*\)|defined\s*\(\s*'ABSPATH'\s*\)" .
# グローバルスコープのプリフィックスのない関数宣言(ネームスペース衝突リスク)
rg -n "^function\s+[a-z_][a-z0-9_]*\s*\(" . -g '*.php'
# テキストドメイン不一致(ヘッダーと i18n 呼び出しを比較)
# まず: メインプラグインファイルヘッダーで予期されるテキストドメインを検査
rg -n "Text Domain:" . -g '*.php'
# その後: 翻訳呼び出しでそのスラッグをプラグイン全体と比較
rg -n "__\(|_e\(|_n\(|_x\(" . -g '*.php'
# アクティベーション/ディアクティベーション外の flush_rewrite_rules(ページロードごとに高コストな操作)
rg -n "flush_rewrite_rules" . -g '*.php'
# 戻り値がないフィルターコールバック(手動フォローアップが必要)
rg -n "add_filter\s*\(" . -g '*.php'
# permission_callback のない register_rest_route(手動でルートごとにレビュー)
rg -n "register_rest_route\s*\(" . -g '*.php'
WARNING パターン
# register_activation_hook がない(セットアップ機構がない)
rg -n "register_activation_hook\s*\(" . -g '*.php'
# アンインストールクリーンアップがない(uninstall.php がなく register_uninstall_hook もない)
test -f uninstall.php || rg -n "register_uninstall_hook\s*\(" . -g '*.php'
# 明らかなテキストドメインパラメータのない i18n 関数(手動確認が必要)
rg -n "__\(|_e\(" . -g '*.php'
# ハードコードされた /wp-content/plugins/ パス(WordPress インストーションによってカスタマイズされる)
rg -n "/wp-content/plugins/" . -g '*.php'
# $wpdb->query での直接的な CREATE TABLE(dbDelta を使用すべき)
rg -n "wpdb->query\s*\(.*CREATE TABLE" . -g '*.php'
# 投稿タイプキー長チェック(最大 20 文字)
rg -n "register_post_type\s*\(" . -g '*.php'
# register_post_type で show_in_rest がない(手動でレジストレーションごとにレビュー)
rg -n "register_post_type\s*\(" . -g '*.php'
INFO パターン
# プラグインヘッダーに「Requires at least」または「Requires PHP」がない
rg -n "Requires at least:|Requires PHP:" . -g '*.php'
# PHP ネームスペースを使用していない関数(最新化できる)
rg -n "^function\s+myprefix_" . -g '*.php'
# フロントエンドで読み込まれている管理者コード(手動コンテキストチェック)
rg -n "add_action\s*\(\s*['\"]admin_" . -g '*.php'
# プラグインヘッダーに Domain Path がない
rg -n "Domain Path:" . -g '*.php'
プラットフォームコンテキスト(PLG-12)
WordPress プラグインはさまざまな配布コンテキストで動作します。コンテキストに基づいてレビューの重大度を調整します。
WordPress.org 投稿コンテキスト
最も厳しい基準が適用されます。 Plugin Check(PCP)ツールはこれらの要件を実施します:
Plugin Check(PCP)適合性:
- 適切なプラグインヘッダー(Plugin Name 必須、Text Domain 推奨)
- テキストドメインはプラグインスラッグと完全に一致する必要がある(小文字、ダッシュはアンダースコアではない)
- すべての PHP ファイルは ABSPATH チェックを持つ必要がある(
defined( 'ABSPATH' ) || exit;) - すべての i18n 関数はテキストドメインパラメータを含む必要がある
- 廃止された WordPress 関数を使用しない
- ライセンス互換性(WordPress.org に必要な GPLv2 以上)
- readme.txt に適切なフォーマット(または readme.md)
- ハードコードされた
/wp-content/パスなし(WP_CONTENT_DIRまたはヘルパー関数を使用) - すべてのグローバルスコープ関数/クラス/オプション上の適切なプリフィックス
検証方法: プラグインチェックツールを投稿前に実行:
# WP-CLI 経由
wp plugin install plugin-check --activate
wp plugin check my-plugin.php
# または WordPress 管理画面経由
# ナビゲート先: Plugins > Add New > Search "Plugin Check"
# その後: Tools > Plugin Check > Select your plugin
WordPress.org コンテキスト重大度エスカレーション:
- テキストドメイン欠落 → CRITICAL(以前は WARNING)
- テキストドメイン不一致 → CRITICAL(以前は WARNING)
- ABSPATH チェック欠落 → CRITICAL(以前は WARNING)
- 廃止された関数 → CRITICAL(以前は INFO)
プライベート/エンタープライズプラグインコンテキスト
より柔軟性があります。 WordPress.org 投稿要件はありません。
緩和された要件:
- readme.txt は不要(内部ドキュメントで十分)
- 独自ライセンス使用可能(GPL 要件なし)
- 命名規則はより柔軟(プリフィックスは重要で他のプラグインとの衝突を避けるため)
- カスタム更新メカニズムを使用可能(WordPress.org 更新システムなし)
重要な点:
- 他のプラグインとの衝突を避けるためのプリフィックス
- セットアップ/クリーンアップのためのライフサイクルフック
- セキュリティベストプラクティス(wp-security-review を参照)
必須プラグイン(mu-plugins)コンテキスト
異なる読み込み動作。 必須プラグインは通常のプラグインの前に自動的に読み込まれます。
主な違い:
wp-content/mu-plugins/ディレクトリからアルファベット順に読み込まれます- 通常のプラグインの前に読み込まれます
- 管理 UI 経由で無効化できません
- アクティベーション/ディアクティベーションフックがありません(アクティベーション/ディアクティベーションがなく、単にロードされる)
- WordPress 管理経由での更新はできません(手動ファイル置換)
mu-plugins のレビュー調整:
- WARNING(CRITICAL ではなく):
register_activation_hook()がない → mu-plugins では予想される - WARNING(CRITICAL ではなく):
register_deactivation_hook()がない → mu-plugins では予想される - まだ必要: 適切なプリフィックス、ABSPATH チェック、i18n、セキュリティプラクティス
mu-plugin コンテキスト検出パターン:
// プラグインが必須として読み込まれているかを確認
if ( strpos( __FILE__, WPMU_PLUGIN_DIR ) !== false ) {
// 必須プラグインとして実行 - アクティベーションフックをスキップ
}
ドロップインプラグインコンテキスト
特定の名前を持つ特別なファイル。 ドロップインは WordPress コア機能を置き換えます。
一般的なドロップイン:
object-cache.php→ オブジェクトキャッシング バックエンド(Redis、Memcached)db.php→ カスタムデータベースクラスadvanced-cache.php→ ページキャッシングシステムdb-error.php→ カスタムデータベースエラーページmaintenance.php→ カスタムメンテナンスモードページ
ドロップインのレビュー調整:
- 標準プラグインヘッダーチェックをスキップ(ドロップインはプラグインヘッダーを使用しない)
- アクティベーションフックチェックをスキップ(ドロップインは自動的に読み込まれる)
- テキストドメインチェックをスキップ(通常は内部、ユーザー向けではない)
- フォーカス: そのドロップインタイプに対する WordPress コア期待との互換性
ドロップイン検出パターン:
// ドロップインは wp-content/ ルートにあります、plugins/ ではありません
// ファイル名を既知のドロップイン名と照合します
$drop_ins = array( 'object-cache.php', 'db.php', 'advanced-cache.php', 'db-error.php', 'maintenance.php' );
クイックリファレンス: プラグインアーキテクチャパターン
懸念別に整理された一般的なプラグインパターン。すべての例は WordPress PHP コーディング標準を使用します(括弧内のスペース、[] ではなく array()、Yoda 条件)。
プラグインヘッダー
すべての推奨フィールドを備えた完全なヘッダーブロック:
❌ BAD: 最小限のヘッダー
<?php
/**
* Plugin Name: My Plugin
*/
✅ GOOD: すべての推奨フィールドを備えた完全なヘッダー
<?php
/**
* Plugin Name: My Awesome Plugin
* Plugin URI: https://example.com/my-awesome-plugin
* Description: Does amazing things with WordPress
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: John Doe
* Author URI: https://example.com
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-awesome-plugin
* Domain Path: /languages
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
プリフィックスとネームスペース
グローバルネームスペース汚染を避ける:
❌ BAD: プリフィックスなし(ネームスペース衝突リスク)
<?php
function save_settings() {
update_option( 'plugin_settings', $_POST['data'] );
}
class Plugin_Settings {
// ...
}
add_action( 'admin_init', 'save_settings' );
✅ GOOD: プリフィックス付きの関数とクラス
<?php
function mypl_save_settings() {
update_option( 'mypl_settings', $_POST['data'] );
}
class MyPL_Settings {
// ...
}
add_action( 'admin_init', 'mypl_save_settings' );
✅ BETTER: PHP ネームスペース使用
<?php
namespace MyPlugin\Admin;
function save_settings() {
update_option( 'mypl_settings', $_POST['data'] ); // Still prefix option names
}
class Settings {
// ...
}
add_action( 'admin_init', __NAMESPACE__ . '\save_settings' );
アクティベーション/ディアクティベーション/アンインストール ライフサイクル
適切なプラグインライフサイクル管理:
❌ BAD: ライフサイクルフックなし
<?php
// プラグインが init で CPT を登録しますが、書き換えをフラッシュしません
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* args */ ) );
}
✅ GOOD: アクティベーション/ディアクティベーション/アンインストールを備えた完全なライフサイクル
<?php
// メインプラグインファイル: my-plugin.php
// アクティベーションフック
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_activate() {
// デフォルトオプションを設定
add_option( 'mypl_version', '1.0.0' );
// フラッシュ前に CPT を登録
mypl_register_cpt();
// アクティベーション時のみ書き換えをフラッシュ
flush_rewrite_rules();
}
// ディアクティベーションフック
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
function mypl_deactivate() {
// トランジェントをクリア
delete_transient( 'mypl_cache' );
// 書き換えをフラッシュ
flush_rewrite_rules();
}
// 通常の動作用 Init フック
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* args */ ) );
}
// アンインストールファイル: uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// すべてのプラグインデータを削除
delete_option( 'mypl_version' );
delete_option( 'mypl_settings' );
// カスタムテーブルを削除
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}mypl_data" );
カスタム投稿タイプとタクソノミー
書き換えルール管理を使用した適切な CPT/タクソノミー登録:
❌ BAD: ページロードごとに書き換えをフラッシュ
<?php
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'book', array( // プリフィックスなし、show_in_rest なし
'public' => true,
) );
flush_rewrite_rules(); // CRITICAL: EVERY ページロードで実行
}
✅ GOOD: プリフィックス付き、Gutenberg 対応、アクティベーション時のみフラッシュ
<?php
add_action( 'init', 'mypl_register_cpt_and_taxonomy' );
function mypl_register_cpt_and_taxonomy() {
// カスタム投稿タイプを登録(最大 20 文字、プリフィックス付き)
register_post_type(
'mypl_book',
array(
'labels' => array(
'name' => __( 'Books', 'my-plugin' ),
'singular_name' => __( 'Book', 'my-plugin' ),
),
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'books' ),
'supports' => array( 'title', 'editor', 'thumbnail' ),
'show_in_rest' => true, // Gutenberg サポート
)
);
// カスタムタクソノミーを登録(最大 32 文字、プリフィックス付き)
register_taxonomy(
'mypl_genre',
'mypl_book',
array(
'labels' => array(
'name' => __( 'Genres', 'my-plugin' ),
'singular_name' => __( 'Genre', 'my-plugin' ),
),
'hierarchical' => true,
'public' => true,
'show_in_rest' => true, // Gutenberg サポート
)
);
}
// アクティベーション時のみ書き換えをフラッシュ
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_activate() {
mypl_register_cpt_and_taxonomy(); // 最初に登録
flush_rewrite_rules(); // その後フラッシュ(1 回のみ)
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
function mypl_deactivate() {
flush_rewrite_rules(); // 書き換えルールをクリーンアップ
}
Settings API
完全な Settings API ワークフロー:
❌ BAD: register_setting のない add_settings_field(保存されない)
<?php
add_action( 'admin_init', 'mypl_settings_init' );
function mypl_settings_init() {
add_settings_section( 'mypl_section', 'Settings', null, 'mypl-settings' );
add_settings_field(
'mypl_api_key',
'API Key',
'mypl_api_key_callback',
'mypl-settings',
'mypl_section'
);
// register_setting() がない - フィールドは保存されない!
}
✅ GOOD: 完全な Settings API ワークフロー
<?php
add_action( 'admin_init', 'mypl_register_settings' );
function mypl_register_settings() {
// 1. Setting を登録(オプション名とサニタイゼーションを宣言)
register_setting(
'mypl_options_group', // オプショングループ
'mypl_settings', // オプション名
array(
'type' => 'array',
'sanitize_callback' => 'mypl_sanitize_settings',
'default' => array(
'api_key' => '',
'enabled' => false,
),
)
);
// 2. Settings セクションを追加
add_settings_section(
'mypl_main_section',
__( 'Main Settings', 'my-plugin' ),
'mypl_section_callback',
'mypl-settings'
);
// 3. Settings フィールドを追加
add_settings_field(
'mypl_api_key',
__( 'API Key', 'my-plugin' ),
'mypl_api_key_callback',
'mypl-settings',
'mypl_main_section'
);
add_settings_field(
'mypl_enabled',
__( 'Enable Feature', 'my-plugin' ),
'mypl_enabled_callback',
'mypl-settings',
'mypl_main_section'
);
}
function mypl_section_callback() {
echo '<p>' . esc_html__( 'Configure your plugin settings below.', 'my-plugin' ) . '</p>';
}
function mypl_api_key_callback() {
$options = get_option( 'mypl_settings' );
$value = isset( $options['api_key'] ) ? $options['api_key'] : '';
?>
<input type="text"
name="mypl_settings[api_key]"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text">
<?php
}
function mypl_enabled_callback() {
$options = get_option( 'mypl_settings' );
$checked = isset( $options['enabled'] ) && $options['enabled'];
?>
<input type="checkbox"
name="mypl_settings[enabled]"
value="1"
<?php checked( $checked, true ); ?>>
<?php
}
function mypl_sanitize_settings( $input ) {
$sanitized = array();
if ( isset( $input['api_key'] ) ) {
$sanitized['api_key'] = sanitize_text_field( $input['api_key'] );
}
$sanitized['enabled'] = isset( $input['enabled'] ) && $input['enabled'] ? true : false;
return $sanitized;
}
// Settings ページ HTML
function mypl_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form method="post" action="options.php">
<?php
// Nonce、アクション、option_page フィールドを出力
settings_fields( 'mypl_options_group' );
// セクションとフィールドを出力
do_settings_sections( 'mypl-settings' );
submit_button();
?>
</form>
</div>
<?php
}
フックシステム
優先度管理を使用したアクションとフィルター:
❌ BAD: 戻り値のないフィルター(致命的エラー)
<?php
add_filter( 'the_content', 'mypl_modify_content' );
function mypl_modify_content( $content ) {
if ( is_single() ) {
$content .= '<p>Footer text</p>';
}
// 戻り値がない!致命的エラー。
}
✅ GOOD: 優先度と戻り値を備えた完全なフック使用
<?php
// 優先度付きアクション(コア前の優先度 10 で実行)
add_action( 'init', 'mypl_register_cpt', 9 );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* args */ ) );
}
// 戻り値を持つフィルター(CRITICAL)
add_filter( 'the_content', 'mypl_modify_content', 10, 1 );
function mypl_modify_content( $content ) {
if ( is_single() ) {
$content .= '<p>Footer text</p>';
}
return $content; // フィルターで常に返す
}
// 複数のパラメータと accepted_args
add_filter( 'wp_mail', 'mypl_modify_email', 10, 1 );
function mypl_modify_email( $args ) {
$args['from'] = 'Custom <custom@example.com>';
return $args;
}
// フック削除(正確な登録パラメータと一致する必要がある)
add_action( 'wp_footer', 'mypl_footer_code', 15 );
// 削除するには: 優先度が一致する必要がある
remove_action( 'wp_footer', 'mypl_footer_code', 15 );
// 拡張性のためのカスタムフックを作成
function mypl_process_data( $data ) {
// 他のプラグインがデータを変更できる
$data = apply_filters( 'mypl_before_process', $data );
// データを処理
$result = process( $data );
// 他のプラグインが処理後にフックできる
do_action( 'mypl_after_process', $result );
return $result;
}
国際化
テキストドメイン一致とプレースホルダー使用:
❌ BAD: 翻訳可能な文字列に埋め込まれた変数
<?php
$count = 5;
echo __( "You have $count messages", 'my-plugin' ); // 翻訳抽出を破壊
// テキストドメイン欠落
echo __( 'Save Settings' );
// テキストドメイン不一致
// プラグインフォルダ: my-awesome-plugin
echo __( 'Save', 'my_plugin' ); // アンダースコアではなくダッシュ
✅ GOOD: sprintf を使用したプレースホルダー、正しいテキストドメイン
<?php
// テキストドメイン付き基本翻訳
echo __( 'Settings saved successfully', 'my-awesome-plugin' );
// echo での翻訳
_e( 'Click here to continue', 'my-awesome-plugin' );
// sprintf を使用したプレースホルダー(埋め込み変数ではない)
$count = 5;
echo sprintf(
__( 'You have %d messages', 'my-awesome-plugin' ),
$count
);
// _n() を使用した複数形化
echo sprintf(
_n(
'One post found',
'%d posts found',
$count,
'my-awesome-plugin'
),
number_format_i18n( $count )
);
// _x() を使用したコンテキスト対応翻訳
echo _x( 'Post', 'noun - blog post', 'my-awesome-plugin' );
echo _x( 'Post', 'verb - submit', 'my-awesome-plugin' );
// エスケープされた出力 + 翻訳
echo '<h1>' . esc_html__( 'Welcome', 'my-awesome-plugin' ) . '</h1>';
echo '<input placeholder="' . esc_attr__( 'Enter your name', 'my-awesome-plugin' ) . '">';
// init でテキストドメインを読み込む(WP 4.6 以降ではオプション)
add_action( 'init', 'mypl_load_textdomain' );
function mypl_load_textdomain() {
load_plugin_textdomain(
'my-awesome-plugin',
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages'
);
}
REST API 登録(表面レベル)
❌ BAD: permission_callback がない(WordPress 5.5 以降のエラー)
<?php
add_action( 'rest_api_init', 'mypl_register_routes' );
function mypl_register_routes() {
register_rest_route( 'mypl/v1', '/posts', array(
'methods' => 'GET',
'callback' => 'mypl_get_posts',
// 欠落: 'permission_callback'(WordPress 5.5 以降でブロック)
) );
}
✅ GOOD: permission_callback と検証を備えた完全な REST エンドポイント
<?php
add_action( 'rest_api_init', 'mypl_register_routes' );
function mypl_register_routes() {
// パブリック読み取り専用エンドポイント
register_rest_route(
'mypl/v1', // バージョン付きネームスペース
'/posts',
array(
'methods' => 'GET',
'callback' => 'mypl_get_posts',
'permission_callback' => '__return_true', // 明示的にパブリック(読み取り専用の場合は OK)
'args' => array(
'per_page' => array(
'default' => 10,
'sanitize_callback' => 'absint',
'validate_callback' => function( $param ) {
return is_numeric( $param ) && $param > 0 && $param <= 100;
},
),
),
)
);
// 保護された書き込みエンドポイント
register_rest_route(
'mypl/v1',
'/posts/(?P<id>\d+)',
array(
'methods' => 'DELETE',
'callback' => 'mypl_delete_post',
'permission_callback' => function() {
return current_user_can( 'delete_posts' );
},
'args' => array(
'id' => array(
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
),
),
)
);
}
function mypl_get_posts( $request ) {
$per_page = $request->get_param( 'per_page' );
$posts = get_posts( array( 'posts_per_page' => $per_page ) );
return rest_ensure_response( $posts );
}
function mypl_delete_post( $request ) {
$post_id = $request->get_param( 'id' );
$result = wp_delete_post( $post_id, true );
if ( ! $result ) {
return new WP_Error(
'delete_failed',
__( 'Failed to delete post', 'my-plugin' ),
array( 'status' => 500 )
);
}
return rest_ensure_response( array( 'deleted' => true, 'id' => $post_id ) );
}
注: REST エンドポイント上のセキュリティの深掘り(nonce 検証、ケーパビリティチェック、入力サニタイゼーション)については、wp-security-review スキルを参照してください。
AJAX ハンドラ(表面レベル)
✅ GOOD: AJAX ハンドラ登録
<?php
// 認証されたユーザー
add_action( 'wp_ajax_mypl_save_data', 'mypl_save_data_callback' );
// パブリック(認証なし)
add_action( 'wp_ajax_nopriv_mypl_save_data', 'mypl_save_data_callback' );
function mypl_save_data_callback() {
// セキュリティチェック(詳細は wp-security-review 参照)
check_ajax_referer( 'mypl_nonce', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( 'Insufficient permissions' );
}
$data = sanitize_text_field( $_POST['data'] );
// データを処理
update_option( 'mypl_data', $data );
wp_send_json_success( 'Data saved' );
}
条件付きロード(PLG-20)
公開コードから管理者コードを分離:
❌ BAD: すべてのページで管理者コードが読み込まれている
<?php
add_action( 'wp_enqueue_scripts', 'mypl_enqueue_admin_assets' );
function mypl_enqueue_admin_assets() {
// 公開ページで管理者アセットが読み込まれている(不要)
wp_enqueue_script( 'mypl-admin', plugin_dir_url( __FILE__ ) . 'admin.js' );
}
✅ GOOD: 条件付き管理者ロード
<?php
// 管理コンテキストでのみ管理者コードをロード
if ( is_admin() ) {
require_once plugin_dir_path( __FILE__ ) . 'admin/class-admin.php';
}
// 管理者固有のアセット
add_action( 'admin_enqueue_scripts', 'mypl_enqueue_admin_assets' );
function mypl_enqueue_admin_assets() {
wp
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- jorgerosal
- ライセンス
- MIT
- 最終更新
- 2026/4/17
Source: https://github.com/jorgerosal/wordpress-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。