Files
2meet-data-optimizer-bookin…/includes/class-tmdo-bookings.php
T
wpdev bb71e38408
Tests / PHP Lint (pull_request) Successful in 6s
fix: partner 偵測符號對齊實際存在的常數 + schema 期望值補齊
TMDO_Bookings::register() 舊版偵測 TMEETIC_Booking_Plugin 類別與 TMEETIC_BOOKING_VERSION 常數(TMEETIC_Booking 則要到 :26 才載入),
在 partner 外掛中並不存在(或載入太晚),偵測實際完全靠 file_exists() 後備
撐著——程式碼讀起來像在驗證「partner 已載入」,實際只驗證了「目錄存在」。

改用 TMB_VERSION / TMB_PLUGIN_DIR,兩者在 partner 主檔的檔案解析當下即
define,早於 plugins_loaded,故本 AddOn bootstrap 的 :6 時點確實可用。
file_exists() 後備保留不動,行為不變;但偵測不再是死碼,即使將來移除後備也
不會重演 infocards-addon 那種「整個 AddOn 靜默失效」的事故。

同一版本另含先前未提交的 schema 工作:補齊 expected_columns / indexes,
並將 registry 索引 config key 由 expected_indexes 更正為 indexes
(wp_parse_args() 會靜默丟棄未知 key,索引期望值先前等同從未送達)。

實測驗證(mu-plugin 於 plugins_loaded:6 量測真實請求):
新常數 AVAILABLE;wp wpdo doctor 89 OK / 0 FAIL;表註冊欄位與索引 100% 完整。
2026-08-09 00:47:26 +08:00

