開発

WordPressで予約システムを作る:プラグイン比較とカスタム投稿型・排他制御の実装【7.1系】

WordPressで予約システムを作る:プラグイン比較とカスタム投稿型・排他制御の実装【7.1系】

WordPressに予約機能を付ける方法は、予約プラグインを入れる、WooCommerceに予約拡張を足す、外部の予約サービスを埋め込む、カスタム投稿型と専用テーブルで自作する、の4通りです。この記事では、Booking Package・Amelia・WooCommerce Bookingsの版と決済対応を2026年10月時点の公式ページで比べ、プラグインで足りる予約と足りない予約の境目を示します。そのうえで、自作する場合のテーブル設計、二重予約をデータベース側で止めるREST API、Stripe決済と仮予約の期限切れ処理を、コピーして動かせるコードで解説します。

まとめ:WordPressの予約はプラグインで始め、同時予約と決済で自作に切り替える

枠が1種類で、事前決済が要らず、予約が月に数百件までなら、予約プラグインで十分です。無料で始めるならBooking Package、スタッフ別の予約やGoogleカレンダー連携が要るならAmeliaの有料版、物販と同じカートで予約を売るならWooCommerce Bookings(年249ドル)が候補になります。

自作に切り替える合図は3つです。人気枠に予約が同時に集中する、決済画面で離脱した客の枠を自動で戻したい、基幹システムや会員データと予約をつなぎたい。この3つは設定画面では直せず、データベースの制約と処理の順番で解く問題だからです。自作する場合は、メニューをカスタム投稿型、予約枠と予約を専用テーブルに分け、残数の判定と更新を1文のUPDATEで行います。

WordPressに予約機能を足す3つの方式と、プラグインで足りる予約の条件

自作を除くと、WordPressで予約を受ける方式は3つです。違いは、予約データをどこに置くかと、決済をどこで受けるかに出ます。

予約プラグイン・WooCommerce拡張・外部SaaS埋め込みの構成と費用差

予約プラグインとWooCommerce拡張は、予約データを自社サーバーのデータベースに保存します。外部SaaSの埋め込みは、予約画面だけをWordPressに表示し、データは事業者側のサーバーに置かれます。

方式 予約データの置き場所 決済の受け方 向く使い方
予約プラグイン 自社のDB 有料版で対応が多い 1店舗の来店予約
WooCommerce拡張 自社のDB WooCommerceの決済 物販と予約の併売
外部SaaS埋め込み SaaS事業者 SaaS側の決済 サーバー保守を持たない

外部SaaSは月額課金で、サーバーの更新や個人情報の保管をSaaS側に任せられます。製品ごとの月額と3年総額は予約システムの料金|5製品の料金比較と課金体系3タイプ、3年総額で見る開発への分岐で比べています。

プラグインで足りるのは枠が1種類、決済が任意、月数百件までの予約

プラグインが得意なのは、美容室の施術枠や教室の体験レッスンのように、1つの時間枠を1人が取る予約です。決済を来店時の現地払いにできれば、無料版の範囲でも運用は回ります。

足りなくなるのは、条件が重なったときです。人気の枠に公開直後から申込みが集まる、事前決済を必須にする、顧客台帳や在庫と予約を突き合わせる。どれか1つでも当てはまるなら、後半のプラグインで詰まる3か所を先に読んでください。

予約プラグイン3種の版・料金・決済対応、2026年10月時点の公式ページで比較

ここでは、日本語で使える配布数の多いプラグインから、無料系・予約専業・WooCommerce拡張を1つずつ選びました。数値は各公式ページを2026年10月27日に確認したものです。

3種の有効インストール数・決済・WordPress要件の比較表と読み方

項目 Booking Package Amelia WooCommerce Bookings
確認した版 1.7.34 2.4.13 3.12.1
有効インストール 1万以上 9万以上 公式に記載なし
価格 無料と有料プラン 無料版と有料版 年249ドル
無料で使える決済 なし Squareのみ 製品自体が有料
Googleカレンダー 記載なし 有料版 標準で1つ
WordPressの要件 5.0以上 6.5以上 7.0以上

Booking Packageの配布ページは、オンライン決済(StripeとPayPal)をスタンダードプランのみの機能としています。Ameliaの配布ページでは、無料版で使える決済はSquareだけで、Stripe・PayPal・複数スタッフ・Googleカレンダー連携は有料版の機能です。Ameliaの2.4.13はセキュリティ修正を含むため、すぐの更新が案内されています。両プラグインとも、WordPress 7.1.3で検証済みと表示されています。

