d6c1540444
- Crypto::is_key_derivable()(A v3.3.2):AUTH_KEY / SECURE_AUTH_SALT 皆缺
時回 false,讓金鑰推導優雅短路
- wp tmdo doctor 新增兩項健檢:
· backup 目錄 HTTP 可及性探測(200 = 紅旗並附 nginx 設定建議,
403/404 = OK,0 = 離線時退回檢查 .htaccess)(A v3.1.9)
· crypto 金鑰可推導性(缺常數時警告 notifier secrets 會以明文儲存)
- doctor_callback 呼叫簽章 3 參數 → 1 參數(A v3.0.3 修復)。**ABI 變更**:
AddOn 註冊的 callback 若依賴 rows / full_name 需自行調整
- Member_Fields::register_admin_prefs_group() 由 7 欄擴到 22 欄(A v3.1.4),
補上 wp_user_level / show_welcome_panel / wp_persisted_preferences /
wp_user-settings / community-events-location 等 15 個 WP 原生 usermeta,
這些先前全部落在 wp_usermeta
- capture_before_value 預設 true → false(A v3.1.5):每次受管寫入省一次
DB read。**行為變更**:需要 value_before 的消費者(audit log)要
add_filter( 'wpdo_capture_before_value', '__return_true' ) 明確開啟
unit 451 / integration 398 GREEN
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
657 lines
21 KiB
PHP
657 lines
21 KiB
PHP
<?php
|
||
/**
|
||
* TMDO_Hook_Bus - 統一 Hook 攔截匯流排
|
||
*
|
||
* 所有 WordPress 的 meta 操作皆通過此匯流排:
|
||
* - {type}_metadata 系列 filter(add/get/update/delete)
|
||
* - 實體刪除 action
|
||
* - 原生查詢擴充 hook
|
||
*
|
||
* @package WP_Data_Optimizer
|
||
*/
|
||
|
||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||
|
||
|
||
defined( 'ABSPATH' ) || exit;
|
||
|
||
final class TMDO_Hook_Bus {
|
||
|
||
/** 標記:防止在內部 UPSERT 時遞迴觸發 filter */
|
||
private static array $internal_ops = array();
|
||
|
||
public static function init(): void {
|
||
|
||
// 取得所有已註冊的適配器
|
||
$adapters = TMDO_Entity_Registry::get_all_adapters();
|
||
|
||
foreach ( $adapters as $type => $adapter ) {
|
||
self::register_hooks_for_type( $type, $adapter );
|
||
}
|
||
}
|
||
|
||
private static function register_hooks_for_type( string $type, TMDO_Entity_Adapter_Interface $adapter ): void {
|
||
|
||
// ── 寫入攔截 ──────────────────────────────────────────
|
||
add_filter( "update_{$type}_metadata", array( self::class, 'intercept_update' ), 10, 5 );
|
||
add_filter( "add_{$type}_metadata", array( self::class, 'intercept_add' ), 10, 5 );
|
||
|
||
// ── 讀取攔截 ──────────────────────────────────────────
|
||
add_filter( "get_{$type}_metadata", array( self::class, 'intercept_get' ), 10, 5 );
|
||
|
||
// ── 刪除攔截 ──────────────────────────────────────────
|
||
add_filter( "delete_{$type}_metadata", array( self::class, 'intercept_delete' ), 10, 5 );
|
||
|
||
// ── 實體刪除時自動清理 ───────────────────────────────
|
||
add_action(
|
||
$adapter->get_delete_hook(),
|
||
function ( $entity_id ) use ( $type, $adapter ) {
|
||
self::cleanup_entity( $type, (int) $entity_id );
|
||
},
|
||
10,
|
||
1
|
||
);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 寫入:update_{type}_metadata filter
|
||
// ─────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 短路 WordPress 原生 update_metadata() 流程
|
||
*
|
||
* @param null|bool $check 若回傳 null 則 WP 繼續原生流程
|
||
* @param int $object_id
|
||
* @param string $meta_key
|
||
* @param mixed $meta_value
|
||
* @param mixed $prev_value
|
||
*/
|
||
public static function intercept_update( $check, $object_id, $meta_key, $meta_value, $prev_value ) {
|
||
|
||
// 已在短路中 → 避免遞迴
|
||
if ( ! empty( self::$internal_ops[ $object_id . ':' . $meta_key ] ) ) {
|
||
return $check;
|
||
}
|
||
|
||
$type = self::resolve_type_from_current_filter();
|
||
if ( ! $type ) {
|
||
return $check;
|
||
}
|
||
|
||
$field_def = TMDO_Entity_Registry::get_field( $type, $meta_key );
|
||
if ( ! $field_def ) {
|
||
return $check; // 非管理欄位,放行
|
||
}
|
||
|
||
// ── Mode-aware dispatch ──────────────────────────────
|
||
// disabled : 完全放行給 WP 原生 meta(回 null / $check)
|
||
// dual_write: 寫 flat,然後 return null 讓 WP 繼續寫 EAV
|
||
// shadow_read: 寫 flat,然後 return null 讓 WP 繼續寫 EAV
|
||
// aeav_only : 寫 flat,return true 短路 WP(不寫 EAV)
|
||
if ( ! TMDO_Mode_Manager::writes_to_flat( $type ) ) {
|
||
return $check; // disabled
|
||
}
|
||
|
||
// Route decision (v1.2.0):讓 UAEPG 等外掛正式訂閱 routing,不必搶 priority 5。
|
||
$route = self::decide_route( 'update', $type, (int) $object_id, $meta_key, $meta_value );
|
||
if ( $route === 'pg' ) {
|
||
// 讓其他 listener(例如 UAEPG)接手;原生 EAV 也放行。
|
||
return null;
|
||
}
|
||
if ( $route === 'skip' ) {
|
||
// 不寫 flat、不寫 EAV,但告訴 WP 已處理。
|
||
return true;
|
||
}
|
||
|
||
// 截取 before value(v1.3.1):audit_logger 等訂閱者需要變更前的值。
|
||
// v1.3.2:透過 filter `wpdo_capture_before_value` 可關閉以省一次 DB read。
|
||
$before_value = self::maybe_read_before_value( $type, (int) $object_id, $meta_key, $field_def, 'update' );
|
||
|
||
$flat_result = self::perform_upsert( $type, (int) $object_id, $meta_key, $meta_value, $field_def );
|
||
|
||
do_action( 'wpdo_after_write', $type, (int) $object_id, $meta_key, $meta_value, $flat_result, 'update', $before_value );
|
||
|
||
// 若 mode 也要寫 EAV → return null 讓 WP 繼續
|
||
if ( TMDO_Mode_Manager::writes_to_eav( $type ) ) {
|
||
return null; // dual_write / shadow_read
|
||
}
|
||
|
||
return $flat_result; // aeav_only
|
||
}
|
||
|
||
public static function intercept_add( $check, $object_id, $meta_key, $meta_value, $unique ) {
|
||
|
||
// 已在短路中 → 避免遞迴
|
||
if ( ! empty( self::$internal_ops[ $object_id . ':' . $meta_key ] ) ) {
|
||
return $check;
|
||
}
|
||
|
||
$type = self::resolve_type_from_current_filter();
|
||
if ( ! $type ) {
|
||
return $check;
|
||
}
|
||
|
||
$field_def = TMDO_Entity_Registry::get_field( $type, $meta_key );
|
||
if ( ! $field_def ) {
|
||
return $check;
|
||
}
|
||
|
||
if ( ! TMDO_Mode_Manager::writes_to_flat( $type ) ) {
|
||
return $check;
|
||
}
|
||
|
||
$route = self::decide_route( 'add', $type, (int) $object_id, $meta_key, $meta_value );
|
||
if ( $route === 'pg' ) {
|
||
return null;
|
||
}
|
||
if ( $route === 'skip' ) {
|
||
return true;
|
||
}
|
||
|
||
// UAE 的設計:每個 entity 只有 1 row,所以 add 與 update 等效(Upsert)。
|
||
// 截取 before value(v1.3.1):add 情境下多半為 null,但若 row 已存在而 user 呼叫 add 也能抓到舊值。
|
||
$before_value = self::maybe_read_before_value( $type, (int) $object_id, $meta_key, $field_def, 'add' );
|
||
|
||
$flat_result = self::perform_upsert( $type, (int) $object_id, $meta_key, $meta_value, $field_def );
|
||
|
||
do_action( 'wpdo_after_write', $type, (int) $object_id, $meta_key, $meta_value, $flat_result, 'add', $before_value );
|
||
|
||
if ( TMDO_Mode_Manager::writes_to_eav( $type ) ) {
|
||
return null;
|
||
}
|
||
|
||
return $flat_result;
|
||
}
|
||
|
||
/**
|
||
* 如 filter `wpdo_capture_before_value` 回 true 才讀 flat table 的 before value,
|
||
* 否則直接回 null — 讓沒在用 audit / 其他 listener 的站台省一次 DB read。
|
||
*
|
||
* filter 參數:(bool $default_true, string $type, string $meta_key, string $op)
|
||
* $op ∈ { 'add', 'update', 'delete' }
|
||
*
|
||
* 使用範例(關閉 audit 的站台):
|
||
* add_filter( 'wpdo_capture_before_value', '__return_false' );
|
||
*
|
||
* @since 1.3.2
|
||
*/
|
||
private static function maybe_read_before_value(
|
||
string $type,
|
||
int $entity_id,
|
||
string $meta_key,
|
||
array $field_def,
|
||
string $op
|
||
) {
|
||
// Default false since v3.1.5 (A): reading the previous flat value costs an
|
||
// extra DB read on every managed write. Sites that consume value_before —
|
||
// e.g. the audit log — must opt in:
|
||
// add_filter( 'wpdo_capture_before_value', '__return_true' );
|
||
$capture = apply_filters(
|
||
'wpdo_capture_before_value',
|
||
false,
|
||
$type,
|
||
$meta_key,
|
||
$op
|
||
);
|
||
|
||
if ( ! $capture ) {
|
||
return null;
|
||
}
|
||
|
||
return self::read_flat_value( $type, $entity_id, $meta_key, $field_def );
|
||
}
|
||
|
||
/**
|
||
* 讀取 flat table 中當前值(before value,供 audit / after_write listener 使用)。
|
||
*
|
||
* 此方法**不經 cache 加熱**,直接查 DB,以避免快取污染與遞迴。表不存在回 null。
|
||
*
|
||
* @since 1.3.1
|
||
*/
|
||
private static function read_flat_value(
|
||
string $type,
|
||
int $entity_id,
|
||
string $meta_key,
|
||
array $field_def
|
||
) {
|
||
global $wpdb;
|
||
|
||
$adapter = TMDO_Entity_Registry::get_adapter( $type );
|
||
if ( ! $adapter ) {
|
||
return null;
|
||
}
|
||
|
||
$group = $field_def['group'] ?? '';
|
||
if ( $group === '' ) {
|
||
return null;
|
||
}
|
||
|
||
$table = TMDO_Schema_Manager::get_table_name( $type, $group );
|
||
if ( ! TMDO_Schema_Manager::table_exists( $table ) ) {
|
||
return null;
|
||
}
|
||
|
||
$col = TMDO_Schema_Manager::sanitize_column_name( $meta_key );
|
||
$id_col = $adapter->get_entity_id_column();
|
||
|
||
$raw = $wpdb->get_var(
|
||
$wpdb->prepare(
|
||
"SELECT `{$col}` FROM `{$table}` WHERE `{$id_col}` = %d LIMIT 1",
|
||
$entity_id
|
||
)
|
||
);
|
||
|
||
return $raw === null ? null : TMDO_Type_Caster::from_db( $raw, $field_def );
|
||
}
|
||
|
||
/**
|
||
* 讓外部 listener(例如 UAEPG)透過 `wpdo_route_decision` filter 指定路由。
|
||
*
|
||
* 回傳值:
|
||
* 'flat' (預設) — UAE 寫入 MySQL flat table
|
||
* 'pg' — 放行,由其他 listener 接手;UAE 不寫 flat,原生 EAV 依 mode 決定
|
||
* 'skip' — 都不寫(用於軟刪除之類特殊情境),但告訴 WP 已處理
|
||
*
|
||
* 其他非預期值會被 fallback 到 'flat' 以維持安全預設。
|
||
*
|
||
* @since 1.2.0
|
||
*/
|
||
private static function decide_route(
|
||
string $op,
|
||
string $type,
|
||
int $object_id,
|
||
string $meta_key,
|
||
$meta_value
|
||
): string {
|
||
$route = apply_filters(
|
||
'wpdo_route_decision',
|
||
'flat',
|
||
$type,
|
||
$object_id,
|
||
$meta_key,
|
||
$meta_value,
|
||
$op
|
||
);
|
||
|
||
if ( in_array( $route, array( 'flat', 'pg', 'skip' ), true ) ) {
|
||
return $route;
|
||
}
|
||
|
||
TMDO_Logger::warning(
|
||
'wpdo_route_decision_invalid_return',
|
||
array(
|
||
'returned' => is_scalar( $route ) ? (string) $route : gettype( $route ),
|
||
'op' => $op,
|
||
'type' => $type,
|
||
'key' => $meta_key,
|
||
)
|
||
);
|
||
return 'flat';
|
||
}
|
||
|
||
/**
|
||
* 執行 Upsert 操作
|
||
*/
|
||
private static function perform_upsert(
|
||
string $type,
|
||
int $entity_id,
|
||
string $meta_key,
|
||
$meta_value,
|
||
array $field_def
|
||
) {
|
||
global $wpdb;
|
||
|
||
$adapter = TMDO_Entity_Registry::get_adapter( $type );
|
||
if ( ! $adapter ) {
|
||
return null;
|
||
}
|
||
|
||
$group = $field_def['group'];
|
||
$table = TMDO_Schema_Manager::get_table_name( $type, $group );
|
||
$id_col = $adapter->get_entity_id_column();
|
||
$col = TMDO_Schema_Manager::sanitize_column_name( $meta_key );
|
||
|
||
// 表不存在則讓 WP 走原生流程(降級處理)
|
||
if ( ! TMDO_Schema_Manager::table_exists( $table ) ) {
|
||
return null;
|
||
}
|
||
|
||
// 型別轉換
|
||
$db_value = TMDO_Type_Caster::to_db( $meta_value, $field_def );
|
||
$format = TMDO_Type_Caster::get_wpdb_format( $field_def['type'] );
|
||
|
||
// 鎖防遞迴
|
||
$lock_key = $entity_id . ':' . $meta_key;
|
||
self::$internal_ops[ $lock_key ] = true;
|
||
|
||
try {
|
||
// 單一 upsert,避免 SELECT→UPDATE/INSERT 的 read-modify-write race。
|
||
$result = TMDO_DB::upsert(
|
||
$table,
|
||
array(
|
||
$id_col => $entity_id,
|
||
$col => $db_value,
|
||
),
|
||
array( $col ),
|
||
$id_col,
|
||
array( '%d', $format )
|
||
);
|
||
|
||
// 清除快取
|
||
TMDO_Cache_Orchestrator::invalidate( $type, $entity_id, $group );
|
||
|
||
// 短路回傳 true(WP 認為寫入成功)
|
||
return $result !== false;
|
||
} finally {
|
||
unset( self::$internal_ops[ $lock_key ] );
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 讀取:get_{type}_metadata filter
|
||
// ─────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 攔截 get_metadata() 呼叫
|
||
*
|
||
* @param null|mixed $check 若回傳 null 則 WP 繼續原生流程
|
||
* @param int $object_id
|
||
* @param string $meta_key 空字串表示取所有 meta
|
||
* @param bool $single
|
||
* @param string $meta_type 5.5+ 額外參數
|
||
*/
|
||
public static function intercept_get( $check, $object_id, $meta_key, $single, $meta_type = '' ) {
|
||
|
||
$type = $meta_type ?: self::resolve_type_from_current_filter();
|
||
if ( ! $type ) {
|
||
return $check;
|
||
}
|
||
|
||
// 空 key:WP 要求所有 meta,UAE 不攔截(維持相容性)
|
||
if ( $meta_key === '' ) {
|
||
return $check;
|
||
}
|
||
|
||
$field_def = TMDO_Entity_Registry::get_field( $type, $meta_key );
|
||
if ( ! $field_def ) {
|
||
return $check;
|
||
}
|
||
|
||
// ── Mode-aware dispatch ──────────────────────────────
|
||
// disabled : 完全不攔截,回傳 $check 讓 WP 走原生 EAV
|
||
// dual_write : 讀取仍走 EAV(flat 可能還沒有資料),回傳 $check
|
||
// shadow_read : 讀取走 UAE flat,同時與 EAV 比對記錄 diff
|
||
// aeav_only : 讀取走 UAE flat,不讀 EAV
|
||
if ( ! TMDO_Mode_Manager::reads_from_flat( $type ) ) {
|
||
return $check; // disabled / dual_write
|
||
}
|
||
|
||
$group = $field_def['group'];
|
||
$row = self::get_or_load_row( $type, (int) $object_id, $group );
|
||
|
||
$col = TMDO_Schema_Manager::sanitize_column_name( $meta_key );
|
||
$has_value = is_array( $row ) && array_key_exists( $col, $row );
|
||
$value = $has_value ? TMDO_Type_Caster::from_db( $row[ $col ], $field_def ) : null;
|
||
|
||
// Shadow-read:與 EAV 比對,記錄差異
|
||
if ( TMDO_Mode_Manager::does_shadow_compare( $type ) ) {
|
||
try {
|
||
TMDO_Shadow_Diff_Logger::compare_and_log(
|
||
$type,
|
||
(int) $object_id,
|
||
$meta_key,
|
||
$value,
|
||
$field_def
|
||
);
|
||
} catch ( \Throwable $e ) {
|
||
// 比對失敗不該影響讀取
|
||
TMDO_Logger::error( 'entity_bridge', 'shadow_compare_exception', $e->getMessage() );
|
||
}
|
||
}
|
||
|
||
// 值為空 → 回 WP 原生慣例
|
||
if ( empty( $row ) || $value === null || $value === '' ) {
|
||
return $single ? '' : array();
|
||
}
|
||
|
||
// WP 的慣例:get_metadata() 即使 $single=true 也回傳陣列包裝
|
||
return $single ? array( $value ) : array( $value );
|
||
}
|
||
|
||
/**
|
||
* 取得(或載入)完整列資料,並快取
|
||
*/
|
||
private static function get_or_load_row( string $type, int $entity_id, string $group ): array {
|
||
|
||
// L1 快取
|
||
$cached = TMDO_Cache_Orchestrator::get_row( $type, $entity_id, $group );
|
||
if ( is_array( $cached ) ) {
|
||
return $cached;
|
||
}
|
||
|
||
global $wpdb;
|
||
|
||
$adapter = TMDO_Entity_Registry::get_adapter( $type );
|
||
if ( ! $adapter ) {
|
||
return array();
|
||
}
|
||
|
||
$table = TMDO_Schema_Manager::get_table_name( $type, $group );
|
||
$id_col = $adapter->get_entity_id_column();
|
||
|
||
if ( ! TMDO_Schema_Manager::table_exists( $table ) ) {
|
||
return array();
|
||
}
|
||
|
||
$row = $wpdb->get_row(
|
||
$wpdb->prepare(
|
||
"SELECT * FROM `{$table}` WHERE `{$id_col}` = %d LIMIT 1",
|
||
$entity_id
|
||
),
|
||
ARRAY_A
|
||
);
|
||
|
||
$row = $row ?: array();
|
||
|
||
TMDO_Cache_Orchestrator::set_row( $type, $entity_id, $group, $row );
|
||
|
||
return $row;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 刪除:delete_{type}_metadata filter
|
||
// ─────────────────────────────────────────────────────────
|
||
|
||
public static function intercept_delete( $check, $object_id, $meta_key, $meta_value, $delete_all ) {
|
||
|
||
$type = self::resolve_type_from_current_filter();
|
||
if ( ! $type ) {
|
||
return $check;
|
||
}
|
||
|
||
$field_def = TMDO_Entity_Registry::get_field( $type, $meta_key );
|
||
if ( ! $field_def ) {
|
||
return $check;
|
||
}
|
||
|
||
// Mode-aware:disabled 完全不攔截
|
||
if ( ! TMDO_Mode_Manager::writes_to_flat( $type ) ) {
|
||
return $check;
|
||
}
|
||
|
||
$route = self::decide_route( 'delete', $type, (int) $object_id, $meta_key, $meta_value );
|
||
if ( $route === 'pg' ) {
|
||
return null;
|
||
}
|
||
if ( $route === 'skip' ) {
|
||
return true;
|
||
}
|
||
|
||
// 截取 before value(v1.3.1)
|
||
$before_value = self::maybe_read_before_value( $type, (int) $object_id, $meta_key, $field_def, 'delete' );
|
||
|
||
global $wpdb;
|
||
|
||
$adapter = TMDO_Entity_Registry::get_adapter( $type );
|
||
$group = $field_def['group'];
|
||
$table = TMDO_Schema_Manager::get_table_name( $type, $group );
|
||
$id_col = $adapter->get_entity_id_column();
|
||
$col = TMDO_Schema_Manager::sanitize_column_name( $meta_key );
|
||
|
||
if ( ! TMDO_Schema_Manager::table_exists( $table ) ) {
|
||
return $check;
|
||
}
|
||
|
||
// UAE 的邏輯:刪除 meta = 設該欄位為 NULL
|
||
// 因為一個 entity 只對應一列,完整刪除列會丟失其他欄位
|
||
$default = $field_def['default'] ?? null;
|
||
|
||
if ( $delete_all ) {
|
||
/**
|
||
* Opt-in gate for mass column clear (delete_all=true).
|
||
*
|
||
* Defaults to false — a single mistargetted call NULLs the column for
|
||
* every entity, and the previous row-count heuristic let small sites
|
||
* (< 500 rows) through unguarded. Enable only in controlled contexts:
|
||
* add_filter( 'wpdo_allow_mass_column_clear', '__return_true' );
|
||
*
|
||
* @param bool $allow Whether to allow the mass clear. Default false.
|
||
* @param string $table Flat table name.
|
||
* @param string $col Column being cleared.
|
||
* @param string $meta_key Original meta key.
|
||
*/
|
||
$allowed = (bool) apply_filters( 'wpdo_allow_mass_column_clear', false, $table, $col, $meta_key );
|
||
/** This filter is documented above (tmdo_* is the forward-looking name). */
|
||
$allowed = (bool) apply_filters( 'tmdo_allow_mass_column_clear', $allowed, $table, $col, $meta_key );
|
||
|
||
if ( ! $allowed ) {
|
||
TMDO_Logger::warning(
|
||
'intercept_delete_mass_blocked',
|
||
array(
|
||
'table' => $table,
|
||
'col' => $col,
|
||
'reason' => 'enable via wpdo_allow_mass_column_clear filter',
|
||
)
|
||
);
|
||
return false;
|
||
}
|
||
|
||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||
$row_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
|
||
|
||
TMDO_Logger::warning(
|
||
'intercept_delete_all',
|
||
array(
|
||
'table' => $table,
|
||
'col' => $col,
|
||
'rows' => $row_count,
|
||
)
|
||
);
|
||
|
||
// 刪除所有 entity 的該欄位
|
||
$result = $wpdb->query(
|
||
$wpdb->prepare(
|
||
"UPDATE `{$table}` SET `{$col}` = %s",
|
||
$default
|
||
)
|
||
);
|
||
} else {
|
||
$result = $wpdb->update(
|
||
$table,
|
||
array( $col => $default ),
|
||
array( $id_col => $object_id ),
|
||
array( TMDO_Type_Caster::get_wpdb_format( $field_def['type'] ) ),
|
||
array( '%d' )
|
||
);
|
||
|
||
TMDO_Cache_Orchestrator::invalidate( $type, (int) $object_id, $group );
|
||
}
|
||
|
||
do_action( 'wpdo_after_delete', $type, (int) $object_id, $meta_key, $meta_value, $result, (bool) $delete_all, $before_value );
|
||
|
||
// 若還要寫 EAV → return null 讓 WP 繼續刪除原生 meta
|
||
if ( TMDO_Mode_Manager::writes_to_eav( $type ) ) {
|
||
return null;
|
||
}
|
||
|
||
return $result !== false;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 實體刪除清理
|
||
// ─────────────────────────────────────────────────────────
|
||
|
||
public static function cleanup_entity( string $type, int $entity_id ): void {
|
||
global $wpdb;
|
||
|
||
$adapter = TMDO_Entity_Registry::get_adapter( $type );
|
||
if ( ! $adapter ) {
|
||
return;
|
||
}
|
||
|
||
$id_col = $adapter->get_entity_id_column();
|
||
$groups = TMDO_Entity_Registry::get_groups_for_type( $type );
|
||
|
||
foreach ( $groups as $group ) {
|
||
$table = TMDO_Schema_Manager::get_table_name( $type, $group );
|
||
if ( TMDO_Schema_Manager::table_exists( $table ) ) {
|
||
$wpdb->delete( $table, array( $id_col => $entity_id ), array( '%d' ) );
|
||
}
|
||
}
|
||
|
||
TMDO_Cache_Orchestrator::flush_entity( $type, $entity_id );
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 工具方法
|
||
// ─────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 從當前 filter 名稱推斷實體類型
|
||
* 例:update_user_metadata → user
|
||
*/
|
||
private static function resolve_type_from_current_filter(): ?string {
|
||
$current = current_filter();
|
||
|
||
if ( preg_match( '/^(?:add|get|update|delete)_(post|user|term|comment)_metadata$/', $current, $matches ) ) {
|
||
return $matches[1];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 直接讀取(繞過 WP filter 系統)
|
||
* 供 wpdo_get_meta() 便利函式使用,性能更好
|
||
*
|
||
* @param string $type
|
||
* @param int $entity_id
|
||
* @param string $key 若為空字串則回傳整列
|
||
* @return mixed
|
||
*/
|
||
public static function direct_read( string $type, int $entity_id, string $key = '' ) {
|
||
|
||
if ( $key === '' ) {
|
||
// 回傳所有群組的所有欄位
|
||
$result = array();
|
||
foreach ( TMDO_Entity_Registry::get_groups_for_type( $type ) as $group ) {
|
||
$row = self::get_or_load_row( $type, $entity_id, $group );
|
||
foreach ( TMDO_Entity_Registry::get_group_fields( $type, $group ) as $field ) {
|
||
$col = TMDO_Schema_Manager::sanitize_column_name( $field['key'] );
|
||
$result[ $field['key'] ] = TMDO_Type_Caster::from_db( $row[ $col ] ?? null, $field );
|
||
}
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
$field_def = TMDO_Entity_Registry::get_field( $type, $key );
|
||
if ( ! $field_def ) {
|
||
return null;
|
||
}
|
||
|
||
$row = self::get_or_load_row( $type, $entity_id, $field_def['group'] );
|
||
$col = TMDO_Schema_Manager::sanitize_column_name( $key );
|
||
return TMDO_Type_Caster::from_db( $row[ $col ] ?? null, $field_def );
|
||
}
|
||
}
|