mcpField
W
wordpress-plugin-core

wordpress-plugin-core

github.com/jackspace開発1639 回インストール12 週間前に更新
リポジトリ

wordpress-plugin-core について

wordpress-plugin-core は、WordPress プラグイン開発者向けのスキルで、構成パターン、実装方法、セキュリティ対策を扱います。権限、入出力、リクエスト検証、データベースクエリの処理を例とともに説明します。

主な機能

  1. 01シンプル、OOP、PSR-4 の構成を解説
  2. 02プラグインヘッダーとセキュリティ基礎
  3. 03権限チェックと nonce 検証を説明
  4. 04入力のサニタイズと出力のエスケープ
  5. 05安全なデータベースクエリの例
  6. 06よくある問題と予防策を整理

活用シーン

  1. 01WordPress プラグインのコード構成を計画
  2. 02フォームや AJAX リクエストを処理
  3. 03ユーザー入力とページ出力を保護
  4. 04ユーザー入力を含むデータベースクエリを作成

インストール

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

$npx skills add https://github.com/jackspace/claudeskillz --skill wordpress-plugin-core

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

同梱ファイルREADME.mdSKILL.json

詳細

WordPress プラグイン開発(コア)

ステータス:本番環境対応 最終更新日:2025-11-06 依存関係:なし(WordPress 5.9+、PHP 7.4+) 最新バージョン:WordPress 6.7+、PHP 8.0+ を推奨


クイックスタート(10 分)

1. プラグインの構成を選ぶ

WordPress プラグインでは、次の 3 つのアーキテクチャパターンを利用できます。

  • シンプル(関数のみ) - 関数が 5 個未満の小規模なプラグイン向け
  • OOP(オブジェクト指向) - 関連する機能を持つ中規模プラグイン向け
  • PSR-4(名前空間 + Composer オートロード) - 大規模または最新のプラグイン向け

重要な理由:

  • シンプルなプラグインは最も手軽に始められますが、拡張性に欠けます
  • OOP は最新の PHP 機能を使わずにコードを整理できます
  • PSR-4 は 2025 年時点での最新標準であり、最も保守しやすい方法です
2. プラグインヘッダーを作成する

すべてのプラグインは、メインファイルにヘッダーコメントを記述する必要があります。

<?php
/**
 * Plugin Name:       My Awesome Plugin
 * Plugin URI:        https://example.com/my-plugin/
 * Description:       Brief description of what this plugin does.
 * Version:           1.0.0
 * Requires at least: 5.9
 * Requires PHP:      7.4
 * Author:            Your Name
 * Author URI:        https://yoursite.com/
 * License:           GPL v2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       my-plugin
 * Domain Path:       /languages
 */

// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

重要:

  • 必須項目は Plugin Name のみです
  • Text Domain は翻訳のため、プラグインのスラッグと完全に一致させる必要があります
  • ファイルへの直接アクセスを防ぐため、必ず ABSPATH チェックを追加してください
3. セキュリティの基盤を実装する

機能を実装する前に、次の 5 つのセキュリティの基本を実装してください。

// 1. Unique Prefix (4-5 chars minimum)
define( 'MYPL_VERSION', '1.0.0' );

function mypl_init() {
    // Your code
}
add_action( 'init', 'mypl_init' );

// 2. ABSPATH Check (every PHP file)
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// 3. Nonces for Forms
<input type="hidden" name="mypl_nonce" value="<?php echo wp_create_nonce( 'mypl_action' ); ?>" />

// 4. Sanitize Input, Escape Output
$clean = sanitize_text_field( $_POST['input'] );
echo esc_html( $output );

// 5. Prepared Statements for Database
global $wpdb;
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}table WHERE id = %d",
        $id
    )
);

5 つのセキュリティの基本

WordPress プラグインのセキュリティには、必ずすべて備えるべき 3 つの要素があります。

ステップ 1:すべてに一意のプレフィックスを使う

理由:ほかのプラグインや WordPress コアとの名前の衝突を防ぎます。

