2026年10月16日

2026年10月16日

WordPressに予約システムを実装する方法【カスタム実装・プラグイン比較】

はじめに

美容室・歯医者・会議室・レンタルスペースなどの予約システムをWordPressに実装したいケースは非常に多いです。既存プラグイン(Bookly・Amelia・Booking Calendar)では要件を満たせない場合、カスタム実装が有効です。本記事ではカスタム投稿タイプとFullCalendarカレンダー、AJAX予約フォーム、管理者向けリスト画面まで完全なカスタム予約システムを構築する方法を解説します。

症状・原因

  • 既存の予約プラグインでは要件(時間枠・スタッフ割り当て・複数ロケーション)を満たせない
  • 予約の二重予約チェックが機能せず、同じ時間枠に複数の予約が入ってしまう
  • 予約完了メールがWordPressの標準wp_mailで送れず、予約者に確認メールが届かない

解決手順

ステップ1:カスタム投稿タイプ’booking’と予約メタを設定する

<?php
// 予約用カスタム投稿タイプを登録
add_action( 'init', 'register_booking_post_type' );
function register_booking_post_type() {
    register_post_type( 'booking', array(
        'label'              => '予約',
        'labels'             => array(
            'name'          => '予約管理',
            'singular_name' => '予約',
            'add_new_item'  => '新しい予約',
            'edit_item'     => '予約を編集',
        ),
        'public'             => false,
        'show_ui'            => true,
        'show_in_menu'       => true,
        'menu_icon'          => 'dashicons-calendar-alt',
        'supports'           => array( 'title', 'custom-fields' ),
        'capability_type'    => 'post',
    ) );
}

// 予約に使うカスタムタクソノミー(サービス種別)
add_action( 'init', 'register_service_taxonomy' );
function register_service_taxonomy() {
    register_taxonomy( 'service', 'booking', array(
        'label'        => 'サービス',
        'hierarchical' => true,
        'show_ui'      => true,
        'rewrite'      => array( 'slug' => 'service' ),
    ) );
}

// 予約メタボックスを追加
add_action( 'add_meta_boxes', 'add_booking_meta_boxes' );
function add_booking_meta_boxes() {
    add_meta_box( 'booking_details', '予約詳細', 'render_booking_meta_box', 'booking', 'normal', 'high' );
}

function render_booking_meta_box( $post ) {
    wp_nonce_field( 'save_booking_meta', 'booking_nonce' );

    $booking_date  = get_post_meta( $post->ID, '_booking_date', true );
    $booking_time  = get_post_meta( $post->ID, '_booking_time', true );
    $booking_end   = get_post_meta( $post->ID, '_booking_end_time', true );
    $capacity      = get_post_meta( $post->ID, '_booking_capacity', true ) ?: 1;
    $customer_name = get_post_meta( $post->ID, '_customer_name', true );
    $customer_email = get_post_meta( $post->ID, '_customer_email', true );
    $customer_phone = get_post_meta( $post->ID, '_customer_phone', true );
    $status        = get_post_meta( $post->ID, '_booking_status', true ) ?: 'pending';
    ?>
    <table class="form-table">
        <tr><th>予約日</th><td><input type="date" name="booking_date" value="<?php echo esc_attr( $booking_date ); ?>"></td></tr>
        <tr><th>開始時間</th><td><input type="time" name="booking_time" value="<?php echo esc_attr( $booking_time ); ?>"></td></tr>
        <tr><th>終了時間</th><td><input type="time" name="booking_end" value="<?php echo esc_attr( $booking_end ); ?>"></td></tr>
        <tr><th>定員</th><td><input type="number" name="booking_capacity" value="<?php echo esc_attr( $capacity ); ?>" min="1"></td></tr>
        <tr><th>お客様名</th><td><input type="text" name="customer_name" value="<?php echo esc_attr( $customer_name ); ?>" class="regular-text"></td></tr>
        <tr><th>メール</th><td><input type="email" name="customer_email" value="<?php echo esc_attr( $customer_email ); ?>" class="regular-text"></td></tr>
        <tr><th>電話番号</th><td><input type="tel" name="customer_phone" value="<?php echo esc_attr( $customer_phone ); ?>" class="regular-text"></td></tr>
        <tr>
            <th>ステータス</th>
            <td>
                <select name="booking_status">
                    <option value="pending" <?php selected( $status, 'pending' ); ?>>未確認</option>
                    <option value="confirmed" <?php selected( $status, 'confirmed' ); ?>>確認済み</option>
                    <option value="cancelled" <?php selected( $status, 'cancelled' ); ?>>キャンセル</option>
                    <option value="completed" <?php selected( $status, 'completed' ); ?>>完了</option>
                </select>
            </td>
        </tr>
    </table>
    <?php
}

