mcpField
W
wp-plugin-development

wp-plugin-development

github.com/jorgerosal開発11361 回インストール21 週間前に更新
リポジトリ

wp-plugin-development について

WordPress プラグイン開発者向けのアーキテクチャレビュー用スキルです。コード構成や WordPress.org への提出準備を評価し、ライフサイクルフック、設定、拡張性、命名、国際化を確認します。セキュリティ特化のレビューは別スキルに振り分けます。

主な機能

  1. 01プラグインのメタデータと構成の確認
  2. 02ライフサイクルフックとクリーンアップの検査
  3. 03設定とフック利用の確認
  4. 04命名と国際化の評価
  5. 05ファイル別・重大度別の指摘整理

活用シーン

  1. 01WordPress.org審査に向けた準備
  2. 02有効化とアンインストール処理の監査
  3. 03カスタム投稿タイプとフックの確認
  4. 04プラグイン国際化の点検

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jorgerosal/wordpress-skills --skill wp-plugin-development

インストール後、Claude Code で/wp-plugin-developmentを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
api-patterns.md
architecture-patterns.md
hooks-guide.md
oop-patterns.md

詳細

WordPress プラグイン開発レビューのスキル

概要

WordPress 6.x 以降を対象とする WordPress プラグインの体系的なアーキテクチャレビューです。中核となる原則: 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つのステップに従ってください。

  1. プラグインの種類とコンテキストを特定する

    • WordPress.org への申請 → 最も厳格な基準を適用します(Plugin Check への準拠)
    • プライベート/エンタープライズ向けプラグイン → 命名の自由度が高く、readme.txt は不要です
    • Must-use プラグイン(mu-plugin) → 読み込み方法が異なります(有効化フックは不要です)
    • ドロップインプラグイン → 特別なファイル(object-cache.php、db.php)を使用し、異なるパターンに従います
  2. プラグインヘッダーの完全性とメタデータを確認する

    • 必須:Plugin Name フィールドが存在すること
    • 推奨:Version、Description、Author、Text Domain、Requires at least、Requires PHP、License
    • Text Domain がプラグインスラッグと完全に一致していることを確認します(小文字、アンダースコアではなくハイフンを使用)
  3. 重大なパターンをスキャンする(機能を損なう、または WordPress.org での却下につながるもの)

    • PHP ファイルに defined( 'ABSPATH' ) || exit; チェックがない
    • グローバルスコープにプレフィックスのない関数/クラスがある(名前空間の衝突リスク)
    • セットアップ/クリーンアップ用の有効化/無効化フックがない
    • ヘッダーと i18n 関数呼び出しの間で Text Domain が一致していない
    • アンインストール時のクリーンアップがない(uninstall.php または register_uninstall_hook がない)
    • flush_rewrite_rules() が init フックで呼び出されている(ページ読み込みごとに高コストな処理が発生します)
    • dbDelta() ではなく $wpdb->query() でデータベーステーブルを直接作成している
    • フィルターコールバックに return 文がない
    • permission_callback のない REST API ルートがある(WordPress 5.5 以降の要件)
    • 投稿タイプのキーが20文字を超えている
    • タクソノミー名が32文字を超えている
  4. 警告対象のパターンを確認する(標準的ではないものの、機能するもの)

    • ユーザーに表示される文字列が国際化(i18n)されていない
    • __() または _e() の呼び出しに Text Domain パラメーターがない
    • WordPress のパス(/wp-content/plugins/)がハードコードされている
    • register_setting() に sanitize_callback がない
    • Gutenberg との互換性のための show_in_rest がない
    • フロントエンドでも管理者向けコードが読み込まれている(is_admin() チェックがない)
  5. 情報としての改善点を記録する(ベストプラクティスと最適化)

    • プレフィックスの代わりに PHP 名前空間を使用できます
    • プラグインヘッダーにバージョン要件がない(Requires at least、Requires PHP)
    • ブロックエディターをサポートするために show_in_rest を追加できます
    • 管理者向けアセットが全ページで読み込まれている(条件付きで読み込むべきです)
    • ヘッダーに Domain Path がない
  6. コンテキストに応じて重大度を調整して報告する

    • WordPress.org の場合:最も厳格に適用します(Plugin Check の失敗は重大)
    • プライベートプラグインの場合:より柔軟に対応します(プレフィックスは引き続き重要ですが、readme.txt は任意です)
    • Must-use プラグインの場合:想定を調整します(有効化フックは該当しません)
    • 以下の出力形式を使用して報告します
    • セキュリティ上の懸念が見つかった場合は、次の注記を追加します:"セキュリティ上の問題が検出されました。包括的なセキュリティ分析を行うには、/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:ファイルの先頭に defined( 'ABSPATH' ) || exit; がない → ファイルに直接アクセスできる可能性があります
  • 検出パターン:最初の 10 行に 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:uninstall.php に WP_UNINSTALL_PLUGIN チェックがない → 直接実行される可能性があります
  • パターン: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 がない → ブロックエディターからアクセスできません
  • WARNING:flush_rewrite_rules() が init で呼び出されている → パフォーマンスに問題があります(ページの読み込みごとにコストの高い DB 書き込みが発生します)
  • パターン:init にフックされた register_post_type( 'key', array( ... ) )
  • ベストプラクティス:投稿タイプキーは最大 20 文字で、小文字の英数字とハイフン/アンダースコアのみを使用します