ルール:

  • 最低 4~5 文字にします
  • 関数、クラス、定数、オプション、一時データ、メタキー、グローバル変数に適用します
  • 次のものは避けてください:wp_、__、_、"WordPress"
// GOOD
function mypl_function_name() {}
class MyPL_Class_Name {}
define( 'MYPL_CONSTANT', 'value' );
add_option( 'mypl_option', 'value' );
set_transient( 'mypl_cache', $data, HOUR_IN_SECONDS );

// BAD
function function_name() {}  // No prefix, will conflict
class Settings {}  // Too generic
ステップ 2:管理者かどうかだけでなく、権限を確認する

誤り:権限チェックに is_admin() を使うこと

// WRONG - Anyone can access admin area URLs
if ( is_admin() ) {
    // Delete user data - SECURITY HOLE
}

// CORRECT - Check user capability
if ( current_user_can( 'manage_options' ) ) {
    // Delete user data - Now secure
}

一般的な権限:

  • manage_options - 管理者
  • edit_posts - 編集者/投稿者
  • publish_posts - 投稿者
  • edit_pages - 編集者
  • read - 購読者
ステップ 3:セキュリティの三位一体

入力 → 処理 → 出力には、それぞれ異なる関数が必要です。

// SANITIZATION (Input) - Clean user data
$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
$url = esc_url_raw( $_POST['url'] );
$html = wp_kses_post( $_POST['content'] );  // Allow safe HTML
$key = sanitize_key( $_POST['option'] );
$ids = array_map( 'absint', $_POST['ids'] );  // Array of integers

// VALIDATION (Logic) - Verify it meets requirements
if ( ! is_email( $email ) ) {
    wp_die( 'Invalid email' );
}

// ESCAPING (Output) - Make safe for display
echo esc_html( $name );
echo '<a href="' . esc_url( $url ) . '">';
echo '<div class="' . esc_attr( $class ) . '">';
echo '<textarea>' . esc_textarea( $content ) . '</textarea>';

重要なルール:入力時にサニタイズし、出力時にエスケープしてください。ユーザーデータを決して信用しないでください。

ステップ 4:ノンス(CSRF 対策)

概要:リクエストが自サイトから送信されたことを証明する使い捨てトークンです。

フォームのパターン:

// Generate nonce in form
<form method="post">
    <?php wp_nonce_field( 'mypl_action', 'mypl_nonce' ); ?>
    <input type="text" name="data" />
    <button type="submit">Submit</button>
</form>

// Verify nonce in handler
if ( ! isset( $_POST['mypl_nonce'] ) || ! wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' ) ) {
    wp_die( 'Security check failed' );
}

// Now safe to proceed
$data = sanitize_text_field( $_POST['data'] );

AJAX のパターン:

// JavaScript
jQuery.ajax({
    url: ajaxurl,
    data: {
        action: 'mypl_ajax_action',
        nonce: mypl_ajax_object.nonce,
        data: formData
    }
});
// PHP Handler
function mypl_ajax_handler() {
    check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );

    // Safe to proceed
    wp_send_json_success( array( 'message' => 'Success' ) );
}
add_action( 'wp_ajax_mypl_ajax_action', 'mypl_ajax_handler' );

// Localize script with nonce
wp_localize_script( 'mypl-script', 'mypl_ajax_object', array(
    'ajaxurl' => admin_url( 'admin-ajax.php' ),
    'nonce'   => wp_create_nonce( 'mypl-ajax-nonce' ),
) );
ステップ 5:データベースにはプリペアドステートメントを使う

重要:ユーザー入力を含むクエリでは、必ず $wpdb->prepare() を使ってください。

global $wpdb;

// WRONG - SQL Injection vulnerability
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}table WHERE id = {$_GET['id']}" );

// CORRECT - Prepared statement
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}table WHERE id = %d",
        $_GET['id']
    )
);

プレースホルダー:

  • %s - 文字列
  • %d - 整数
  • %f - 浮動小数点数

LIKE クエリ(特殊なケース):

$search = '%' . $wpdb->esc_like( $term ) . '%';
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}posts WHERE post_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 を使う