346 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* TMDO_Bookings — integration with 2meet-bookings (Wave 2 / v2.1.0).
*
* 2meet-bookings uses 7 custom tables (wp_2mb_*) — fully escaped from
* wp_postmeta — so this integration's job is purely to register those tables
* with TMDO_Custom_Table_Registry so they appear in:
* - `wp wpdo doctor` health checks
* - `wp wpdo benchmark --custom-tables`
* - Site monitor metrics
*
* No postmeta interception is needed for bookings.
*
* @package WP_Data_Optimizer
* @since 2.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable Squiz.Commenting.FunctionComment.Missing,Squiz.Commenting.InlineComment.InvalidEndChar,Generic.Commenting.DocComment.MissingShort,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.PHP.YodaConditions,Generic.CodeAnalysis.EmptyStatement -- v2.0.0 partner integrations: pure registration helpers + intentional silent catches.
/**
* 2meet-bookings integration. No-op when partner plugin not loaded.
*/
final class TMDO_Bookings {
/**
* 偵測以常數優先:`TMB_VERSION` / `TMB_PLUGIN_DIR` 在 2meet-bookings 主檔的
* **檔案解析當下**(第 22/25 行)即 define,早於 `plugins_loaded`;本 AddOn
* bootstrap 掛在 `plugins_loaded:6`,故此時常數已可用。
*
* 舊版偵測的 `TMEETIC_Booking_Plugin` 類別與 `TMEETIC_BOOKING_VERSION` 常數在
* 2meet-bookings 中**皆不存在**`TMEETIC_Booking` 雖存在,但要到 `plugins_loaded:26`
* `tmb_run`)才 require,在 :6 當下必定 MISSING。偵測實際完全靠下方的
* `file_exists()` 撐著。保留 file_exists 作最終後備,但常數檢查現在真的有效——
* 即使將來移除後備也不會像 infocards 那樣整個 AddOn 靜默失效。
*/
public static function register(): void {
// Detect via constants defined at partner file-parse time (class loads too late at :6).
$detected = defined( 'TMB_VERSION' )
|| defined( 'TMB_PLUGIN_DIR' )
|| class_exists( 'TMEETIC_Booking' );
if ( ! $detected ) {
// Also detect via plugin file existence (loose — partner plugin may rename main class).
if ( ! file_exists( WP_PLUGIN_DIR . '/2meet-bookings/2meet-bookings.php' ) ) {
return;
}
}
add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ) );
}
/**
* Register the 8 wp_2mb_* tables so wp wpdo doctor/benchmark know about them.
*
* V2.1.5: added wp_2mb_audit_log (was 7 tables, now 8 — matches plugin's
* TablePrefix header declaration in 2meet-bookings.php).
*
* expected_columns/expected_indexes (this audit) are transcribed verbatim
* from the CREATE TABLE statements in
* 2meet-bookings/includes/class-2meetic-bookings-activator.php::create_tables()
* so `wp wpdo doctor`/`benchmark` can do schema drift detection.
*
* @param TMDO_Custom_Table_Registry $registry Singleton instance.
* @return void
*/
public static function register_custom_tables( TMDO_Custom_Table_Registry $registry ): void {
$tables = array(
'2mb_services' => array(
'primary_key' => 'id',
'expected_columns' => array(
'id' => 'BIGINT',
'vendor_id' => 'BIGINT',
'title' => 'VARCHAR',
'slug' => 'VARCHAR',
'description' => 'TEXT',
'category' => 'VARCHAR',
'tags' => 'VARCHAR',
'duration_min' => 'INT',
'buffer_min' => 'INT',
'capacity_max' => 'TINYINT',
'service_fee' => 'INT',
'deposit_amount' => 'INT',
'cover_image_url' => 'VARCHAR',
'location_type' => 'VARCHAR',
'location_county' => 'VARCHAR',
'location_address' => 'VARCHAR',
'location_url' => 'VARCHAR',
'online_url' => 'VARCHAR',
'cancellation_hours' => 'INT',
'advance_booking_days' => 'INT',
'min_booking_hours' => 'INT',
'confirmation_mode' => 'VARCHAR',
'questionnaire_json' => 'TEXT',
'notes' => 'TEXT',
'target_audience' => 'VARCHAR',
'payment_cash' => 'TINYINT',
'payment_bank_info' => 'VARCHAR',
'payment_note' => 'TEXT',
'status' => 'VARCHAR',
'admin_note' => 'TEXT',
'community_type' => 'VARCHAR',
'community_url' => 'VARCHAR',
'waitlist_enabled' => 'TINYINT',
'waitlist_max' => 'INT',
'questionnaire_form_id' => 'BIGINT',
'booking_count' => 'INT',
'avg_rating' => 'DECIMAL',
'created_at' => 'DATETIME',
'updated_at' => 'DATETIME',
),
'expected_indexes' => array(
'slug' => array( 'slug' ),
'vendor_id_status' => array( 'vendor_id', 'status' ),
'category' => array( 'category' ),
'location_county' => array( 'location_county' ),
'status' => array( 'status' ),
),
),
'2mb_availability' => array(
'primary_key' => 'id',
'expected_columns' => array(
'id' => 'BIGINT',
'vendor_id' => 'BIGINT',
'service_id' => 'BIGINT',
'day_of_week' => 'TINYINT',
'start_time' => 'VARCHAR',
'end_time' => 'VARCHAR',
'is_active' => 'TINYINT',
'created_at' => 'DATETIME',
'updated_at' => 'DATETIME',
),
'expected_indexes' => array(
'vendor_id_service' => array( 'vendor_id', 'service_id' ),
'day_of_week' => array( 'day_of_week' ),
),
),
'2mb_date_overrides' => array(
'primary_key' => 'id',
'expected_columns' => array(
'id' => 'BIGINT',
'vendor_id' => 'BIGINT',
'service_id' => 'BIGINT',
'override_date' => 'VARCHAR',
'override_type' => 'VARCHAR',
'override_source' => 'VARCHAR',
'start_time' => 'VARCHAR',
'end_time' => 'VARCHAR',
'note' => 'VARCHAR',
'created_at' => 'DATETIME',
),
'expected_indexes' => array(
'vendor_date' => array( 'vendor_id', 'override_date' ),
'service_date' => array( 'service_id', 'override_date' ),
'source_date' => array( 'vendor_id', 'override_source', 'override_date' ),
),
),
'2mb_bookings' => array(
'primary_key' => 'id',
'expected_columns' => array(
'id' => 'BIGINT',
'service_id' => 'BIGINT',
'vendor_id' => 'BIGINT',
'wp_user_id' => 'BIGINT',
'customer_name' => 'VARCHAR',
'customer_email' => 'VARCHAR',
'customer_phone' => 'VARCHAR',
'line_id' => 'VARCHAR',
'booking_date' => 'VARCHAR',
'start_time' => 'VARCHAR',
'end_time' => 'VARCHAR',
'headcount' => 'TINYINT',
'custom_note' => 'TEXT',
'booking_status' => 'VARCHAR',
'payment_status' => 'VARCHAR',
'payment_note' => 'VARCHAR',
'payment_gateway' => 'VARCHAR',
'payment_transaction_id' => 'VARCHAR',
'payment_amount' => 'DECIMAL',
'paid_at' => 'DATETIME',
'vendor_note' => 'TEXT',
'confirmation_token' => 'VARCHAR',
'google_event_id' => 'VARCHAR',
'confirmed_at' => 'DATETIME',
'cancelled_at' => 'DATETIME',
'cancel_reason' => 'VARCHAR',
'cancelled_by' => 'VARCHAR',
'rescheduled_from' => 'BIGINT',
'questionnaire_answers' => 'TEXT',
'group_label' => 'VARCHAR',
'completed_at' => 'DATETIME',
'order_id' => 'BIGINT',
'commission_rate' => 'DECIMAL',
'platform_fee' => 'INT',
'vendor_payout' => 'INT',
'payout_status' => 'VARCHAR',
'payout_note' => 'TEXT',
'settled_at' => 'DATETIME',
'waitlist_position' => 'INT',
'eligibility_tier' => 'VARCHAR',
'questionnaire_status' => 'VARCHAR',
'review_reminder_sent_at' => 'DATETIME',
'created_at' => 'DATETIME',
'updated_at' => 'DATETIME',
),
'expected_indexes' => array(
'confirmation_token' => array( 'confirmation_token' ),
'service_id' => array( 'service_id' ),
'vendor_id' => array( 'vendor_id' ),
'wp_user_id' => array( 'wp_user_id' ),
'customer_email' => array( 'customer_email' ),
'booking_status' => array( 'booking_status' ),
'booking_date' => array( 'booking_date' ),
'payment_status' => array( 'payment_status' ),
'payout_status' => array( 'payout_status' ),
'service_date_status' => array( 'service_id', 'booking_date', 'booking_status' ),
),
),
'2mb_vendor_settings' => array(
'primary_key' => 'id',
'expected_columns' => array(
'id' => 'BIGINT',
'vendor_id' => 'BIGINT',
'payment_linepay_url' => 'VARCHAR',
'payment_cash_default' => 'TINYINT',
'payment_bank_default' => 'VARCHAR',
'payment_note_default' => 'TEXT',
'ical_token' => 'VARCHAR',
'line_notify_token' => 'VARCHAR',
'line_notify_enabled' => 'TINYINT',
'default_buffer_min' => 'INT',
'default_cancellation_hours' => 'INT',
'google_tokens' => 'TEXT',
'gcal_watch_channel_id' => 'VARCHAR',
'gcal_watch_resource_id' => 'VARCHAR',
'gcal_watch_expiration' => 'BIGINT',
'gcal_watch_token' => 'VARCHAR',
'telegram_bot_token' => 'VARCHAR',
'telegram_chat_id' => 'VARCHAR',
'slack_webhook_url' => 'VARCHAR',
'created_at' => 'DATETIME',
'updated_at' => 'DATETIME',
),
'expected_indexes' => array(
'vendor_id' => array( 'vendor_id' ),
'gcal_watch_expiration' => array( 'gcal_watch_expiration' ),
'gcal_watch_channel_id' => array( 'gcal_watch_channel_id' ),
),
),
'2mb_service_reviews' => array(
'primary_key' => 'id',
'expected_columns' => array(
'id' => 'BIGINT',
'service_id' => 'BIGINT',
'booking_id' => 'BIGINT',
'vendor_id' => 'BIGINT',
'customer_name' => 'VARCHAR',
'rating' => 'TINYINT',
'review_text' => 'TEXT',
'vendor_reply' => 'TEXT',
'status' => 'VARCHAR',
'created_at' => 'DATETIME',
'updated_at' => 'DATETIME',
),
'expected_indexes' => array(
'booking_id' => array( 'booking_id' ),
'service_id_status' => array( 'service_id', 'status' ),
'vendor_id' => array( 'vendor_id' ),
),
),
'2mb_waitlist_submissions' => array(
'primary_key' => 'id',
'expected_columns' => array(
'id' => 'BIGINT',
'booking_id' => 'BIGINT',
'fluent_entry_id' => 'BIGINT',
'answers_snapshot' => 'TEXT',
'submitted_at' => 'DATETIME',
'review_status' => 'VARCHAR',
'reviewed_by_vendor' => 'BIGINT',
'reviewed_at_vendor' => 'DATETIME',
'vendor_notes' => 'TEXT',
'reviewed_by_admin' => 'BIGINT',
'reviewed_at_admin' => 'DATETIME',
'admin_notes' => 'TEXT',
'revision_count' => 'INT',
),
'expected_indexes' => array(
'idx_booking_id' => array( 'booking_id' ),
'idx_fluent_entry' => array( 'fluent_entry_id' ),
'review_status' => array( 'review_status' ),
),
),
'2mb_audit_log' => array(
'primary_key' => 'id', // v2.1.5: missing previously
'expected_columns' => array(
'id' => 'BIGINT',
'level' => 'VARCHAR',
'event' => 'VARCHAR',
'message' => 'VARCHAR',
'context' => 'TEXT',
'user_id' => 'BIGINT',
'ip' => 'VARCHAR',
'created_at' => 'DATETIME',
),
'expected_indexes' => array(
'level_created' => array( 'level', 'created_at' ),
'event_created' => array( 'event', 'created_at' ),
'user_id' => array( 'user_id' ),
),
),
);
foreach ( $tables as $table_name => $definition ) {
$registry->register(
'2meet-bookings',
array(
'table_name' => $table_name,
'primary_key' => $definition['primary_key'],
'post_type_link' => null,
'doctor_callback' => array( __CLASS__, 'doctor_table_factory' ),
'expected_columns' => $definition['expected_columns'],
'indexes' => $definition['expected_indexes'],
)
);
}
}
/**
* Generic doctor check — verifies table exists + reports row count.
*
* @return array{ok:bool, message:string}
*/
public static function doctor_table_factory(): array {
// Stateless probe — defers to TMDO_Bookings::doctor_check() which knows
// the calling table from the registry context. For v2.1.0 we just confirm
// reachability; deep schema diff arrives in a follow-up.
return array(
'ok' => true,
'message' => '2meet-bookings table registered',
);
}
}