リライトルールのフラッシュのタイミング(PLG-06):

  • CRITICAL:init フックでの flush_rewrite_rules() → ページの読み込みごとに DB 書き込みが発生します
  • 正しいパターン:投稿タイプの登録後、有効化フック内でのみ呼び出します
タクソノミーファイル

register_taxonomy() の使用(PLG-07):

  • CRITICAL:タクソノミー名が 32 文字を超えている → データベースエラーが発生します
  • CRITICAL:タクソノミー名にプレフィックスがない → コアタクソノミーや他のプラグインと衝突します
  • WARNING:show_in_rest => true がない → ブロックエディターからアクセスできません
  • パターン:init にフックされた register_taxonomy( 'name', 'post_type', array( ... ) )
  • ベストプラクティス:タクソノミー名は最大 32 文字で、小文字とアンダースコアのみを使用します
設定 API ファイル

register_setting() の使用方法(PLG-08):

  • 重大:対応する register_setting() がない add_settings_field() → フィールドが保存されません
  • 警告:sanitize_callback がない register_setting() → サニタイズされていないデータが保存されます
  • 警告:type パラメーターがない register_setting() → 型の推論が不確実になります
  • パターン:admin_init での register_setting( 'option_group', 'option_name', array( ... ) )

Settings API のワークフロー(PLG-09):

  • 警告:add_settings_section() のページスラッグが do_settings_sections() の呼び出しと一致しない → セクションが表示されません
  • 警告:add_settings_field() のセクション ID が add_settings_section() の ID と一致しない → フィールドが表示されません
  • 警告:フォームに settings_fields() の呼び出しがない → nonce による保護がなく、オプションが保存されません
  • 警告:フォームに do_settings_sections() の呼び出しがない → フィールドが表示されません
  • パターン:ワークフロー全体には register_setting() → add_settings_section() → add_settings_field() → settings_fields() と do_settings_sections() を含むフォーム HTML が必要です
フック使用ファイル

add_action/add_filter のパターン(PLG-10):

  • 警告:実行順序が重要なのにフックの優先度が指定されていない → 順序が正しくない状態で実行される可能性があります
  • 警告:コールバックが複数のパラメーターを必要とするのに accepted_args が指定されていない → パラメーターが切り詰められます
  • 情報:明確にするため、優先度を明示的に指定できます(デフォルトの 10 を使用する場合も同様です)
  • パターン:add_action( 'hook_name', 'callback', $priority, $accepted_args )

フィルターの戻り値の要件(PLG-10):

  • 重大:フィルターのコールバックが値を返さない → フィルターチェーンが壊れ、致命的なエラーが発生します
  • パターン:戻り値のステートメントがないコールバックを使った add_filter( 'hook', 'callback' )
  • 確認事項:すべてのフィルターコールバックは値を返す必要があります

フックの削除(PLG-10):

  • 警告:remove_action() または remove_filter() のパラメーターが登録時と一致しない → フックが削除されません
  • パターン:優先度と accepted_args は、元の add_action()/add_filter() の呼び出しと完全に一致させる必要があります
  • 例:add_action( 'init', 'callback', 15 ) には remove_action( 'init', 'callback', 15 ) が必要です

拡張性のためのカスタムフック(PLG-10):

  • 情報:プラグインでは、拡張性のために do_action() または apply_filters() を使ったカスタムフックを提供できます
  • パターン:do_action( 'myplugin_after_save', $data ) または apply_filters( 'myplugin_modify_output', $output )
国際化(i18n)ファイル

テキストドメインの一致(PLG-11):

  • 重大:プラグインヘッダーのテキストドメインが i18n 呼び出しのテキストドメインと一致していません → 翻訳の読み込みに失敗します
  • 重大:テキストドメインにハイフンではなくアンダースコアが使用されています → WordPress.org によって却下されます
  • 重大:テキストドメインがプラグインのスラッグと一致していません → Plugin Check に失敗します
  • パターン:Text Domain: my-plugin はプラグインのフォルダー名/ファイル名と完全に一致させる必要があります

