chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
@@ -0,0 +1,658 @@
|
||||
<?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
|
||||
) {
|
||||
$capture = apply_filters(
|
||||
'wpdo_capture_before_value',
|
||||
true,
|
||||
$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 {
|
||||
// 檢查列是否存在
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM `{$table}` WHERE `{$id_col}` = %d",
|
||||
$entity_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( $exists ) {
|
||||
// UPDATE
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
array( $col => $db_value ),
|
||||
array( $id_col => $entity_id ),
|
||||
array( $format ),
|
||||
array( '%d' )
|
||||
);
|
||||
} else {
|
||||
// INSERT
|
||||
$result = $wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
$id_col => $entity_id,
|
||||
$col => $db_value,
|
||||
),
|
||||
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(
|
||||
'shadow_compare_exception',
|
||||
array(
|
||||
'error' => $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 ) {
|
||||
// Safety cap: refuse mass-null if affected row count exceeds threshold.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$row_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
|
||||
|
||||
if ( $row_count > 500 ) {
|
||||
TMDO_Logger::warning(
|
||||
'intercept_delete_mass_blocked',
|
||||
array(
|
||||
'table' => $table,
|
||||
'col' => $col,
|
||||
'rows' => $row_count,
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
TMDO_Logger::info(
|
||||
'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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user