📚 実例1: お問い合わせログプラグイン

Contact Form 7 と連携して、送信されたフォームデータをカスタムテーブルに記録し、管理画面で一覧表示するプラグインです。「STEP 3: 管理画面」「STEP 4: フック」「応用編: データベース操作」の技術を組み合わせています。

使用する主な技術

  • カスタムテーブル(dbDelta)
  • フック(wpcf7_mail_sent)
  • 管理メニュー(add_menu_page)
  • サニタイズ・エスケープ
  • $wpdb->get_results

機能概要

  • フォーム送信時にDBに記録
  • 管理画面で一覧表示
  • CSVエクスポート機能
  • 古いログの自動削除

プラグインの主要コード

<?php /** * Plugin Name: Contact Form Logger * Description: CF7のお問い合わせをDBに記録します * Version: 1.0.0 */ if ( ! defined( 'ABSPATH' ) ) exit; // ─ テーブル作成 ──────────────────────────────────────────── register_activation_hook( __FILE__, function() { global $wpdb; $table = $wpdb->prefix . 'cf_logs'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, form_id int(11) NOT NULL DEFAULT 0, name varchar(200) NOT NULL DEFAULT '', email varchar(200) NOT NULL DEFAULT '', message text NOT NULL, ip_address varchar(45) NOT NULL DEFAULT '', created_at datetime NOT NULL, PRIMARY KEY (id), KEY created_at (created_at) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $sql ); } ); // ─ CF7送信時に記録 ───────────────────────────────────────── add_action( 'wpcf7_mail_sent', function( $cf7 ) { global $wpdb; $sub = WPCF7_Submission::get_instance(); if ( ! $sub ) return; $data = $sub->get_posted_data(); $wpdb->insert( $wpdb->prefix . 'cf_logs', array( 'form_id' => $cf7->id(), 'name' => sanitize_text_field( $data['your-name'] ?? '' ), 'email' => sanitize_email( $data['your-email'] ?? '' ), 'message' => sanitize_textarea_field( $data['your-message'] ?? '' ), 'ip_address' => sanitize_text_field( $_SERVER['REMOTE_ADDR'] ?? '' ), 'created_at' => current_time( 'mysql' ), ), array( '%d', '%s', '%s', '%s', '%s', '%s' ) ); } ); // ─ 管理メニュー ─────────────────────────────────────────── add_action( 'admin_menu', function() { add_menu_page( 'お問い合わせログ', // ページタイトル 'お問い合わせログ', // メニュー名 'manage_options', // 必要な権限 'cf-logs', // スラッグ 'cf_logs_render_page',// コールバック関数 'dashicons-email-alt' // アイコン ); } ); function cf_logs_render_page() { if ( ! current_user_can( 'manage_options' ) ) return; global $wpdb; $logs = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}cf_logs ORDER BY created_at DESC LIMIT 100" ); echo '<div class="wrap">'; echo '<h1>お問い合わせログ</h1>'; echo '<table class="wp-list-table widefat striped">'; echo '<thead>'; echo ' <tr>'; echo ' <th>日時</th>'; echo ' <th>名前</th>'; echo ' <th>メール</th>'; echo ' <th>メッセージ</th>'; echo ' </tr>'; echo '</thead>'; echo '<tbody>'; foreach ( $logs as $log ) { printf( '<tr>' . ' <td>%s</td>' . ' <td>%s</td>' . ' <td>%s</td>' . ' <td>%s</td>' . '</tr>', esc_html( $log->created_at ), esc_html( $log->name ), esc_html( $log->email ), esc_html( mb_strimwidth( $log->message, 0, 50, '...' ) ) ); } echo '</tbody>'; echo '</table>'; echo '</div>'; }

📚 実例2: 投稿ビューカウンタープラグイン

投稿が閲覧されるたびにカウントを増やし、人気記事ランキングをショートコードで表示するプラグインです。「STEP 4: ショートコード」「応用編: パフォーマンス最適化(Transients API)」「応用編: データベース操作(メタデータAPI)」の技術を組み合わせています。

使用する主な技術

  • postmetaでビュー数を記録
  • Transients APIでキャッシュ
  • ショートコード [popular_posts]
  • WP_Queryの最適化

機能概要

  • 閲覧時に自動カウントアップ
  • [popular_posts count="5"] で表示
  • 結果を1時間キャッシュ
  • 管理画面でランキング確認
<?php /** * Plugin Name: View Counter * Description: 投稿のビュー数をカウントし人気記事を表示します */ if ( ! defined( 'ABSPATH' ) ) exit; // ─ ビュー数をカウントアップ ────────────────────────────── add_action( 'wp', function() { if ( ! is_singular( 'post' ) ) return; $post_id = get_the_ID(); $count = (int) get_post_meta( $post_id, '_view_count', true ); update_post_meta( $post_id, '_view_count', $count + 1 ); // キャッシュをクリア(ランキングの順位が変わる可能性があるため) delete_transient( 'vc_popular_posts' ); } ); // ─ 人気記事ショートコード [popular_posts count="5"] ────── add_shortcode( 'popular_posts', function( $atts ) { $atts = shortcode_atts( array( 'count' => 5 ), $atts ); $count = absint( $atts['count'] ); $cache_key = 'vc_popular_posts_' . $count; $posts = get_transient( $cache_key ); if ( false === $posts ) { $posts = get_posts( array( 'numberposts' => $count, 'meta_key' => '_view_count', 'orderby' => 'meta_value_num', 'order' => 'DESC', 'no_found_rows' => true, 'update_post_term_cache' => false, ) ); set_transient( $cache_key, $posts, HOUR_IN_SECONDS ); } if ( empty( $posts ) ) return ''; $html = '<ol class="popular-posts">'; foreach ( $posts as $post ) { $views = (int) get_post_meta( $post->ID, '_view_count', true ); $html .= sprintf( '<li><a href="%s">%s</a> <span class="views">(%d views)</span></li>', esc_url( get_permalink( $post ) ), esc_html( get_the_title( $post ) ), $views ); } $html .= '</ol>'; return $html; } );

