Files
2meet-data-optimizer/includes/engine/class-tmdo-entity-registry.php
T
wpdev e9d8696c93 fix(2meet-data-optimizer): report entity group name collisions instead of silently dropping
- register_group() keeps the first definition but records a field diff (added/removed/changed) when the same entity/group is re-registered differently; identical re-registration stays silent
- Surface collisions via Site Health (wpdo_entity_group_collisions), the admin Doctor tab, and wp wpdo doctor; warning log throttled to once a day
- Verify: EntityRegistryCollisionTest (7); core unit 613 / integration 432, HP 141/49, WC 10/22 OK
- PR: PR-017
2026-09-25 23:27:05 +08:00

283 lines
9.6 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();
/** 合法的欄位型別 */
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();
}
// ─────────────────────────────────────────────────────────
// 適配器管理
// ─────────────────────────────────────────────────────────
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 = 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;
}
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;
}
/**
* 記錄同名群組衝突(每個 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 ] );
}
}