Files
2meet-data-optimizer-infoca…/includes/class-tmdo-infocards.php
wpdev cf47f141c5
Tests / PHP Lint (pull_request) Successful in 12s
fix: 修復 partner 偵測時序 bug(AddOn 從未生效)+ IG meta_key 錯配
TMDO_Infocards::register() 舊版在 bootstrap 當下以 class_exists('TMEETIC_*')
偵測 partner,失敗即 early return。但本 AddOn bootstrap 掛在 plugins_loaded:6,
而 2meet-infocards 要到 :20(tmeetic_run)才 require 自身類別——在 :6 當下
TMEETIC_* 一律不存在,偵測必定失敗,兩個註冊 hook 從未被掛上,整個 AddOn
等同未啟用。

三處修法:
1. 偵測從 bootstrap 移進 hook callback 內執行,hook 一律先掛上
2. 偵測改以 TMEETIC_VERSION / TMEETIC_PLUGIN_DIR 常數優先(檔案解析當下即
   define,早於 plugins_loaded),class_exists() 退為後備
3. bootstrap priority 6 → 3;wpdo_register_custom_tables callback prio 設 5,
   讓本 AddOn 較豐富的定義勝過主外掛無 expected_columns 的版本

同時修正 IG token 兩個欄位的 meta_key 錯配(tmeetic_ig_access_token_enc /
tmeetic_ig_token_expiry),以及 registry 索引 config key expected_indexes →
indexes。

實測驗證:
- wpdo_register_fields 掛載 prio 10、wpdo_register_custom_tables prio 5
- 2meet_vendor_profiles 由本 AddOn 註冊(16 欄位 / 1 索引 / doctor_callback)
- 缺 expected_columns 由 26/29 降為 0/46
- wp wpdo doctor 60 OK / 0 FAIL;anti-EAV lint 通過;站台 200/200
2026-08-08 18:47:23 +08:00