ステップ2:FullCalendarでJavaScript予約カレンダーを実装する

<?php
// FullCalendarのスクリプトと予約データを読み込む
add_action( 'wp_enqueue_scripts', 'enqueue_booking_calendar_assets' );
function enqueue_booking_calendar_assets() {
    if ( ! is_page( 'booking' ) ) {
        return;
    }

    // FullCalendar(CDN経由)
    wp_enqueue_script( 'fullcalendar', 'https://cdn.jsdelivr.net/npm/fullcalendar@6.1.9/index.global.min.js', array(), '6.1.9', true );
    wp_enqueue_script( 'booking-calendar', plugin_dir_url( __FILE__ ) . 'js/booking-calendar.js', array( 'fullcalendar', 'jquery' ), '1.0', true );

    // 予約済み日程をJSに渡す(サニタイズ済み)
    wp_localize_script( 'booking-calendar', 'bookingParams', array(
        'ajax_url'    => admin_url( 'admin-ajax.php' ),
        'nonce'       => wp_create_nonce( 'booking_nonce' ),
        'booked_dates' => get_booked_dates_for_calendar(),
    ) );
}

// カレンダー用の予約済み日程データを取得
function get_booked_dates_for_calendar() {
    $bookings = get_posts( array(
        'post_type'      => 'booking',
        'posts_per_page' => -1,
        'post_status'    => 'publish',
        'date_query'     => array(
            array( 'after' => '-1 month' ), // 過去1ヶ月以降のみ
        ),
        'meta_query'     => array(
            array(
                'key'     => '_booking_status',
                'value'   => array( 'pending', 'confirmed' ),
                'compare' => 'IN',
            ),
        ),
    ) );

    $events = array();
    foreach ( $bookings as $booking ) {
        $events[] = array(
            'id'      => $booking->ID,
            'title'   => get_post_meta( $booking->ID, '_customer_name', true ) ? '予約済み' : '予約あり',
            'start'   => get_post_meta( $booking->ID, '_booking_date', true ) . 'T' . get_post_meta( $booking->ID, '_booking_time', true ),
            'end'     => get_post_meta( $booking->ID, '_booking_date', true ) . 'T' . get_post_meta( $booking->ID, '_booking_end_time', true ),
            'color'   => '#0073aa',
        );
    }

    return $events;
}
// js/booking-calendar.js
document.addEventListener('DOMContentLoaded', function() {
    const calendarEl = document.getElementById('booking-calendar');
    if (!calendarEl) return;

    const calendar = new FullCalendar.Calendar(calendarEl, {
        initialView: 'dayGridMonth',
        locale: 'ja',
        headerToolbar: {
            left: 'prev,next today',
            center: 'title',
            right: 'dayGridMonth,timeGridWeek'
        },
        events: bookingParams.booked_dates,
        selectable: true, // 日付をクリックで選択
        select: function(info) {
            // 選択した日付を予約フォームに設定
            document.getElementById('booking-date').value = info.startStr;
            document.getElementById('booking-form-section').scrollIntoView();
        }
    });
    calendar.render();
});

ステップ3:AJAXで予約フォームを送信して空き確認を行う

<?php
// AJAX予約処理(ログイン不要)
add_action( 'wp_ajax_nopriv_submit_booking', 'ajax_submit_booking' );
add_action( 'wp_ajax_submit_booking', 'ajax_submit_booking' );
function ajax_submit_booking() {
    // Nonce検証
    check_ajax_referer( 'booking_nonce', 'nonce' );

    // 入力値のサニタイズ
    $booking_date   = sanitize_text_field( $_POST['booking_date'] );
    $booking_time   = sanitize_text_field( $_POST['booking_time'] );
    $booking_end    = sanitize_text_field( $_POST['booking_end'] );
    $customer_name  = sanitize_text_field( $_POST['customer_name'] );
    $customer_email = sanitize_email( $_POST['customer_email'] );
    $customer_phone = sanitize_text_field( $_POST['customer_phone'] );

    // バリデーション
    if ( empty( $booking_date ) || empty( $booking_time ) || empty( $customer_name ) || ! is_email( $customer_email ) ) {
        wp_send_json_error( array( 'message' => '必須項目を入力してください。' ) );
    }

    // 日付バリデーション(過去日は不可)
    if ( strtotime( $booking_date ) < strtotime( 'today' ) ) {
        wp_send_json_error( array( 'message' => '過去の日付は予約できません。' ) );
    }

    // 空き確認(二重予約チェック)
    if ( ! is_time_slot_available( $booking_date, $booking_time, $booking_end ) ) {
        wp_send_json_error( array( 'message' => '選択した時間帯はすでに予約が入っています。別の時間をお選びください。' ) );
    }

    // 予約を保存
    $booking_id = wp_insert_post( array(
        'post_type'   => 'booking',
        'post_status' => 'publish',
        'post_title'  => $booking_date . ' ' . $booking_time . ' - ' . $customer_name,
        'meta_input'  => array(
            '_booking_date'     => $booking_date,
            '_booking_time'     => $booking_time,
            '_booking_end_time' => $booking_end,
            '_customer_name'    => $customer_name,
            '_customer_email'   => $customer_email,
            '_customer_phone'   => $customer_phone,
            '_booking_status'   => 'pending',
        ),
    ) );

    if ( is_wp_error( $booking_id ) ) {
        wp_send_json_error( array( 'message' => '予約の保存に失敗しました。' ) );
    }

    // 確認メールを送信
    send_booking_confirmation_email( $booking_id );

    wp_send_json_success( array(
        'message'    => '予約を受け付けました。確認メールをご確認ください。',
        'booking_id' => $booking_id,
    ) );
}