i18n 関数の使用(PLG-11):

  • 警告:テキストドメインのパラメーターなしで __() または _e() を使用しています → コアの翻訳にフォールバックします(不適切です)
  • 警告:翻訳対象の文字列に変数が埋め込まれています → 翻訳文字列の抽出に失敗します
  • パターン:必ず __( 'Text', 'text-domain' ) を使用し、__( 'Text' ) は使用しないでください
  • パターン:__( "Text $var", 'domain' ) ではなく、sprintf( __( 'Text %s', 'domain' ), $var ) を使用してください

複数形(PLG-11):

  • 警告:複数形の処理に _n() ではなく if/else を使用しています → 複雑な複数形のルールがある言語で正しく動作しません
  • パターン:数に応じて変化する文字列には _n( 'singular', 'plural', $count, 'domain' ) を使用してください

文脈を考慮した翻訳(PLG-11):

  • 情報:同じ英単語に異なる訳語がある場合、曖昧さを解消するために _x() を使用できます
  • パターン:_x( 'Post', 'noun', 'domain' ) と _x( 'Post', 'verb', 'domain' ) を使い分けます

エスケープ付き翻訳バリエーション(PLG-11):

  • 情報:翻訳とエスケープをまとめて行うには、esc_html__() または esc_attr__() を使用してください
  • パターン:echo esc_html( __( 'Text', 'domain' ) ) ではなく、echo esc_html__( 'Text', 'domain' ) を使用してください

翻訳の読み込み(PLG-11):

  • 情報:WordPress 4.6 以降では load_plugin_textdomain() は任意です(translate.wordpress.org から自動的に読み込まれます)
  • パターン:呼び出す場合は init にフックします:add_action( 'init', 'load_textdomain_callback' )
REST API エンドポイント(PLG-13、表面的な確認)

register_rest_route() の使用:

  • 重大:permission_callback がありません → WordPress 5.5 以降ではエラーとなり、エンドポイントがブロックされます
  • 重大:書き込み操作で permission_callback => '__return_true' を使用しています → 認証されていないアクセスが許可されます
  • パターン:rest_api_init フックで register_rest_route( 'namespace/v1', '/route', array( ... ) ) を使用します
  • 注:公開読み取り専用エンドポイントでは __return_true が正しい設定です

名前空間とバージョン管理:

  • 警告:名前空間にバージョンがありません → API を進化させるための道筋がありません
  • パターン:myplugin だけでなく、myplugin/v1 を使用してください

パラメーターの検証:

  • 警告:引数に sanitize_callback がありません → 入力がサニタイズされません
  • 警告:引数に validate_callback がありません → 無効な入力が受け入れられます
  • セキュリティの詳細については、wp-security-review skill を参照してください
AJAX ハンドラー(PLG-14、表面的な確認)

フックの登録:

  • 警告:頻繁に呼び出される処理に 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 skill を参照してください
管理メニュー(PLG-15、表面的な確認)

add_menu_page/add_submenu_page:

  • 警告:権限パラメーターがありません → アクセス制御がありません
  • 警告:ページコールバックで current_user_can() を検証していません → 権限パラメーターだけでは不十分です
  • パターン:add_menu_page( $title, $menu_title, 'manage_options', $slug, $callback ) を使用します
データベーステーブル(PLG-16、表面的な確認)

テーブルの作成:

  • 重大:dbDelta() ではなく $wpdb->query( "CREATE TABLE..." ) を使用しています → アップグレード手段がなく、文字セットの問題が発生します
  • パターン:require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta( $sql ); を使用します
  • パターン:テーブル名にはハードコードした wp_ ではなく、$wpdb->prefix を使用します
  • パターン:文字セット/照合順序には $wpdb->get_charset_collate() を使用します

dbDelta の SQL 書式要件:

  • 警告:SQL が dbDelta 用に書式設定されていません → テーブルの作成が通知なく失敗します
  • 要件: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' ) を実行します
Transient(PLG-18、表層的な確認)

set_transient() の使用:

  • WARNING:Transient キーにプレフィックスが付いていない → 他のプラグインとキーが衝突します
  • WARNING:有効期限のない Transient → 永続的なオプションのように動作します
  • パターン:set_transient( 'mypl_key', $value, HOUR_IN_SECONDS )

すばやく検出するための検索パターン(PLG-21)