既知の問題の防止

このスキルにより、記録されている 20 件の問題を防止できます。

Issue #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'] ) );
Issue #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'] ) . '">';
Issue #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'] ) );
Issue #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
}
Issue #5:ファイルへの直接アクセス

エラー:PHP ファイルが WordPress のコンテキスト外で実行されます 出典:WordPress プラグインハンドブック 発生する理由:ファイルの先頭に ABSPATH チェックがありません 防止策:すべての PHP ファイルに ABSPATH チェックを追加してください

// Add to top of EVERY PHP file
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}
Issue #6:プレフィックスの衝突

エラー:関数やクラスが他のプラグインと衝突します 出典:WordPress コーディング規約 発生する理由:一意のプレフィックスを付けずに汎用的な名前を使用しています 防止策:グローバルコードすべてに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 プラグインハンドブック 発生する理由:CPT の登録後にリライトルールをフラッシュし忘れています 防止策:有効化時にフラッシュし、無効化時にクリアしてください

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' );
Issue #8:トランジェントがクリーンアップされない

エラー:期限切れのトランジェントがデータベースに蓄積します 出典:WordPress Transients API ドキュメント 発生する理由:アンインストール時にクリーンアップしていません 防止策:uninstall.php でトランジェントを削除してください

// 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 パフォーマンスのベストプラクティス 発生する理由:条件を確認せずにスクリプトやスタイルをエンキューしています 防止策:必要な場所でのみアセットを読み込んでください

// 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 データ検証 発生する理由:サニタイズせずに $_POST データを保存しています 防止策:保存前に必ずサニタイズしてください

// VULNERABLE
update_option( 'mypl_setting', $_POST['value'] );

// SECURE
update_option( 'mypl_setting', sanitize_text_field( $_POST['value'] ) );
Issue #11:LIKE クエリの誤り

エラー:SQL 構文エラーまたはインジェクションの脆弱性が発生します 出典:WordPress $wpdb ドキュメント 発生する理由: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 コーディング規約 発生する理由: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 ハンドブック 発生する理由:permission_callback が指定されていません 防止策:必ず permission_callback を追加してください

// VULNERABLE
register_rest_route( 'myplugin/v1', '/data', array(
    'callback' => 'my_callback',
) );

// SECURE
register_rest_route( 'myplugin/v1', '/data', array(
    'callback'            => 'my_callback',
    'permission_callback' => function() {
        return current_user_can( 'edit_posts' );
    },
) );
Issue #14:アンインストールフックが繰り返し登録される

エラー:ページが読み込まれるたびにオプションが書き込まれます 出典:WordPress プラグインハンドブック 発生する理由:メインの処理フロー内で 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' );
        },
    ) );
} );

プラグインのアーキテクチャパターン

パターン 1:シンプルなプラグイン(関数のみ)

使用する場合:関数が 5 個未満で、複雑な状態を持たない小規模なプラグイン

<?php
/**
 * Plugin Name: Simple Plugin
 */

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

function mypl_init() {
    // Your code here
}
add_action( 'init', 'mypl_init' );

function mypl_admin_menu() {
    add_options_page(
        'My Plugin',
        'My Plugin',
        'manage_options',
        'my-plugin',
        'mypl_settings_page'
    );
}
add_action( 'admin_menu', 'mypl_admin_menu' );

function mypl_settings_page() {
    ?>
    <div class="wrap">
        <h1>My Plugin Settings</h1>
    </div>
    <?php
}
パターン 2:OOP プラグイン

使用する場合:関連する機能を持ち、整理が必要な中規模のプラグイン