// 空き確認関数
function is_time_slot_available( $date, $start_time, $end_time ) {
    global $wpdb;

    // 同じ日の予約を検索して重複チェック
    $existing = $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->postmeta} pm1
         INNER JOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id
         INNER JOIN {$wpdb->postmeta} pm3 ON pm1.post_id = pm3.post_id
         INNER JOIN {$wpdb->posts} p ON pm1.post_id = p.ID
         WHERE pm1.meta_key = '_booking_date' AND pm1.meta_value = %s
         AND pm2.meta_key = '_booking_time' AND pm2.meta_value < %s
         AND pm3.meta_key = '_booking_end_time' AND pm3.meta_value > %s
         AND p.post_status = 'publish'",
        $date, $end_time, $start_time
    ) );

    return intval( $existing ) === 0;
}

ステップ4:予約確認メールをwp_mailで送信する

<?php
// 予約確認メールを送信する
function send_booking_confirmation_email( $booking_id ) {
    $booking_date   = get_post_meta( $booking_id, '_booking_date', true );
    $booking_time   = get_post_meta( $booking_id, '_booking_time', true );
    $customer_name  = get_post_meta( $booking_id, '_customer_name', true );
    $customer_email = get_post_meta( $booking_id, '_customer_email', true );

    // 日付のフォーマット
    $formatted_date = wp_date( 'Y年n月j日(D)', strtotime( $booking_date ), new DateTimeZone( 'Asia/Tokyo' ) );

    // お客様への確認メール
    $customer_message = sprintf(
        "こんにちは、%s 様\n\n以下の内容でご予約を受け付けました。\n\n" .
        "【予約日時】%s %s〜\n" .
        "【予約番号】%d\n\n" .
        "ご来店をお待ちしております。\n\n" .
        "キャンセルは前日17:00までにお電話ください。\n\n" .
        "%s",
        $customer_name,
        $formatted_date,
        $booking_time,
        $booking_id,
        get_bloginfo( 'name' )
    );

    // HTMLメールのヘッダー
    $headers = array(
        'Content-Type: text/plain; charset=UTF-8',
        'From: ' . get_bloginfo( 'name' ) . ' <' . get_option( 'admin_email' ) . '>',
    );

    // お客様へ送信
    wp_mail( $customer_email, '【予約確認】' . $formatted_date . ' ' . $booking_time . 'のご予約', $customer_message, $headers );

    // 管理者への通知
    $admin_message = sprintf(
        "新しい予約が入りました。\n\nお客様: %s\nメール: %s\n日時: %s %s\n予約番号: %d\n\n管理画面: %s",
        $customer_name,
        $customer_email,
        $formatted_date,
        $booking_time,
        $booking_id,
        admin_url( 'post.php?post=' . $booking_id . '&action=edit' )
    );

    wp_mail( get_option( 'admin_email' ), '【管理者通知】新しい予約 - ' . $customer_name, $admin_message, $headers );
}

ステップ5:管理者用予約一覧画面をWP_List_Tableで実装する

<?php
// 管理者向け予約一覧ページ
add_action( 'admin_menu', 'add_booking_admin_page' );
function add_booking_admin_page() {
    add_menu_page(
        '予約管理',
        '予約管理',
        'manage_options',
        'booking-manager',
        'render_booking_list_page',
        'dashicons-calendar-alt',
        25
    );
}

