📚 実例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改造:スパム判定機能を追加し、スパムと判定されたログに印をつける
- 実例2改造:ビュー数をウィジェットで表示し、期間(7日・30日)で絞り込めるようにする
- 実例3改造:カスタム投稿タイプにも対応させ、タイプを切り替えられるUIを追加する
✅ この章のチェックリスト
- 実例1のコードを読んで、カスタムテーブル→フック→管理画面の流れを理解した
- 実例2のTransients APIキャッシュとキャッシュ無効化の実装を理解した
- 実例3のREST APIエンドポイントとJavaScript fetchの連携を理解した
- セキュリティの3点セット(nonce・権限・サニタイズ)が全実例で実装されているのを確認した
- 実例のコードを参考に、自分のプラグインに応用できるアイデアをメモした