308 lines
11 KiB
PHP
Raw Permalink 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_Infocards — integration with 2meet-infocards plugin (PR-8 / Wave 1).
*
* Solves audit finding R-1 (infocards integration layer missing).
*
* Responsibilities:
* - Register tmeetic_profile_* (vendor primary fields) into Hot zone
* - Register tmeetic_ig_* (Instagram tokens etc.) into Cold zone
* - Register wp_2meet_vendor_profiles custom table to Custom_Table_Registry
* - Provide cache loader for inject_profile_styles() so it doesn't query DB
* on every wp_head call
*
* NOTE: 2meet-infocards 主檔(2meet-infocards.php)自 v6.9.24 起也直接掛
* wpdo_register_fields / wpdo_register_custom_tablesbelt-and-suspenders
* 繞過舊版 wp-data-optimizer 曾有的時序偵測 bug)。兩邊重複註冊同一批欄位是
* 刻意的防禦性冗餘,Schema Registry 對同一 meta_key 重複 register 是 idempotent
* 操作,不會造成資料損壞;但若未來要精簡,應二擇一保留,勿只改其中一邊的 key 名稱。
*
* @package TMDO
* @since 2.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Integration with 2meet-infocards. No-op when the partner plugin isn't loaded.
*/
final class TMDO_Infocards {
/**
* Static registration entry. Called from the AddOn bootstrap on plugins_loaded:6.
*
* 時序修正:偵測**不可**在此處做。本 AddOn bootstrap 於 plugins_loaded:6 執行,
* 但 2meet-infocards 要到 plugins_loaded:20`tmeetic_run`)才 require 自身類別,
* 因此在 :6 當下 TMEETIC_* 一律不存在 → 舊版在此早退,導致整個 AddOn 從未掛上
* 任何 hook(實測:TMEETIC_Frontend 在 prio 6 = MISSING、prio 21 = EXISTS)。
*
* 核心於 plugins_loaded:30 有 late-bind safety net 會重新 fire 註冊 hook
* 所以改為「hook 一律掛上、偵測延後到 callback 執行當下」才會正確生效。
*
* priority 5(而非預設 10)的理由:Custom_Table_Registry 對同一
* `provider:table_name` 是「先註冊者勝、後者回傳 false」。主外掛
* `2meet-infocards.php` 的 closure 在**檔案解析當下**就掛上 prio 10,早於本
* AddOn 在 plugins_loaded:6 掛載,同 prio 內先掛先跑 → 主外掛會先以「無
* expected_columns」的版本佔走 key。本 AddOn 的定義較豐富(含 expected_columns
* / indexes / doctor_callback),故提前到 prio 5 讓它勝出;主外掛其餘 26 張表
* 不受影響,仍由它自己註冊。
*/
public static function register(): void {
add_action( 'wpdo_register_fields', array( __CLASS__, 'register_post_fields' ) );
add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ), 5 );
}
/**
* Detect the partner plugin.
*
* 以常數優先、類別為輔:`TMEETIC_VERSION` / `TMEETIC_PLUGIN_DIR` 在
* `2meet-infocards.php` 的**檔案解析當下**(第 2533 行,不在任何 function 內)
* 就已 define,早於 `plugins_loaded`;而 TMEETIC_* 類別要到 plugins_loaded:20
* 才 require。核心的 `fire_registration()` 最早在 plugins_loaded:4 觸發,
* 該時點只有常數可用——只靠 class_exists() 會在最關鍵的第一次註冊時誤判為
* 「partner 不存在」。
*/
private static function partner_detected(): bool {
return defined( 'TMEETIC_VERSION' )
|| defined( 'TMEETIC_PLUGIN_DIR' )
|| class_exists( 'TMEETIC_Plugin' )
|| class_exists( 'TMEETIC_Frontend' )
|| class_exists( '\\TwoMeet_InfoCards\\Plugin' );
}
/**
* Register hp_vendor meta keys owned by 2meet-infocards.
*
* @param TMDO_Schema_Registry $registry Singleton instance.
* @return void
*/
public static function register_post_fields( TMDO_Schema_Registry $registry ): void {
if ( ! self::partner_detected() ) {
return;
}
$registry->register_many(
'2meet-infocards',
array(
// Vendor profile colour fields → Hot zone (varchar columns).
array(
'post_type' => 'hp_vendor',
'meta_key' => 'tmeetic_profile_primary',
'zone' => 'hot',
'data_type' => "varchar(20) NOT NULL DEFAULT ''",
'column' => 'tmeetic_profile_primary',
),
array(
'post_type' => 'hp_vendor',
'meta_key' => 'tmeetic_profile_accent',
'zone' => 'hot',
'data_type' => "varchar(20) NOT NULL DEFAULT ''",
'column' => 'tmeetic_profile_accent',
),
array(
'post_type' => 'hp_vendor',
'meta_key' => 'tmeetic_profile_font',
'zone' => 'hot',
'data_type' => "varchar(50) NOT NULL DEFAULT ''",
'column' => 'tmeetic_profile_font',
),
// Hero image is descriptive → Cold zone.
array(
'post_type' => 'hp_vendor',
'meta_key' => 'tmeetic_profile_hero_image',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_hp_vendor',
'cache_ttl' => HOUR_IN_SECONDS,
),
// IG tokens — sensitive + display-only → Cold zone.
// 註:app id / secret 不在此列——那是站台層級的 wp_options
// (`2meetic_ig_app_id` / `2meetic_ig_app_secret_enc`),非 hp_vendor
// postmeta。舊版誤註冊的兩個 postmeta 欄位全外掛零讀寫,已移除。
array(
'post_type' => 'hp_vendor',
'meta_key' => 'tmeetic_ig_access_token_enc',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_hp_vendor',
'cache_ttl' => HOUR_IN_SECONDS,
),
array(
'post_type' => 'hp_vendor',
'meta_key' => 'tmeetic_ig_token_expiry',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_hp_vendor',
'cache_ttl' => HOUR_IN_SECONDS,
),
array(
'post_type' => 'hp_vendor',
'meta_key' => 'tmeetic_ig_user_id',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_hp_vendor',
'cache_ttl' => HOUR_IN_SECONDS,
),
array(
'post_type' => 'hp_vendor',
'meta_key' => 'tmeetic_ig_username',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_hp_vendor',
'cache_ttl' => HOUR_IN_SECONDS,
),
array(
'post_type' => 'hp_vendor',
'meta_key' => 'tmeetic_ig_feed_show',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_hp_vendor',
'cache_ttl' => HOUR_IN_SECONDS,
),
)
);
}
/**
* Register infocards custom tables so they appear in `wp wpdo doctor`,
* `benchmark`, and `cleanup` tooling.
*
* @param TMDO_Custom_Table_Registry $registry Custom Table Registry singleton.
* @return void
*/
public static function register_custom_tables( TMDO_Custom_Table_Registry $registry ): void {
if ( ! self::partner_detected() ) {
return;
}
// CSS variable source — checked by WPDO doctor for index health.
$registry->register(
'2meet-infocards',
array(
'table_name' => '2meet_vendor_profiles',
'primary_key' => 'vendor_id',
'post_type_link' => 'hp_vendor',
'doctor_callback' => array( __CLASS__, 'doctor_vendor_profiles' ),
'expected_columns' => array(
'id' => 'BIGINT',
'vendor_id' => 'BIGINT',
'primary_color' => 'VARCHAR',
'accent_color' => 'VARCHAR',
'bg_color' => 'VARCHAR',
'text_color' => 'VARCHAR',
'muted_color' => 'VARCHAR',
'font' => 'VARCHAR',
'radius_card' => 'INT',
'radius_btn' => 'INT',
'shadow_style' => 'VARCHAR',
'hero_image_id' => 'BIGINT',
'hero_fullwidth' => 'TINYINT',
'ig_feed_show' => 'TINYINT',
'theme_style_id' => 'BIGINT',
'updated_at' => 'DATETIME',
),
'indexes' => array(
'vendor_id' => array( 'vendor_id' ),
),
)
);
$registry->register(
'2meet-infocards',
array(
'table_name' => '2meet_cards',
'primary_key' => 'id',
'post_type_link' => 'hp_vendor',
'expected_columns' => array(
'id' => 'BIGINT',
'page_id' => 'BIGINT',
'story_id' => 'BIGINT',
'epk_id' => 'BIGINT',
'card_type' => 'VARCHAR',
'title' => 'VARCHAR',
'description' => 'TEXT',
'image_id' => 'BIGINT',
'config' => 'TEXT',
'cta_text' => 'VARCHAR',
'cta_url' => 'VARCHAR',
'button_bg_color' => 'VARCHAR',
'button_text_color' => 'VARCHAR',
'sort_order' => 'INT',
'status' => 'VARCHAR',
'start_at' => 'DATETIME',
'end_at' => 'DATETIME',
'vendor_id' => 'BIGINT',
'show_click_count' => 'TINYINT',
'ab_enabled' => 'TINYINT',
'liff_enabled' => 'TINYINT',
'created_at' => 'DATETIME',
'updated_at' => 'DATETIME',
'click_count' => 'BIGINT',
'last_click_at' => 'DATETIME',
),
'indexes' => array(
'page_id_status_sort' => array( 'page_id', 'status', 'sort_order' ),
'story_id_status_sort' => array( 'story_id', 'status', 'sort_order' ),
'epk_id_status_sort' => array( 'epk_id', 'status', 'sort_order' ),
'card_type' => array( 'card_type' ),
'vendor_status_sort' => array( 'vendor_id', 'status', 'sort_order' ),
'status_schedule' => array( 'status', 'start_at', 'end_at' ),
),
)
);
$registry->register(
'2meet-infocards',
array(
'table_name' => '2meet_theme_styles',
'primary_key' => 'id',
'post_type_link' => null,
'expected_columns' => array(
'id' => 'BIGINT',
'name' => 'VARCHAR',
'slug' => 'VARCHAR',
'config' => 'TEXT',
'sort_order' => 'INT',
'is_active' => 'TINYINT',
'created_at' => 'DATETIME',
'updated_at' => 'DATETIME',
),
'indexes' => array(
'slug' => array( 'slug' ),
),
)
);
}
/**
* Doctor check: verifies row count + sample read latency on vendor_profiles.
*
* @return array{ok:bool, message:string}
*/
public static function doctor_vendor_profiles(): array {
global $wpdb;
try {
$exists = (bool) $wpdb->get_var(
$wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->prefix . '2meet_vendor_profiles' )
);
if ( ! $exists ) {
return array(
'ok' => false,
'message' => 'wp_2meet_vendor_profiles table missing',
);
}
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->prefix}2meet_vendor_profiles`" );
return array(
'ok' => true,
'message' => "wp_2meet_vendor_profiles: {$count} rows",
);
} catch ( \Throwable $e ) {
return array(
'ok' => false,
'message' => 'doctor failed: ' . $e->getMessage(),
);
}
}
/**
* Cache key for a vendor profile blob (used by inject_profile_styles).
*
* @param int $vendor_id Vendor post ID.
*/
public static function vendor_profile_cache_key( int $vendor_id ): string {
return 'wpdo_vendor_profile_' . $vendor_id;
}
}