WooCommerce Bookingsの製品ページは、スタッフ・部屋・機材をリソースとして登録し、リソースごとの空きで二重予約を防ぐと説明しています。PHP 7.4以上とWooCommerce 11.1以上が必要です。

無料版で決済できない・スタッフを増やせない、有料版へ移る分岐点

無料版で困るのは、ほぼこの2点に限られます。事前決済を受けたいとき、Booking Packageはスタンダードプラン、AmeliaはStripeを使うなら有料版が必要です。施術者や講師ごとに空き枠を分けたいときも、Ameliaは有料版の範囲になります。

有料版に移る前に、決済を予約と同時に取る必要が本当にあるかを確かめてください。無断キャンセルが月に数件なら、前日のリマインドメールで足りる場合があります。決済を足すと、返金とキャンセル規定の運用まで抱えることになるからです。

プラグインで詰まる3か所、同時予約・決済の離脱・仮予約の解放で起きること

ここからは、設定画面では解決できない問題を扱います。3つとも、データベースへの書き込みの順番が原因で起きます。

投稿メタで空き枠を判定すると、同時アクセスで二重予約が起きる仕組み

カスタム投稿型の予約を投稿メタ(wp_postmeta)に保存し、「同じ枠の予約が0件なら登録する」という順で書くと、二重予約が起きます。2人がほぼ同時に送信すると、2つの処理がどちらも「0件」を読んでから登録に進むためです。

投稿メタには、同じ枠に2件目を入れさせないUNIQUE制約を付けられません。読んでから書くまでの数十ミリ秒の隙間を、PHPのコードだけで閉じる方法はありません。予約を1件ずつ確実に止めるには、データベースの制約か、行のロックが必要になります。データベース設計の一般論は予約システム開発の進め方|自作の設計・技術選定・必須機能と費用を解説で扱っており、本記事ではWordPressでの書き方に絞ります。

決済画面で離脱した客の枠が押さえられたまま残る仮予約の扱い方

事前決済にすると、予約を「仮予約」で押さえてから決済画面へ送る流れになります。客が決済画面を閉じると、仮予約は残ったままです。人気の枠ほど、この放置された仮予約が空き枠を食います。

対策は、仮予約に期限を持たせ、期限を過ぎたら枠を戻すことです。決済完了の通知が期限後に届いた場合の扱いも、先に決めておきます。本記事では、期限切れの予約を確定させず返金する方針で実装します。

WP-Cronはアクセスが無い時間に動かず、期限切れの仮予約が残る問題

期限切れの仮予約を戻す処理を、WordPressの予約実行機能WP-Cronに任せると、深夜や早朝に止まります。公式のCronの解説は、WP-Cronはシステムのcronのように常に動くのではなく、ページの読み込みをきっかけに動くと書いています。

アクセスが少ない店舗サイトでは、朝一番の来訪者が来るまで前夜の仮予約が残ります。朝7時の枠が「満席」に見えたまま、という事態です。解放処理はサーバーのcronから決まった間隔で呼び出します。

カスタム投稿型と専用テーブルで予約を自作する、テーブル作成からAPIまで

以下のコードは、1つのプラグインファイルにまとめて置く前提です。WordPress 7.1系、MySQL 8.0系またはMariaDBのInnoDBテーブルで動かすことを想定しています。

メニューはカスタム投稿型、予約枠と予約は専用テーブルに分ける設計

施術メニューやコースの説明は、画像と本文を管理画面で編集できるカスタム投稿型に置きます。予約枠と予約は件数が増え、UNIQUE制約や条件付き更新が必要なので、保存先は専用テーブルです。テーブルは有効化時にdbDeltaで作ります。

<?php
/**
 * Plugin Name: Issoh Booking Sample
 */
register_activation_hook( __FILE__, 'issoh_booking_install' );