<?php
/**
 * Plugin Name: OOP Plugin
 */

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

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() {
        $this->define_constants();
        $this->init_hooks();
    }

    private function define_constants() {
        define( 'MYPL_VERSION', '1.0.0' );
        define( 'MYPL_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
        define( 'MYPL_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    }

    private function init_hooks() {
        add_action( 'init', array( $this, 'init' ) );
        add_action( 'admin_menu', array( $this, 'admin_menu' ) );
    }

    public function init() {
        // Initialization code
    }

    public function admin_menu() {
        add_options_page(
            'My Plugin',
            'My Plugin',
            'manage_options',
            'my-plugin',
            array( $this, 'settings_page' )
        );
    }

    public function settings_page() {
        ?>
        <div class="wrap">
            <h1>My Plugin Settings</h1>
        </div>
        <?php
    }
}

// Initialize plugin
function mypl() {
    return MyPL_Plugin::get_instance();
}
mypl();
パターン 3:PSR-4 プラグイン(モダン、推奨)

使用する場合:大規模またはモダンなプラグイン、チーム開発、2025 年以降のベストプラクティス

ディレクトリ構成:

my-plugin/
├── my-plugin.php       # Main file
├── composer.json       # Autoloading config
├── src/                # PSR-4 autoloaded classes
│   ├── Admin.php
│   ├── Frontend.php
│   └── Settings.php
├── languages/
└── uninstall.php

composer.json:

{
    "name": "my-vendor/my-plugin",
    "autoload": {
        "psr-4": {
            "MyPlugin\\": "src/"
        }
    },
    "require": {
        "php": ">=7.4"
    }
}

my-plugin.php:

<?php
/**
 * Plugin Name: PSR-4 Plugin
 */

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

// Composer autoloader
require_once __DIR__ . '/vendor/autoload.php';

use MyPlugin\Admin;
use MyPlugin\Frontend;

class MyPlugin {

    private static $instance = null;

    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        $this->init();
    }

    private function init() {
        new Admin();
        new Frontend();
    }
}

MyPlugin::get_instance();

src/Admin.php:

<?php

namespace MyPlugin;

class Admin {

    public function __construct() {
        add_action( 'admin_menu', array( $this, 'add_menu' ) );
    }

    public function add_menu() {
        add_options_page(
            'My Plugin',
            'My Plugin',
            'manage_options',
            'my-plugin',
            array( $this, 'settings_page' )
        );
    }

    public function settings_page() {
        ?>
        <div class="wrap">
            <h1>My Plugin Settings</h1>
        </div>
        <?php
    }
}

よく使われるパターン

パターン 1:カスタム投稿タイプ
function mypl_register_cpt() {
    register_post_type( 'book', array(
        'labels' => array(
            'name'          => 'Books',
            'singular_name' => 'Book',
            'add_new_item'  => 'Add New Book',
        ),
        'public'       => true,
        'has_archive'  => true,
        'show_in_rest' => true,  // Gutenberg support
        'supports'     => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
        'rewrite'      => array( 'slug' => 'books' ),
        'menu_icon'    => 'dashicons-book',
    ) );
}
add_action( 'init', 'mypl_register_cpt' );

// CRITICAL: Flush rewrite rules 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' );
パターン 2:カスタムタクソノミー
function mypl_register_taxonomy() {
    register_taxonomy( 'genre', 'book', array(
        'labels' => array(
            'name'          => 'Genres',
            'singular_name' => 'Genre',
        ),
        'hierarchical' => true,  // Like categories
        'show_in_rest' => true,
        'rewrite'      => array( 'slug' => 'genre' ),
    ) );
}
add_action( 'init', 'mypl_register_taxonomy' );
パターン 3:メタボックス
function mypl_add_meta_box() {
    add_meta_box(
        'book_details',
        'Book Details',
        'mypl_meta_box_html',
        'book',
        'normal',
        'high'
    );
}
add_action( 'add_meta_boxes', 'mypl_add_meta_box' );

function mypl_meta_box_html( $post ) {
    $isbn = get_post_meta( $post->ID, '_book_isbn', true );

    wp_nonce_field( 'mypl_save_meta', 'mypl_meta_nonce' );
    ?>
    <label for="book_isbn">ISBN:</label>
    <input type="text" id="book_isbn" name="book_isbn" value="<?php echo esc_attr( $isbn ); ?>" />
    <?php
}

function mypl_save_meta( $post_id ) {
    // Security checks
    if ( ! isset( $_POST['mypl_meta_nonce'] )
         || ! wp_verify_nonce( $_POST['mypl_meta_nonce'], 'mypl_save_meta' ) ) {
        return;
    }

    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    // Save data
    if ( isset( $_POST['book_isbn'] ) ) {
        update_post_meta(
            $post_id,
            '_book_isbn',
            sanitize_text_field( $_POST['book_isbn'] )
        );
    }
}
add_action( 'save_post_book', 'mypl_save_meta' );
パターン 4:Settings API
function mypl_add_menu() {
    add_options_page(
        'My Plugin Settings',
        'My Plugin',
        'manage_options',
        'my-plugin',
        'mypl_settings_page'
    );
}
add_action( 'admin_menu', 'mypl_add_menu' );

function mypl_register_settings() {
    register_setting( 'mypl_options', 'mypl_api_key', array(
        'type'              => 'string',
        'sanitize_callback' => 'sanitize_text_field',
        'default'           => '',
    ) );

    add_settings_section(
        'mypl_section',
        'API Settings',
        'mypl_section_callback',
        'my-plugin'
    );

    add_settings_field(
        'mypl_api_key',
        'API Key',
        'mypl_field_callback',
        'my-plugin',
        'mypl_section'
    );
}
add_action( 'admin_init', 'mypl_register_settings' );

function mypl_section_callback() {
    echo '<p>Configure your API settings.</p>';
}

function mypl_field_callback() {
    $value = get_option( 'mypl_api_key' );
    ?>
    <input type="text" name="mypl_api_key" value="<?php echo esc_attr( $value ); ?>" />
    <?php
}

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
            settings_fields( 'mypl_options' );
            do_settings_sections( 'my-plugin' );
            submit_button();
            ?>
        </form>
    </div>
    <?php
}
パターン 5:REST API エンドポイント
add_action( 'rest_api_init', function() {
    register_rest_route( 'myplugin/v1', '/data', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'mypl_rest_callback',
        'permission_callback' => function() {
            return current_user_can( 'edit_posts' );
        },
        'args'                => array(
            'id' => array(
                'required'          => true,
                'validate_callback' => function( $param ) {
                    return is_numeric( $param );
                },
                'sanitize_callback' => 'absint',
            ),
        ),
    ) );
} );