プラグインをすばやくスキャンするには、以下の rg コマンドを使用します。重大度別に整理されています。

CRITICAL パターン
# Main plugin file should contain "Plugin Name" header
# If this returns nothing, the plugin likely lacks a valid main header.
rg -n "Plugin Name:" . -g '*.php'

# PHP files missing ABSPATH check (direct file access possible)
rg -L --iglob '*.php' "defined\s*\(\s*['\"]ABSPATH['\"]\s*\)|defined\s*\(\s*'ABSPATH'\s*\)" .

# Unprefixed function declarations in global scope (namespace collision risk)
rg -n "^function\s+[a-z_][a-z0-9_]*\s*\(" . -g '*.php'

# Text domain mismatch (compare header vs i18n calls)
# First: inspect the main plugin file header for the expected text domain
rg -n "Text Domain:" . -g '*.php'
# Then: compare that slug with translation calls across the plugin
rg -n "__\(|_e\(|_n\(|_x\(" . -g '*.php'

# flush_rewrite_rules outside activation/deactivation (expensive operation on every page load)
rg -n "flush_rewrite_rules" . -g '*.php'

# Filter callbacks missing return statement (manual follow-up required)
rg -n "add_filter\s*\(" . -g '*.php'

# register_rest_route without permission_callback (manual per-route review)
rg -n "register_rest_route\s*\(" . -g '*.php'
WARNING パターン
# Missing register_activation_hook (no setup mechanism)
rg -n "register_activation_hook\s*\(" . -g '*.php'

# Missing uninstall cleanup (no uninstall.php and no register_uninstall_hook)
test -f uninstall.php || rg -n "register_uninstall_hook\s*\(" . -g '*.php'

# i18n functions without obvious text domain parameter (manual confirmation required)
rg -n "__\(|_e\(" . -g '*.php'

# Hardcoded /wp-content/plugins/ paths (breaks on custom WordPress installations)
rg -n "/wp-content/plugins/" . -g '*.php'

# Direct CREATE TABLE with $wpdb->query (should use dbDelta)
rg -n "wpdb->query\s*\(.*CREATE TABLE" . -g '*.php'

# Post type key length check (max 20 characters)
rg -n "register_post_type\s*\(" . -g '*.php'

# Missing show_in_rest in register_post_type (manual per-registration review)
rg -n "register_post_type\s*\(" . -g '*.php'
INFO パターン
# Missing "Requires at least" or "Requires PHP" in plugin header
rg -n "Requires at least:|Requires PHP:" . -g '*.php'

# Functions not using PHP namespaces (could modernize)
rg -n "^function\s+myprefix_" . -g '*.php'

# Admin code loading on frontend (manual context check)
rg -n "add_action\s*\(\s*['\"]admin_" . -g '*.php'

# Missing Domain Path in plugin header
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 またはヘルパー関数を使用)
  • グローバルスコープのすべての関数/クラス/オプションに適切なプレフィックスを付けること

確認方法: 提出前に Plugin Check ツールを実行します。

# Via WP-CLI
wp plugin install plugin-check --activate
wp plugin check my-plugin.php

# Or via WordPress Admin
# Navigate to: Plugins > Add New > Search "Plugin Check"
# Then: 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 を参照)
Must-Use プラグイン(mu-plugins)のコンテキスト

読み込み動作が異なります。 Must-Use プラグインは、通常のプラグインより先に自動的に読み込まれます。

主な違い:

  • wp-content/mu-plugins/ ディレクトリからアルファベット順に読み込まれます
  • 通常のプラグインより先に読み込まれます
  • 管理画面から無効化できません
  • 有効化/無効化フックはありません(有効化/無効化されるのではなく、読み込まれるだけです)
  • WordPress 管理画面から更新できません(ファイルを手動で置き換えます)

mu-plugins のレビューでの調整:

  • WARNING(CRITICAL ではありません):register_activation_hook() がない → mu-plugins では想定どおりです
  • WARNING(CRITICAL ではありません):register_deactivation_hook() がない → mu-plugins では想定どおりです
  • 引き続き必要:適切なプレフィックス、ABSPATH チェック、i18n、セキュリティ対策

mu-plugin のコンテキストを検出するパターン:

// Check if plugin is loaded as must-use
if ( strpos( __FILE__, WPMU_PLUGIN_DIR ) !== false ) {
    // Running as must-use plugin - skip activation hooks
}
ドロップインプラグインのコンテキスト

特定の名前を持つ特殊なファイルです。 ドロップインは WordPress のコア機能を置き換えます。