📚 実例3: REST APIを使ったライブ検索

カスタムREST APIエンドポイントとJavaScriptを組み合わせた、ページ遷移なしのリアルタイム検索機能です。「応用編: REST API拡張」「STEP 4: Ajax処理」の技術を発展させた実装例です。

使用する主な技術

  • REST API エンドポイント
  • permission_callback(公開)
  • WP_Query で全文検索
  • JavaScript fetch API
  • デバウンス処理

機能概要

  • 入力ごとに検索API呼び出し
  • 300ms後に実行(デバウンス)
  • 10件の検索結果をJSON返却
  • リアルタイムでリストを更新
<?php /** * Plugin Name: Live Search * Description: リアルタイム検索機能を追加します */ if ( ! defined( 'ABSPATH' ) ) exit; // ─ REST APIエンドポイント ───────────────────────────────── add_action( 'rest_api_init', function() { register_rest_route( 'ls/v1', '/search', array( 'methods' => 'GET', 'callback' => 'ls_search_callback', 'permission_callback' => '__return_true', 'args' => array( 'q' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => function( $v ) { return strlen( $v ) >= 2; // 2文字以上必須 }, ), 'type' => array( 'default' => 'post', 'sanitize_callback' => 'sanitize_key', ), ), ) ); } ); function ls_search_callback( $request ) { $keyword = $request->get_param( 'q' ); $type = $request->get_param( 'type' ); $posts = get_posts( array( 's' => $keyword, 'post_type' => $type, 'numberposts' => 10, 'post_status' => 'publish', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); $results = array_map( function( $post ) { return array( 'id' => $post->ID, 'title' => get_the_title( $post ), 'url' => get_permalink( $post ), 'excerpt' => wp_trim_words( get_the_excerpt( $post ), 20 ), ); }, $posts ); return new WP_REST_Response( $results, 200 ); } // ─ 検索ボックスをショートコードで出力 ──────────────────── add_shortcode( 'live_search', function() { wp_enqueue_script( 'ls-script', plugin_dir_url( __FILE__ ) . 'js/live-search.js', array(), '1.0', true // フッターに出力 ); wp_localize_script( 'ls-script', 'lsApi', array( 'url' => rest_url( 'ls/v1/search' ) ) ); return '<div id="live-search"> <input type="text" id="ls-input" placeholder="検索..." autocomplete="off"> <ul id="ls-results"></ul> </div>'; } );
document.addEventListener('DOMContentLoaded', function() { const input = document.getElementById('ls-input'); const results = document.getElementById('ls-results'); let timer; input.addEventListener('input', function() { const q = this.value.trim(); clearTimeout(timer); if ( q.length < 2 ) { results.innerHTML = ''; return; } // デバウンス:300ms後に実行(入力のたびに即実行しない) timer = setTimeout(async () => { try { const res = await fetch(`${lsApi.url}?q=${encodeURIComponent(q)}`); const data = await res.json(); if ( data.length === 0 ) { results.innerHTML = '<li>結果が見つかりませんでした</li>'; return; } results.innerHTML = data.map(p => `<li><a href="${p.url}">${p.title}</a><p>${p.excerpt}</p></li>` ).join(''); } catch(e) { console.error('Search error:', e); } }, 300); }); });

🎯 実例から学ぶポイントまとめ

セキュリティを一切妥協しない

3つの実例すべてで nonce・権限チェック・サニタイズ・エスケープを実装しています。面倒でも省略は絶対にしてはいけません。

パフォーマンスを常に意識する

実例2ではTransients APIでキャッシュし、WP_Queryのオプションを最適化しています。実例3ではデバウンスでAPI呼び出し回数を減らしています。

責務を分離してコードを整理する

テーブル作成・データ記録・表示の各処理を別々の関数に分離しています。関数が小さくなると、テストや修正が容易になります。

WordPressの標準機能を最大活用する

独自にHTMLを組み立てるより、wp_trim_words()get_permalink()current_time('mysql') などのWordPress標準関数を使う方が安全で確実です。

🚀 次のステップ

📝 実例を改造してみよう

  1. 実例1改造:スパム判定機能を追加し、スパムと判定されたログに印をつける
  2. 実例2改造:ビュー数をウィジェットで表示し、期間(7日・30日)で絞り込めるようにする
  3. 実例3改造:カスタム投稿タイプにも対応させ、タイプを切り替えられるUIを追加する

✅ この章のチェックリスト

  • 実例1のコードを読んで、カスタムテーブル→フック→管理画面の流れを理解した
  • 実例2のTransients APIキャッシュとキャッシュ無効化の実装を理解した
  • 実例3のREST APIエンドポイントとJavaScript fetchの連携を理解した
  • セキュリティの3点セット(nonce・権限・サニタイズ)が全実例で実装されているのを確認した
  • 実例のコードを参考に、自分のプラグインに応用できるアイデアをメモした

🔗 関連ページ・さらに学ぶ

📘 実例で使われている技術

🔒 安全に実装するために