function issoh_booking_install() {
    global $wpdb;
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    $charset = $wpdb->get_charset_collate();
    $slots   = $wpdb->prefix . 'booking_slots';
    $books   = $wpdb->prefix . 'bookings';

    dbDelta( "CREATE TABLE $slots (
  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  menu_id bigint(20) unsigned NOT NULL,
  start_at datetime NOT NULL,
  capacity smallint(5) unsigned NOT NULL DEFAULT 1,
  booked smallint(5) unsigned NOT NULL DEFAULT 0,
  PRIMARY KEY  (id),
  UNIQUE KEY menu_start (menu_id,start_at)
) $charset;" );

    dbDelta( "CREATE TABLE $books (
  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  slot_id bigint(20) unsigned NOT NULL,
  tel varchar(20) NOT NULL,
  status varchar(20) NOT NULL DEFAULT 'pending',
  expires_at datetime NULL,
  created_at datetime NOT NULL,
  PRIMARY KEY  (id),
  KEY status_expires (status,expires_at)
) $charset;" );
}

add_action( 'init', function () {
    register_post_type( 'booking_menu', array(
        'label'        => '予約メニュー',
        'public'       => true,
        'show_in_rest' => true,
        'supports'     => array( 'title', 'editor', 'thumbnail' ),
    ) );
} );

dbDeltaは、列を1行に1つずつ書き、upgrade.phpを読み込んでから呼びます。公式ページの利用者コメントでは、外部キー制約を扱えないことと、IF NOT EXISTSを付けないことも注意されています。カスタム投稿型の引数の確認先は、register_post_typeの公式リファレンスです。menu_startのUNIQUE制約は、同じメニューの同じ時刻に枠が2つできるのを防ぎます。

残数の判定と更新を1文のUPDATEにまとめ、二重予約をDBで止めるAPI

予約APIの中心は、「残数があるときだけ予約数を1増やす」を1文のUPDATEで書くことです。判定と更新が同じ文なので、InnoDBが行を更新している間、同じ枠への別の更新は待たされます。定員を超える更新は0行で終わります。

add_action( 'rest_api_init', function () {
    register_rest_route( 'issoh-booking/v1', '/reserve', array(
        'methods'             => 'POST',
        'callback'            => 'issoh_booking_reserve',
        // 未ログインの来店客も予約するため公開。スパム対策は別途入れる
        'permission_callback' => '__return_true',
        'args'                => array(
            'slot_id' => array( 'type' => 'integer', 'required' => true ),
            'tel'     => array( 'type' => 'string', 'required' => true ),
        ),
    ) );
} );

function issoh_booking_reserve( WP_REST_Request $req ) {
    global $wpdb;
    $slot_id = (int) $req['slot_id'];

    $wpdb->query( 'START TRANSACTION' );
    // 残数があるときだけ1増やす。判定と更新が1文なので同時に来ても定員を超えない
    $updated = $wpdb->query( $wpdb->prepare(
        "UPDATE {$wpdb->prefix}booking_slots
            SET booked = booked + 1
          WHERE id = %d AND booked < capacity",
        $slot_id
    ) );
    if ( 1 !== $updated ) {
        $wpdb->query( 'ROLLBACK' );
        return new WP_Error( 'slot_full', 'この枠は満席です', array( 'status' => 409 ) );
    }

    $ok = $wpdb->insert( $wpdb->prefix . 'bookings', array(
        'slot_id'    => $slot_id,
        'tel'        => sanitize_text_field( $req['tel'] ),
        'status'     => 'pending',
        'expires_at' => gmdate( 'Y-m-d H:i:s', time() + 35 * MINUTE_IN_SECONDS ),
        'created_at' => current_time( 'mysql', true ),
    ) );
    if ( false === $ok ) {
        $wpdb->query( 'ROLLBACK' );
        return new WP_Error( 'insert_failed', '予約を保存できませんでした', array( 'status' => 500 ) );
    }
    $wpdb->query( 'COMMIT' );

    return new WP_REST_Response( array( 'booking_id' => $wpdb->insert_id ), 201 );
}

判定の根拠は戻り値です。wpdb::queryのリファレンスどおり、UPDATEでは影響した行数が返るため、1なら枠を確保でき、0なら満席です。register_rest_routeのリファレンスでは、5.5.0からpermission_callbackを省くと警告が出ると明記されています。公開するAPIでも__return_trueを明示してください。

定員1の枠へ2件を同時に送り、201と409が1件ずつ返るか確かめる手順

排他制御は、正常系の画面操作では確かめられません。定員1の枠を1つ用意し、同じ枠へ2件を同時に送ります。

# 定員1の枠(id=42)へ同時に2件送る
for i in 1 2; do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST \
    -H "Content-Type: application/json" \
    -d "{\"slot_id\":42,\"tel\":\"090000000$i\"}" \
    https://example.com/wp-json/issoh-booking/v1/reserve &