一般的なドロップイン:

  • object-cache.php → オブジェクトキャッシュバックエンド(Redis、Memcached)
  • db.php → カスタムデータベースクラス
  • advanced-cache.php → ページキャッシュシステム
  • db-error.php → カスタムデータベースエラーページ
  • maintenance.php → カスタムメンテナンスモードページ

ドロップインのレビューで調整する点:

  • 標準のプラグインヘッダーチェックをスキップします(ドロップインはプラグインヘッダーを使用しません)
  • 有効化フックのチェックをスキップします(ドロップインは自動的に読み込まれます)
  • テキストドメインのチェックをスキップします(通常は内部向けで、ユーザーには表示されません)
  • 次の点に注目します:ドロップインの種類に応じた WordPress コアの想定との互換性

ドロップインを検出するパターン:

// Drop-ins are in wp-content/ root, not plugins/
// Check filename against known drop-in names
$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
// Plugin registers CPT on init but never flushes rewrites
add_action( 'init', 'mypl_register_cpt' );

function mypl_register_cpt() {
    register_post_type( 'mypl_book', array( /* args */ ) );
}

✅ GOOD:有効化/無効化/アンインストールを含む完全なライフサイクル

<?php
// Main plugin file: my-plugin.php

// Activation hook
register_activation_hook( __FILE__, 'mypl_activate' );

function mypl_activate() {
    // Set default options
    add_option( 'mypl_version', '1.0.0' );

    // Register CPT before flushing
    mypl_register_cpt();

    // Flush rewrite rules ONLY on activation
    flush_rewrite_rules();
}

// Deactivation hook
register_deactivation_hook( __FILE__, 'mypl_deactivate' );

function mypl_deactivate() {
    // Clear transients
    delete_transient( 'mypl_cache' );

    // Flush rewrite rules
    flush_rewrite_rules();
}

// Init hook for normal operation
add_action( 'init', 'mypl_register_cpt' );

function mypl_register_cpt() {
    register_post_type( 'mypl_book', array( /* args */ ) );
}

// Uninstall file: uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

// Delete all plugin data
delete_option( 'mypl_version' );
delete_option( 'mypl_settings' );

// Drop custom table
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( // Missing prefix, no show_in_rest
        'public' => true,
    ) );

    flush_rewrite_rules(); // CRITICAL: Runs on EVERY page load
}

✅ GOOD:プレフィックス付きで Gutenberg 対応、有効化時のみフラッシュ

<?php
add_action( 'init', 'mypl_register_cpt_and_taxonomy' );

function mypl_register_cpt_and_taxonomy() {
    // Register custom post type (max 20 chars, prefixed)
    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 support
        )
    );

    // Register custom taxonomy (max 32 chars, prefixed)
    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 support
        )
    );
}

// Flush rewrites ONLY on activation
register_activation_hook( __FILE__, 'mypl_activate' );

function mypl_activate() {
    mypl_register_cpt_and_taxonomy(); // Register first
    flush_rewrite_rules(); // Then flush (only once)
}

register_deactivation_hook( __FILE__, 'mypl_deactivate' );

function mypl_deactivate() {
    flush_rewrite_rules(); // Clean up 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'
    );
    // Missing register_setting() - field won't save!
}

✅ GOOD:Settings API の完全なワークフロー

<?php
add_action( 'admin_init', 'mypl_register_settings' );

function mypl_register_settings() {
    // 1. Register setting (declares option name and sanitization)
    register_setting(
        'mypl_options_group',    // Option group
        'mypl_settings',          // Option name
        array(
            'type'              => 'array',
            'sanitize_callback' => 'mypl_sanitize_settings',
            'default'           => array(
                'api_key' => '',
                'enabled' => false,
            ),
        )
    );

    // 2. Add settings section
    add_settings_section(
        'mypl_main_section',
        __( 'Main Settings', 'my-plugin' ),
        'mypl_section_callback',
        'mypl-settings'
    );

    // 3. Add settings fields
    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 page 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
            // Output nonce, action, option_page fields
            settings_fields( 'mypl_options_group' );

            // Output sections and fields
            do_settings_sections( 'mypl-settings' );

            submit_button();
            ?>
        </form>
    </div>
    <?php
}
フックシステム

優先度を管理したアクションとフィルター:

❌ BAD:return 文のないフィルター(致命的なエラー)

<?php
add_filter( 'the_content', 'mypl_modify_content' );

function mypl_modify_content( $content ) {
    if ( is_single() ) {
        $content .= '<p>Footer text</p>';
    }
    // Missing return! Fatal error.
}

✅ GOOD:優先度と return を含む完全なフックの使用方法

<?php
// Action with priority (runs before core at priority 10)
add_action( 'init', 'mypl_register_cpt', 9 );