function mypl_rest_callback( $request ) {
    $id = $request->get_param( 'id' );

    // Process...

    return new WP_REST_Response( array(
        'success' => true,
        'data'    => $data,
    ), 200 );
}
パターン 6:AJAX ハンドラー(レガシー)
// Enqueue script with localized data
function mypl_enqueue_ajax_script() {
    wp_enqueue_script( 'mypl-ajax', plugins_url( 'js/ajax.js', __FILE__ ), array( 'jquery' ), '1.0', true );

    wp_localize_script( 'mypl-ajax', 'mypl_ajax_object', array(
        'ajaxurl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'mypl-ajax-nonce' ),
    ) );
}
add_action( 'wp_enqueue_scripts', 'mypl_enqueue_ajax_script' );

// AJAX handler (logged-in users)
function mypl_ajax_handler() {
    check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );

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

    // Process...

    wp_send_json_success( array( 'message' => 'Success' ) );
}
add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' );

// AJAX handler (logged-out users)
add_action( 'wp_ajax_nopriv_mypl_action', 'mypl_ajax_handler' );

JavaScript(js/ajax.js):

jQuery(document).ready(function($) {
    $('#my-button').on('click', function() {
        $.ajax({
            url: mypl_ajax_object.ajaxurl,
            type: 'POST',
            data: {
                action: 'mypl_action',
                nonce: mypl_ajax_object.nonce,
                data: 'value'
            },
            success: function(response) {
                console.log(response.data.message);
            }
        });
    });
});
パターン 7:カスタムデータベーステーブル
function mypl_create_tables() {
    global $wpdb;

    $table_name = $wpdb->prefix . 'mypl_data';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE $table_name (
        id bigint(20) NOT NULL AUTO_INCREMENT,
        user_id bigint(20) NOT NULL,
        data text NOT NULL,
        created datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
        PRIMARY KEY  (id),
        KEY user_id (user_id)
    ) $charset_collate;";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );

    add_option( 'mypl_db_version', '1.0' );
}