function render_booking_list_page() {
    // 日付フィルタリング
    $filter_date   = sanitize_text_field( $_GET['filter_date'] ?? date( 'Y-m' ) );
    $filter_status = sanitize_text_field( $_GET['filter_status'] ?? '' );

    // メタクエリの構築
    $meta_query = array( 'relation' => 'AND' );

    if ( $filter_date ) {
        $meta_query[] = array(
            'key'     => '_booking_date',
            'value'   => $filter_date,
            'compare' => 'LIKE',
        );
    }

    if ( $filter_status ) {
        $meta_query[] = array(
            'key'   => '_booking_status',
            'value' => $filter_status,
        );
    }

    $bookings = get_posts( array(
        'post_type'      => 'booking',
        'posts_per_page' => -1,
        'post_status'    => 'publish',
        'orderby'        => 'meta_value',
        'meta_key'       => '_booking_date',
        'order'          => 'ASC',
        'meta_query'     => $meta_query,
    ) );

    ?>
    <div class="wrap">
        <h1>予約管理</h1>
        <!-- フィルタフォーム -->
        <form method="get">
            <input type="hidden" name="page" value="booking-manager">
            <label>月: <input type="month" name="filter_date" value="<?php echo esc_attr( $filter_date ); ?>"></label>
            <label>ステータス:
                <select name="filter_status">
                    <option value="">すべて</option>
                    <option value="pending" <?php selected( $filter_status, 'pending' ); ?>>未確認</option>
                    <option value="confirmed" <?php selected( $filter_status, 'confirmed' ); ?>>確認済み</option>
                    <option value="cancelled" <?php selected( $filter_status, 'cancelled' ); ?>>キャンセル</option>
                </select>
            </label>
            <input type="submit" class="button" value="絞り込み">
        </form>

        <table class="wp-list-table widefat fixed striped" style="margin-top:20px;">
            <thead>
                <tr>
                    <th>予約日</th><th>時間</th><th>お客様名</th>
                    <th>メール</th><th>電話</th><th>ステータス</th><th>操作</th>
                </tr>
            </thead>
            <tbody>
            <?php foreach ( $bookings as $booking ) :
                $date   = get_post_meta( $booking->ID, '_booking_date', true );
                $time   = get_post_meta( $booking->ID, '_booking_time', true );
                $name   = get_post_meta( $booking->ID, '_customer_name', true );
                $email  = get_post_meta( $booking->ID, '_customer_email', true );
                $phone  = get_post_meta( $booking->ID, '_customer_phone', true );
                $status = get_post_meta( $booking->ID, '_booking_status', true );
                $status_labels = array( 'pending' => '未確認', 'confirmed' => '確認済み', 'cancelled' => 'キャンセル', 'completed' => '完了' );
                $status_colors = array( 'pending' => '#f0b849', 'confirmed' => '#00a32a', 'cancelled' => '#dc3232', 'completed' => '#6c7086' );
            ?>
                <tr>
                    <td><?php echo esc_html( $date ); ?></td>
                    <td><?php echo esc_html( $time ); ?></td>
                    <td><?php echo esc_html( $name ); ?></td>
                    <td><?php echo esc_html( $email ); ?></td>
                    <td><?php echo esc_html( $phone ); ?></td>
                    <td><span style="color:<?php echo $status_colors[ $status ] ?? '#000'; ?>"><?php echo esc_html( $status_labels[ $status ] ?? $status ); ?></span></td>
                    <td><a href="<?php echo esc_url( admin_url( 'post.php?post=' . $booking->ID . '&action=edit' ) ); ?>" class="button button-small">詳細</a></td>
                </tr>
            <?php endforeach; ?>
            </tbody>
        </table>
    </div>
    <?php
}

注意事項

  • トランザクション安全性: 高トラフィックサイトでは同時予約リクエストで二重予約が発生する可能性があります。SELECT ... FOR UPDATEでデータベースレベルのロックを実装することを検討してください
  • タイムゾーン: 予約日時はサーバーのタイムゾーンとWordPressの設定が一致していることを確認してください。wp_date()はWordPressのタイムゾーン設定を考慮します
  • メール到達率: wp_mail()はサーバーのsendmailを使うため、迷惑メールに入りやすいです。WP-SMTP(SendGrid/Mailgun連携)で送信元ドメインを認証してください
  • キャンセルポリシー: 無断キャンセルを防ぐため、予約48時間前に自動リマインダーメールをWP-Cronで送信する実装を追加することを推奨します

まとめ

カスタム予約システムはカスタム投稿タイプbookingでデータを管理し、FullCalendarで視覚的なカレンダーUIを提供し、AJAXで非同期の予約フォームを実装し、wp_mail()で確認メールを送るという構成で実現できます。既存プラグインより柔軟に要件に合わせた予約ロジックを実装できます。関連記事:WordPressのAJAXフォームを実装する方法

お気軽にご相談ください

お見積りへ お問い合わせへ
▲