function mypl_register_cpt() {
    register_post_type( 'mypl_book', array( /* args */ ) );
}

// Filter with return (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; // ALWAYS return in filters
}

// Multiple parameters with accepted_args
add_filter( 'wp_mail', 'mypl_modify_email', 10, 1 );

function mypl_modify_email( $args ) {
    $args['from'] = 'Custom <[email protected]>';
    return $args;
}

// Removing hooks (MUST match exact registration params)
add_action( 'wp_footer', 'mypl_footer_code', 15 );

// To remove: priority must match
remove_action( 'wp_footer', 'mypl_footer_code', 15 );

// Create custom hooks for extensibility
function mypl_process_data( $data ) {
    // Allow other plugins to modify data
    $data = apply_filters( 'mypl_before_process', $data );

    // Process data
    $result = process( $data );

    // Allow other plugins to hook after processing
    do_action( 'mypl_after_process', $result );

    return $result;
}
国際化

テキストドメインの一致とプレースホルダーの使用:

❌ BAD:翻訳可能な文字列に変数を埋め込む

<?php
$count = 5;
echo __( "You have $count messages", 'my-plugin' ); // Breaks translation extraction

// Missing text domain
echo __( 'Save Settings' );

// Text domain mismatch
// Plugin folder: my-awesome-plugin
echo __( 'Save', 'my_plugin' ); // Underscore instead of dash

✅ GOOD:sprintf を使ったプレースホルダーと正しいテキストドメイン

<?php
// Basic translation with text domain
echo __( 'Settings saved successfully', 'my-awesome-plugin' );

// Translation with echo
_e( 'Click here to continue', 'my-awesome-plugin' );

// Placeholders with sprintf (NOT embedded variables)
$count = 5;
echo sprintf(
    __( 'You have %d messages', 'my-awesome-plugin' ),
    $count
);

// Pluralization with _n()
echo sprintf(
    _n(
        'One post found',
        '%d posts found',
        $count,
        'my-awesome-plugin'
    ),
    number_format_i18n( $count )
);

// Context-aware translation with _x()
echo _x( 'Post', 'noun - blog post', 'my-awesome-plugin' );
echo _x( 'Post', 'verb - submit', 'my-awesome-plugin' );

// Escaped output + translation
echo '<h1>' . esc_html__( 'Welcome', 'my-awesome-plugin' ) . '</h1>';
echo '<input placeholder="' . esc_attr__( 'Enter your name', 'my-awesome-plugin' ) . '">';

// Load text domain on init (optional in 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',
        // Missing: 'permission_callback' (WordPress 5.5+ blocks this)
    ) );
}

✅ GOOD:permission_callback と検証を含む完全な REST エンドポイント

<?php
add_action( 'rest_api_init', 'mypl_register_routes' );

function mypl_register_routes() {
    // Public read-only endpoint
    register_rest_route(
        'mypl/v1', // Namespace with version
        '/posts',
        array(
            'methods'             => 'GET',
            'callback'            => 'mypl_get_posts',
            'permission_callback' => '__return_true', // Explicitly public (OK for read-only)
            'args'                => array(
                'per_page' => array(
                    'default'           => 10,
                    'sanitize_callback' => 'absint',
                    'validate_callback' => function( $param ) {
                        return is_numeric( $param ) && $param > 0 && $param <= 100;
                    },
                ),
            ),
        )
    );

    // Protected write endpoint
    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
// Authenticated users
add_action( 'wp_ajax_mypl_save_data', 'mypl_save_data_callback' );

// Public (non-authenticated)
add_action( 'wp_ajax_nopriv_mypl_save_data', 'mypl_save_data_callback' );

function mypl_save_data_callback() {
    // Security checks (see wp-security-review for depth)
    check_ajax_referer( 'mypl_nonce', 'nonce' );

    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_send_json_error( 'Insufficient permissions' );
    }

    $data = sanitize_text_field( $_POST['data'] );

    // Process 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() {
    // Admin assets loading on public pages (unnecessary)
    wp_enqueue_script( 'mypl-admin', plugin_dir_url( __FILE__ ) . 'admin.js' );
}

✅ GOOD:管理画面用コードの条件付き読み込み

<?php
// Only load admin code in admin context
if ( is_admin() ) {
    require_once plugin_dir_path( __FILE__ ) . 'admin/class-admin.php';
}

// Admin-specific assets
add_action( 'admin_enqueue_scripts', 'mypl_enqueue_admin_assets' );