done; wait

# 予約数が定員を超えていないかを確かめる(期待値:booked=1)
wp db query "SELECT booked, capacity FROM wp_booking_slots WHERE id = 42"

期待する出力は、201が1行、409が1行です。201が2行出たら、どこかで残数を読んでから更新する処理が残っています。テーブル接頭辞をwp_から変えている場合は、SQLのテーブル名も合わせてください。

Stripe決済と仮予約を組み合わせ、期限切れの枠を自動で戻す実装の流れ

事前決済は、仮予約の作成、決済画面への送り出し、決済完了の通知での確定、期限切れの解放、の4段で組みます。

Checkout Sessionの期限30分と仮予約の期限35分をずらして持つ理由

StripeのCheckout Session作成APIでは、決済画面の有効期限expires_atを作成から30分〜24時間の範囲で指定でき、省略すると24時間です。既定のままだと、離脱した客の枠が丸1日押さえられます。

  1. 予約APIで仮予約を作り、期限を35分後にする(上のコード)
  2. Checkout Sessionをexpires_at=30分後、client_reference_id=予約IDで作る
  3. checkout.session.completedの通知で、状態がpendingの予約だけをconfirmedに更新する
  4. 更新が0行(すでに失効)なら、その決済は返金処理に回す

仮予約の期限を5分長く取るのは、決済画面の期限より先に枠を戻さないためです。逆にすると、決済できる画面が開いたまま枠だけが他の客に渡ります。submit_typeにbookを渡すと、ボタンに表示されるのは予約向けの表記です。通知の種類はStripeのイベント種別一覧で確認できます。

期限切れの仮予約をシステムcronとWP-CLIで5分ごとに解放する設定

解放処理は、1件ずつ「pendingのままの行だけを失効させ、失効できたら枠を1つ戻す」順で書きます。決済完了の通知と同時に走っても、同じ予約を確定と失効の両方にしないためです。

function issoh_booking_release_expired() {
    global $wpdb;
    $b = $wpdb->prefix . 'bookings';
    $s = $wpdb->prefix . 'booking_slots';
    $rows = $wpdb->get_results( $wpdb->prepare(
        "SELECT id, slot_id FROM $b WHERE status = 'pending' AND expires_at < %s LIMIT 200",
        gmdate( 'Y-m-d H:i:s' )
    ) );
    foreach ( $rows as $r ) {
        $wpdb->query( 'START TRANSACTION' );
        $hit = $wpdb->query( $wpdb->prepare(
            "UPDATE $b SET status = 'expired' WHERE id = %d AND status = 'pending'", $r->id
        ) );
        if ( 1 === $hit ) {
            $wpdb->query( $wpdb->prepare(
                "UPDATE $s SET booked = booked - 1 WHERE id = %d AND booked > 0", $r->slot_id
            ) );
        }
        $wpdb->query( 'COMMIT' );
    }
}

// wp-config.php:アクセス起点のWP-Cronを止める
define( 'DISABLE_WP_CRON', true );

# crontab:5分ごとに解放処理と、WP-Cronの予定を実行する
*/5 * * * * cd /var/www/html && wp eval 'issoh_booking_release_expired();' --quiet
*/5 * * * * cd /var/www/html && wp cron event run --due-now --quiet

DISABLE_WP_CRONの書き方とシステムcronへの置き換えは、公式のHooking WP-Cron Into the System Task Schedulerに沿っています。wp evalはWordPressを読み込んだ状態でPHPを実行するため、プラグインの関数をそのまま呼べます。WP-Cronを止めたら、他のプラグインの予定も2行目で動かすのを忘れないでください。

プラグインのまま運用するか、自作か受託開発へ移るかを決める判断基準

自作のコードは、動かしてからの保守が本番です。WordPress本体とPHPの更新、決済の通知の失敗、予約データの個人情報保護を、誰が見るかまで決めてから選びます。

プラグインのままでよい条件と、自作に手を出さず止めるべき場面

次のどれかに当てはまるなら、自作はしません。予約が1日数件で同時申込みが起きない。事前決済が無い。WordPressを更新する担当者が社内にいない。この場合、上のコードを入れても、PHPやWordPressの更新で動かなくなったときに直す人がいません。

