Files
2meet-data-optimizer/includes/engine/class-tmdo-entity-registry.php
T
wpdev 1653011f4f feat(2meet-data-optimizer): guard orphaned flat-table groups
- On init:1, re-register aeav_only groups whose flat table exists and whose definition is in wpdo_registry_meta but no active plugin registers them, so their data stays readable (routing only, never DDL)
- Keys owned by a live group, or with rows in native meta (newer data written while orphaned), are left alone; the native-meta check is cached for a day
- Surface restored groups via Site Health (wpdo_orphaned_entity_groups), the admin Doctor tab, and wp wpdo doctor
- Verify: EntityRegistryRestoreTest (9), OrphanGroupRestoreTest (7); core unit 622 / integration 439, HP 141/49, WC 10/22 OK; dry-run on dev20–22 restores 16/17/4 groups, keeps hp_taxonomy hp_sort_order/hp_icon on native
- PR: PR-018
2026-09-25 23:38:12 +08:00

365 lines
12 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_Entity_Registry - 全域欄位與適配器登錄中心
*
* 提供:
* - 適配器註冊(post/user/term/comment)
* - 欄位群組註冊(Schema 定義)
* - 欄位查詢 API(meta_key → field_def)
*
* @package WP_Data_Optimizer
*/
declare(strict_types=1);
// 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_Entity_Registry {
/** @var array<string, TMDO_Entity_Adapter_Interface> 實體類型 → 適配器實例 */
private static array $adapters = array();
/** @var array<string, array<string, array>> 實體類型 → 群組名 → 欄位定義陣列 */
private static array $groups = array();
/** @var array<string, array<string, array>> 實體類型 → meta_key → 欄位定義(含 group 資訊)*/
private static array $field_index = array();
/** @var array<int, array{type:string, group:string, fields:array}> 待建表的 Schema 清單 */
private static array $pending_schemas = array();
/** @var array<string, array> "entity/group" → 同名群組以不同定義重複註冊的紀錄 */
private static array $collisions = array();
/** @var array<string, array> "entity/group" → 從 wpdo_registry_meta 還原的孤兒群組 */
private static array $restored = array();
/** 合法的欄位型別 */
public const VALID_TYPES = array(
'text',
'textarea',
'integer',
'decimal',
'boolean',
'date',
'datetime',
'timestamp',
'json',
'enum',
'binary',
);
/** 合法的實體類型 */
public const VALID_ENTITY_TYPES = array( 'post', 'user', 'term', 'comment' );
public static function init(): void {
self::$adapters = array();
self::$groups = array();
self::$field_index = array();
self::$pending_schemas = array();
self::$collisions = array();
self::$restored = array();
}
// ─────────────────────────────────────────────────────────
// 適配器管理
// ─────────────────────────────────────────────────────────
public static function register_adapter( string $entity_type, TMDO_Entity_Adapter_Interface $adapter ): void {
if ( ! in_array( $entity_type, self::VALID_ENTITY_TYPES, true ) ) {
return;
}
self::$adapters[ $entity_type ] = $adapter;
}
public static function get_adapter( string $entity_type ): ?TMDO_Entity_Adapter_Interface {
return self::$adapters[ $entity_type ] ?? null;
}
public static function get_all_adapters(): array {
return self::$adapters;
}
// ─────────────────────────────────────────────────────────
// 欄位群組登錄
// ─────────────────────────────────────────────────────────
/**
* 登錄一組 UAE 管理的欄位
*
* @param string $entity_type post|user|term|comment
* @param string $group_name 群組名稱(會成為表名的一部分)
* @param array $fields 欄位定義陣列,每項需有 key、type,選填:
* - required (bool)
* - default (mixed)
* - searchable (bool) 加索引
* - fulltext (bool) 全文索引(僅 text/textarea)
* - unique (bool) 唯一索引
* - options (array) enum 選項
* - label (string) 顯示名稱
*
* @return bool
*/
public static function register_group( string $entity_type, string $group_name, array $fields ): bool {
if ( ! in_array( $entity_type, self::VALID_ENTITY_TYPES, true ) ) {
return false;
}
if ( ! isset( self::$adapters[ $entity_type ] ) ) {
return false;
}
$normalized = self::normalize_fields( $entity_type, $group_name, $fields );
if ( empty( $normalized ) ) {
return false;
}
// 防重複登錄:相同定義(init:1 重新觸發)靜默略過;定義不同則記錄衝突,保留先註冊者。
if ( isset( self::$groups[ $entity_type ][ $group_name ] ) ) {
if ( self::$groups[ $entity_type ][ $group_name ] !== $normalized ) {
self::record_collision( $entity_type, $group_name, self::$groups[ $entity_type ][ $group_name ], $normalized );
}
return false;
}
// 建立索引以供快速查詢
foreach ( $normalized as $normalized_field ) {
self::$field_index[ $entity_type ][ $normalized_field['key'] ] = $normalized_field;
}
self::$groups[ $entity_type ][ $group_name ] = $normalized;
// 加入待建表佇列
self::$pending_schemas[] = array(
'type' => $entity_type,
'group' => $group_name,
'fields' => $normalized,
);
return true;
}
/**
* 驗證並正規化欄位定義(略過缺 key/type 或型別不合法者)。
*/
private static function normalize_fields( string $entity_type, string $group_name, array $fields ): array {
$normalized = array();
foreach ( $fields as $field ) {
if ( empty( $field['key'] ) || empty( $field['type'] ) ) {
continue;
}
if ( ! in_array( $field['type'], self::VALID_TYPES, true ) ) {
continue;
}
$normalized_field = wp_parse_args(
$field,
array(
'key' => '',
'type' => 'text',
'required' => false,
'default' => null,
'searchable' => false,
'fulltext' => false,
'unique' => false,
'options' => array(),
'label' => '',
)
);
$normalized_field['group'] = $group_name;
$normalized_field['entity_type'] = $entity_type;
$normalized[] = $normalized_field;
}
return $normalized;
}
/**
* 還原孤兒群組:依 wpdo_registry_meta 保存的定義重新登錄,只供讀寫路由用。
*
* 與 register_group() 的差異:不進 pending_schemas(絕不觸發 DDL),且已被
* 其他存活群組管理的 key 一律略過,不覆蓋。$native_keys 列出原生 meta 仍有
* 資料的 key:那些值比扁平表新(孤兒期間寫入),必須留在原生 meta。
*
* @param string[] $native_keys 原生 meta 仍有資料、不可還原的 key。
* @return bool 有登錄任何欄位時為 true
*/
public static function restore_group( string $entity_type, string $group_name, array $fields, array $native_keys = array() ): bool {
if ( ! isset( self::$adapters[ $entity_type ] ) || isset( self::$groups[ $entity_type ][ $group_name ] ) ) {
return false;
}
$kept = array();
$skipped = array();
$native = array();
foreach ( self::normalize_fields( $entity_type, $group_name, $fields ) as $field ) {
if ( isset( self::$field_index[ $entity_type ][ $field['key'] ] ) ) {
$skipped[] = (string) $field['key'];
continue;
}
if ( in_array( $field['key'], $native_keys, true ) ) {
$native[] = (string) $field['key'];
continue;
}
$kept[] = $field;
}
$id = $entity_type . '/' . $group_name;
if ( empty( $kept ) ) {
// 整組都留在原生 meta 時仍要回報:扁平表的舊資料需要人工處理。
if ( $native ) {
self::$restored[ $id ] = array(
'entity_type' => $entity_type,
'group' => $group_name,
'fields' => 0,
'skipped' => $skipped,
'native' => $native,
);
}
return false;
}
foreach ( $kept as $field ) {
self::$field_index[ $entity_type ][ $field['key'] ] = $field;
}
self::$groups[ $entity_type ][ $group_name ] = $kept;
self::$restored[ $id ] = array(
'entity_type' => $entity_type,
'group' => $group_name,
'fields' => count( $kept ),
'skipped' => $skipped,
'native' => $native,
);
return true;
}
/**
* 本請求從 wpdo_registry_meta 還原的孤兒群組。
*
* @return array<int, array{entity_type:string, group:string, fields:int, skipped:string[], native:string[]}>
*/
public static function get_restored_groups(): array {
return array_values( self::$restored );
}
/**
* 記錄同名群組衝突(每個 entity/group 每請求一筆),並寫入 warning。
*
* 註冊每個請求都會跑,warning 以 transient 節流為每天一次,避免灌爆 wpdo_errors。
*/
private static function record_collision( string $entity_type, string $group_name, array $existing, array $incoming ): void {
$id = $entity_type . '/' . $group_name;
if ( isset( self::$collisions[ $id ] ) ) {
return;
}
$old = array_column( $existing, null, 'key' );
$new = array_column( $incoming, null, 'key' );
$changed = array();
foreach ( array_intersect_key( $new, $old ) as $key => $field ) {
if ( $field !== $old[ $key ] ) {
$changed[] = (string) $key;
}
}
self::$collisions[ $id ] = array(
'entity_type' => $entity_type,
'group' => $group_name,
'added' => array_map( 'strval', array_keys( array_diff_key( $new, $old ) ) ),
'removed' => array_map( 'strval', array_keys( array_diff_key( $old, $new ) ) ),
'changed' => $changed,
);
$throttle = 'tmdo_group_collision_' . md5( (string) wp_json_encode( self::$collisions[ $id ] ) );
if ( class_exists( 'TMDO_Logger' ) && false === get_transient( $throttle ) ) {
set_transient( $throttle, 1, DAY_IN_SECONDS );
TMDO_Logger::warning( 'entity_group_collision', self::$collisions[ $id ] );
}
}
/**
* 本請求偵測到的同名群組衝突(第二份定義已被捨棄)。
*
* @return array<int, array{entity_type:string, group:string, added:string[], removed:string[], changed:string[]}>
*/
public static function get_collisions(): array {
return array_values( self::$collisions );
}
// ─────────────────────────────────────────────────────────
// 查詢 API
// ─────────────────────────────────────────────────────────
/**
* 依 meta_key 查詢欄位定義
*
* @return array|null 欄位定義,或 null(非 UAE 管理欄位)
*/
public static function get_field( string $entity_type, string $meta_key ): ?array {
return self::$field_index[ $entity_type ][ $meta_key ] ?? null;
}
/**
* 取得實體類型下所有群組名稱
*
* @return array<string>
*/
public static function get_groups_for_type( string $entity_type ): array {
return array_keys( self::$groups[ $entity_type ] ?? array() );
}
/**
* 取得特定群組的完整欄位定義
*/
public static function get_group_fields( string $entity_type, string $group_name ): array {
return self::$groups[ $entity_type ][ $group_name ] ?? array();
}
/**
* 取得一個群組中所有 meta_key
*
* @return array<string>
*/
public static function get_group_keys( string $entity_type, string $group_name ): array {
$fields = self::get_group_fields( $entity_type, $group_name );
return array_column( $fields, 'key' );
}
/**
* 取得所有已登錄的欄位(跨實體、跨群組)
*/
public static function get_all_fields(): array {
return self::$field_index;
}
/**
* 取得待建表清單(供 Schema_Manager 處理)
*/
public static function get_pending_schemas(): array {
return self::$pending_schemas;
}
public static function clear_pending_schemas(): void {
self::$pending_schemas = array();
}
/**
* 判斷某個 meta_key 是否由 UAE 管理
*/
public static function is_managed( string $entity_type, string $meta_key ): bool {
return isset( self::$field_index[ $entity_type ][ $meta_key ] );
}
}