function mypl_enqueue_admin_assets() {
    wp_enqueue_script(
        'mypl-admin',
        plugin_dir_url( __FILE__ ) . 'admin/admin.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );
}

// Public-facing assets
add_action( 'wp_enqueue_scripts', 'mypl_enqueue_public_assets' );

function mypl_enqueue_public_assets() {
    wp_enqueue_script(
        'mypl-public',
        plugin_dir_url( __FILE__ ) . 'public/public.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );
}
関数/クラスの存在チェック(PLG-19)

競合による致命的なエラーを防ぎます:

✅ GOOD:関数とクラスの存在チェック

<?php
// Function existence check
if ( ! function_exists( 'mypl_init' ) ) {
    function mypl_init() {
        // Plugin initialization
    }
}

// Class existence check
if ( ! class_exists( 'MyPL_Plugin' ) ) {
    class MyPL_Plugin {
        // Plugin class
    }
}

重大度の定義(PLG-23)

重大度定義例
CRITICALWordPress.org での審査に不合格となる、プラグインの機能が損なわれる、または致命的なエラーが発生しますPlugin Name ヘッダーがない、名前空間の衝突を招くプレフィックスなしのグローバル関数、テキストドメインの不一致、ABSPATH チェックがない、dbDelta を使わない直接的な DB テーブル作成、return 文のないフィルターコールバック、20 文字を超える投稿タイプキー、32 文字を超えるタクソノミー名、init フックでの flush_rewrite_rules()、permission_callback のない REST ルート(WP 5.5 以降)
WARNING保守性または互換性の問題につながる標準的でないパターンですが、プラグイン自体は機能します国際化(i18n)がない、テキストドメインのない __()、ハードコードされた /wp-content/ パス、register_setting() に sanitize_callback がない、Gutenberg サポートに show_in_rest がない、既存のスケジュールを確認せずに wp_schedule_event() を使用している、有効期限のないトランジェント
INFOベストプラクティスの改善と近代化の機会ですプレフィックス付けの代わりに PHP 名前空間を使用できる、ヘッダーにバージョン要件(Requires at least、Requires PHP)がない、フロントエンドで管理画面用コードを読み込んでいる、ヘッダーに Domain Path がない、コンテキストに応じた翻訳に _x() を使用できる

出力形式(PLG-23)

ファイルごとにまとめ、行番号と重大度ラベルを付けて指摘事項を報告してください。各指摘事項には BAD/GOOD のコードペアを使用してください。

# WordPress Plugin Review: my-awesome-plugin

## FILE: my-awesome-plugin.php

### Line 15: CRITICAL - Missing ABSPATH check
Direct file access possible. Add ABSPATH check at top of all PHP files.

❌ BAD:
<?php
/**
 * Plugin Name: My Plugin
 */

function mypl_init() { ... }

✅ GOOD:
<?php
/**
 * Plugin Name: My Plugin
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

function mypl_init() { ... }

### Line 45: WARNING - Missing text domain in i18n call
Translation won't work. Always include text domain parameter.

❌ BAD:
echo __( 'Save Settings' );

✅ GOOD:
echo __( 'Save Settings', 'my-awesome-plugin' );

## FILE: includes/class-cpt.php

### Line 23: CRITICAL - flush_rewrite_rules on init hook
Expensive DB write on every page load. Move to activation hook.

❌ BAD:
add_action( 'init', 'mypl_register_cpt' );

function mypl_register_cpt() {
    register_post_type( 'book', array( /* ... */ ) );
    flush_rewrite_rules(); // Runs on EVERY request
}

✅ GOOD:
add_action( 'init', 'mypl_register_cpt' );

function mypl_register_cpt() {
    register_post_type( 'mypl_book', array( /* ... */ ) );
}

register_activation_hook( __FILE__, 'mypl_activate' );

function mypl_activate() {
    mypl_register_cpt();
    flush_rewrite_rules(); // Only on activation
}

## SUMMARY

**Total issues: 15**
- CRITICAL: 4 (must fix for WordPress.org submission)
- WARNING: 8 (non-standard patterns)
- INFO: 3 (best practice improvements)

**WordPress.org readiness:** NOT READY - 4 critical issues block submission

**Security note:** This review focused on plugin architecture and WordPress.org compliance. Security issues detected (missing nonce in AJAX handler, unsanitized input in form). Run `/wp-sec-review` for comprehensive security analysis.

よくある間違い(PLG-24)

問題のように見えても、問題ではないパターン:

パターン問題ではない理由コンテキスト
有効化フックのない mu-pluginMust-use プラグインは自動的に読み込まれ、有効化・無効化されませんwp-content/mu-plugins/ 内の mu-plugin では通常のことです
標準ヘッダーのないドロップインプラグインドロップインではプラグインヘッダーではなく、特別なファイル名を使用しますobject-cache.php、db.php、advanced-cache.php などでは有効です
readme.txt のない非公開プラグインreadme.txt が必要なのは WordPress.org への提出時のみです非公開/エンタープライズ向けプラグインでは内部ドキュメントを使用します
GET 用 REST permission_callback の __return_true公開読み取り専用エンドポイントでは認証は不要です公開 REST API の読み取りでは正しいパターンです
型を変更する Settings API の sanitize_callbackチェックボックスの文字列をブール値に、テキストを配列に変換できますバグではなく、有効な変換です
WordPress コア文字列にテキストドメインがないコア文字列はすでに翻訳されていますwp_die() に文字列を渡す場合や、コア文字列を使った __() では有効です
名前空間を使った短いプレフィックス(3 文字)PHP 名前空間によって衝突が防止されますnamespace MyPlugin\Feature; を使用する場合は許容されます
セットアップ処理のないプラグインに有効化フックがないすべてのプラグインがデータベーステーブル、オプション、または rewrite のフラッシュを必要とするわけではありませんシンプルなプラグイン(ショートコード、フィルターのみ)では有効です
管理画面の設定保存時に flush_rewrite_rules() を実行するユーザーが明示的にパーマリンク構造を変更する場合は許容されますスラッグ/rewrite が変更される設定ページでは有効です
DROP TABLE に直接 $wpdb->query() を使用するdbDelta() は CREATE/ALTER 用であり、DROP 用ではありませんuninstall.php では正しいパターンです
内部デバッグ文字列に i18n がない開発者向けの文字列は翻訳する必要がありませんerror_log() や内部コメントでは有効です
show_in_rest なしで register_taxonomy() を使用するすべてのタクソノミーで Gutenberg UI が必要なわけではありません管理画面専用のタクソノミーでは有効です

WordPress.org 準拠チェックリスト(PLG-12)

WordPress.org への提出前に、Plugin Check の基準を確認するための簡易チェックリストです。

  • メインのプラグインファイルに Plugin Name ヘッダーがある
  • Text Domain がプラグインのスラッグと完全に一致している(小文字で、アンダースコアではなくハイフンを使用)
  • すべての PHP ファイルで ABSPATH チェックが行われている(defined( 'ABSPATH' ) || exit;)
  • すべての i18n 関数に text domain パラメーターが含まれている
  • 非推奨の WordPress 関数を使用していない
  • ライセンスヘッダーがある(GPLv2 以降を推奨)
  • 適切な形式の readme.txt(または readme.md)がある
  • /wp-content/ または /wp-content/plugins/ のパスをハードコードしていない
  • グローバルスコープのすべての関数/クラス/オプション/トランジェントに適切なプレフィックスを付けている
  • クリーンアップ用の uninstall.php または register_uninstall_hook がある
  • eval()、base64_decode()、または難読化されたコードを使用していない
  • 必要なバージョンを指定している(Requires at least、Requires PHP)

確認コマンド:

wp plugin install plugin-check --activate
wp plugin check my-plugin.php

詳細リファレンス

高度なプラグイン開発パターンについては、以下の関連リファレンスドキュメントを参照してください。

タスク読み込むリファレンス
プラグインのファイル構造、シングルトンパターン、オートローディング、依存性注入、メインプラグインクラスのアーキテクチャreferences/architecture-patterns.md
アクション/フィルターのライフサイクル、優先度システム、フックの削除、カスタムフック、プラガブル関数、フックの命名規則references/hooks-guide.md
クラスベースのプラグインアーキテクチャ、PHP 名前空間、PSR-4 オートローディング、トレイトの使用、抽象クラス、サービスコンテナーreferences/oop-patterns.md
Settings API のワークフロー、Options API のベストプラクティス、REST API のスキーマ検証、Transients API、dbDelta を使用したカスタムデータベーステーブルreferences/api-patterns.md

注: リファレンスドキュメントでは、詳細な内容を説明しています。この SKILL.md は、標準的なプラグインレビューに必要な情報を網羅しています。

セキュリティとの関連: セキュリティに関わるパターン(nonce のないフォームハンドラー、権限チェックのない AJAX、権限検証のない REST エンドポイントなど)が見つかった場合、このスキルでは簡単な注意事項を示し、包括的なセキュリティ分析は wp-security-review に委ねます。状態を変更するすべての操作に、3 段階のセキュリティパターン(nonce +権限確認+サニタイズ)を適用します。詳細なセキュリティ監査には、/wp-sec-review コマンドを使用してください。

コメント