// Create tables on activation
register_activation_hook( __FILE__, 'mypl_create_tables' );
パターン 8:キャッシュ用の Transients
function mypl_get_expensive_data() {
    // Try to get cached data
    $data = get_transient( 'mypl_expensive_data' );

    if ( false === $data ) {
        // Not cached - regenerate
        $data = perform_expensive_operation();

        // Cache for 12 hours
        set_transient( 'mypl_expensive_data', $data, 12 * HOUR_IN_SECONDS );
    }

    return $data;
}

// Clear cache when data changes
function mypl_clear_cache() {
    delete_transient( 'mypl_expensive_data' );
}
add_action( 'save_post', 'mypl_clear_cache' );

同梱リソースの使用

テンプレート(templates/)

これらの本番環境対応テンプレートを使って、プラグインをすばやく作成できます。

  • templates/plugin-simple/ - 関数を使ったシンプルなプラグイン
  • templates/plugin-oop/ - オブジェクト指向のプラグイン構成
  • templates/plugin-psr4/ - Composer を使ったモダンな PSR-4 プラグイン
  • templates/examples/meta-box.php - メタボックスの実装
  • templates/examples/settings-page.php - Settings API のページ
  • templates/examples/custom-post-type.php - CPT の登録
  • templates/examples/rest-endpoint.php - REST API エンドポイント
  • templates/examples/ajax-handler.php - AJAX の実装

Claude がこれらを使用する場合:新しいプラグインを作成するとき、または特定の機能パターンを実装するとき。

スクリプト(scripts/)
  • scripts/scaffold-plugin.sh - 対話形式のプラグインひな形作成
  • scripts/check-security.sh - よくある問題のセキュリティ監査
  • scripts/validate-headers.sh - プラグインヘッダーの検証

使用例:

# Scaffold new plugin
./scripts/scaffold-plugin.sh my-plugin simple

# Check for security issues
./scripts/check-security.sh my-plugin.php

# Validate plugin headers
./scripts/validate-headers.sh my-plugin.php
参考資料(references/)

Claude が必要に応じて読み込める詳細なドキュメントです。

  • references/security-checklist.md - セキュリティ監査の完全なチェックリスト
  • references/hooks-reference.md - WordPress の一般的なフックとフィルター
  • references/sanitization-guide.md - すべてのサニタイズ/エスケープ関数
  • references/wpdb-patterns.md - データベースクエリのパターン
  • references/common-errors.md - エラー防止に関する詳細ガイド

Claude がこれらを読み込むべき場面:セキュリティの問題への対応、適切なフックの選択、特定のデータ型のサニタイズ、データベースクエリの作成、よくあるエラーのデバッグを行う場合です。


高度なトピック