プラグインを複数組み合わせて足りない機能を埋めるのも避けます。予約・会員・決済を別々のプラグインでつなぐと、更新のたびに必要なのは、プラグイン間の互換性の確認です。会員サイトで同じ問題が起きる経緯は、WordPressで会員サイトを作る方法|プラグイン4種の比較と限界、受託開発へ移る判断にまとめています。更新と監視を外部に任せる場合の範囲は、WordPress保守運用とは:作業範囲・費用相場と内製か外注かの判断基準を解説が参考になります。

外部連携と予約件数で受託開発へ移る目安と、予約データの移し方

受託開発を検討するのは、予約を基幹システムや顧客台帳、LINEと双方向でつなぐときです。人気枠の公開時に数百件の申込みが同時に来る場合も、受託開発を検討する対象です。WordPressのままでも作れますが、テストと保守の量が予約プラグインの比ではなくなります。WordPressの表示はそのまま残し、予約部分だけを別のアプリケーションに切り出す構成も選択肢の1つです。要件の整理から相談したい場合は、一創の予約管理システム開発で、既存サイトを残した構成も含めて提案しています。

予約データを移すときは、未来の予約だけを先に移し、過去の予約は顧客台帳の履歴として別に取り込みます。移行日の前後で二重に受けないよう、旧システムの受付を止める時刻を決めてから切り替えます。

よくある質問

WordPressで予約システムを作るときに、検討段階でよく出る質問をまとめました。

WordPressの予約システムは無料で作れますか?

現地払いの来店予約なら、Booking PackageやAmeliaの無料版で作れます。どちらもWordPress 7.1.3で検証済みと表示されています。費用がかかるのは、事前決済やスタッフ別の空き枠、Googleカレンダー連携が必要になった時点です。Ameliaの無料版で使える決済はSquareだけで、Stripeを使うなら有料版が必要になります。

WordPressの予約プラグインで二重予約は防げますか?

通常の利用では、各プラグインが空き枠を確認してから登録するため防げます。ただし、人気枠に申込みが同時に集中する場面での動きは、プラグインごとに実装が異なります。同時申込みが想定されるなら、検証用のサイトで同じ枠へ2件を同時に送り、2件とも通らないかを試してください。手順は本文のcurlの例が使えます。

予約システムを作るのにWooCommerceは必要ですか?

必要ありません。WooCommerceが要るのは、WooCommerce Bookingsを使う場合と、物販と予約を同じカートで売りたい場合です。WooCommerce BookingsはWordPress 7.0以上とWooCommerce 11.1以上が条件で、年249ドルかかります。予約だけならBooking PackageやAmeliaの方が導入の手数が少なく済みます。

予約プラグインとGoogleカレンダーは連携できますか?

プラグインによります。AmeliaでGoogleカレンダーとOutlookカレンダーとの連携に対応するのは、有料版です。WooCommerce Bookingsは標準で1つのGoogleカレンダーと同期し、公式ページでは60秒以内に反映すると説明しています。Booking Packageの配布ページにはGoogleカレンダー連携の記載がありません。

予約フォームで集めた個人情報を守るには何をすればよいですか?

予約データには氏名と電話番号が入るため、WordPress本体とプラグインの更新を止めないことが前提です。管理画面のログイン保護、不要なプラグインの削除、改ざんの確認は、WordPressセキュリティ対策:攻撃面ごとの設定とWP-CLIでの確認手順でコマンド付きで解説しています。予約を保存しない外部SaaSの埋め込みにする選択肢もあります。

関連記事

お気に入りに入れた記事の一覧

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.23 テックブログ Renta!はいつ再開?不正アクセスでメールアドレス等が流出したおそれ・再開時のパスワード再設定と運営側の対応
  2. 2026.07.09 テックブログ Muse Imageとは?Metaの画像生成AIの使い方・API料金(1枚0.01ドル)と日本での提供状況【2026年10月】
  3. 2026.10.23 テックブログ Splunk 脆弱性CVE-2026-76299:影響版の確認と10.2.8などへの更新手順
  4. 2026.10.22 テックブログ Tomcatのバージョン一覧とサポート期限【2026年10月】最新版11.0.27、9.0系は2027年3月末終了・9.1系は2030年末まで
  5. 2026.10.21 テックブログ ユミルリンク不正アクセス第二報で延べ5,162万件:Cuenote FC利用企業と受信者の確認点

RELATED POSTS 関連記事

目次