国際化(i18n)
// Load text domain
function mypl_load_textdomain() {
    load_plugin_textdomain( 'my-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}
add_action( 'plugins_loaded', 'mypl_load_textdomain' );

// Translatable strings
__( 'Text', 'my-plugin' );  // Returns translated string
_e( 'Text', 'my-plugin' );  // Echoes translated string
_n( 'One item', '%d items', $count, 'my-plugin' );  // Plural forms
esc_html__( 'Text', 'my-plugin' );  // Translate and escape
esc_html_e( 'Text', 'my-plugin' );  // Translate, escape, and echo
WP-CLI コマンド
if ( defined( 'WP_CLI' ) && WP_CLI ) {

    class MyPL_CLI_Command {

        /**
         * Process data
         *
         * ## EXAMPLES
         *
         *     wp mypl process --limit=100
         *
         * @param array $args
         * @param array $assoc_args
         */
        public function process( $args, $assoc_args ) {
            $limit = isset( $assoc_args['limit'] ) ? absint( $assoc_args['limit'] ) : 10;

            WP_CLI::line( "Processing $limit items..." );

            // Process...

            WP_CLI::success( 'Processing complete!' );
        }
    }

    WP_CLI::add_command( 'mypl', 'MyPL_CLI_Command' );
}
スケジュールイベント(Cron)
// Schedule event on activation
function mypl_activate() {
    if ( ! wp_next_scheduled( 'mypl_daily_task' ) ) {
        wp_schedule_event( time(), 'daily', 'mypl_daily_task' );
    }
}
register_activation_hook( __FILE__, 'mypl_activate' );

// Clear event on deactivation
function mypl_deactivate() {
    wp_clear_scheduled_hook( 'mypl_daily_task' );
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );

// Hook to scheduled event
function mypl_do_daily_task() {
    // Perform task
}
add_action( 'mypl_daily_task', 'mypl_do_daily_task' );
プラグインの依存関係チェック
add_action( 'admin_init', function() {
    // Check for WooCommerce
    if ( ! class_exists( 'WooCommerce' ) ) {
        deactivate_plugins( plugin_basename( __FILE__ ) );

        add_action( 'admin_notices', function() {
            echo '<div class="error"><p><strong>My Plugin</strong> requires WooCommerce to be installed and active.</p></div>';
        } );

        if ( isset( $_GET['activate'] ) ) {
            unset( $_GET['activate'] );
        }
    }
} );

配布と自動更新

GitHub の自動更新を有効にする

WordPress.org 以外でホストされているプラグインでも、YahnisElsts の Plugin Update Checker を使用すれば自動更新を提供できます。ほとんどのユースケースで推奨されるソリューションです。

クイックスタート:

// 1. Install library (git submodule or Composer)
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'
);

// Use GitHub Releases (recommended)
$updateChecker->getVcsApi()->enableReleaseAssets();

// For private repos, use token from wp-config.php
if ( defined( 'YOUR_PLUGIN_GITHUB_TOKEN' ) ) {
    $updateChecker->setAuthentication( YOUR_PLUGIN_GITHUB_TOKEN );
}

デプロイ:

# 1. Update version in plugin header
# 2. Commit and tag
git add my-plugin.php
git commit -m "Bump version to 1.0.1"
git tag 1.0.1
git push origin main
git push origin 1.0.1

# 3. Create GitHub Release (optional but recommended)
# - Upload pre-built ZIP file (exclude .git, tests, etc.)
# - Add release notes for users

主な機能:

✅ GitHub、GitLab、BitBucket、またはカスタムサーバーで動作します ✅ パブリックリポジトリとプライベートリポジトリに対応しています ✅ バージョン管理に GitHub Releases またはタグを使用します ✅ HTTPS ベースの安全な更新を利用できます ✅ オプションでライセンスキーと連携できます ✅ プロフェッショナルなリリースノートと変更履歴を利用できます ✅ ライブラリのサイズは約 100KB です

代替ソリューション:

  1. Git Updater(ユーザーがインストールできるプラグイン。コーディングは不要です)
  2. カスタム更新サーバー(完全な制御が可能ですが、ホスティングが必要です)
  3. Freemius(商用サービス。ライセンス管理と支払い機能を含みます)

詳細なリソース:

  • 完全ガイド:references/github-auto-updates.md を参照してください(全 21 ページ、すべての方法を網羅)
  • 実装例:examples/github-updater.php を参照してください(10 個の例)
  • セキュリティのベストプラクティス:チェックサム、署名、トークンの保存、レート制限
  • テンプレートとの連携:3 種類すべてのプラグインテンプレートにセットアップ手順が含まれています

セキュリティに関する考慮事項:

  • ✅ リポジトリの URL には必ず HTTPS を使用してください
  • ✅ 認証トークンをハードコードしないでください(wp-config.php を使用してください)
  • ✅ 更新を提供する前にライセンスを検証してください
  • ✅ オプション:ファイル検証用のチェックサムを追加してください
  • ✅ API のレート制限を超えないよう、更新チェックをレート制限してください
  • ✅ インストール後にキャッシュされた更新データを消去してください

各方法の用途:

用途推奨ソリューション
オープンソース、パブリックリポジトリPlugin Update Checker
プライベートプラグイン、クライアント案件Plugin Update Checker + プライベートリポジトリ
商用プラグインFreemius またはカスタムサーバー
複数のプラットフォームでの Git ホスティングGit Updater
カスタムライセンス要件カスタム更新サーバー

ZIP 構造の要件:

plugin.zip
└── my-plugin/       ← Plugin folder MUST be inside ZIP
    ├── my-plugin.php
    ├── readme.txt
    └── ...

構造が正しくない場合、WordPress がランダムなフォルダー名を作成し、プラグインが動作しなくなります!


依存関係

必須:

  • WordPress 5.9+(6.7+ を推奨)
  • PHP 7.4+(8.0+ を推奨)

任意:

  • Composer 2.0+ - PSR-4 オートローディング用
  • WP-CLI 2.0+ - コマンドラインでのプラグイン管理用
  • Query Monitor - デバッグとパフォーマンス分析用

公式ドキュメント


トラブルシューティング

問題:プラグインが致命的なエラーを引き起こす

解決方法:

  1. wp-config.php で WP_DEBUG を有効にします
  2. wp-content/debug.log のエラーログを確認します
  3. すべてのクラス名/関数名にプレフィックスが付いていることを確認します
  4. 依存関係の不足がないか確認します
問題:カスタム投稿タイプのページで 404 エラーが発生する

解決方法:リライトルールを再生成します

// Temporarily add to wp-admin
flush_rewrite_rules();
// Remove after visiting wp-admin once
問題:Nonce の検証が常に失敗する

解決方法:

  1. フィールドと検証処理で Nonce 名が一致していることを確認します
  2. 正しいアクション名を使って検証します
  3. Nonce の有効期限が切れていないことを確認します(デフォルトは 24 時間です)
問題:AJAX が 0 または -1 を返す

解決方法:

  1. アクション名がフック wp_ajax_{action} と一致していることを確認します
  2. Nonce が送信され、検証されていることを確認します
  3. ハンドラー関数が存在し、正しくフックされていることを確認します
問題:サニタイズによって HTML が削除される

解決方法:安全な HTML を許可するには、sanitize_text_field() の代わりに wp_kses_post() を使用します

問題:データベースクエリが機能しない

解決方法:

  1. 変数を含むクエリには、必ず $wpdb->prepare() を使用します
  2. テーブル名に $wpdb->prefix が含まれていることを確認します
  3. カラム名と構文を確認します

セットアップ完全チェックリスト

このチェックリストを使って、プラグインを確認してください。

  • すべてのフィールドを含むプラグインヘッダーが完成している
  • すべての PHP ファイルの先頭に ABSPATH チェックがある
  • すべての関数/クラスで一意のプレフィックスを使用している
  • すべてのフォームで Nonce 検証を行っている
  • すべてのユーザー入力をサニタイズしている
  • すべての出力をエスケープしている
  • すべてのデータベースクエリで $wpdb->prepare() を使用している
  • 権限チェックを行っている(is_admin() だけに頼っていない)
  • カスタム投稿タイプの有効化時にリライトルールを再生成している
  • 停止フックでは一時データのみを消去している
  • uninstall.php で永続データをクリーンアップしている
  • テキストドメインがプラグインのスラッグと一致している
  • スクリプト/スタイルを必要な場所でのみ読み込んでいる
  • 開発中は WP_DEBUG を有効にしている
  • Query Monitor でパフォーマンスをテストしている
  • 非推奨関数の警告がない
  • 最新の WordPress バージョンで動作する

質問や問題がある場合

  1. 詳細なトラブルシューティングについては references/common-errors.md を確認します
  2. セキュリティの基礎に関するすべての手順を確認します
  3. 公式ドキュメントを確認します:https://developer.wordpress.org/plugins/
  4. WP_DEBUG を有効にして debug.log を確認します
  5. Query Monitor プラグインを使ってフックとクエリをデバッグします

コメント