Files
2meet-data-optimizer/includes/integrations/class-tmdo-term-comment-misc-bucket.php
wpdev 76c01e44df refactor: 全部 128 個生產檔加入 declare(strict_types=1)(PR-H)
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入
並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 06:13:33 +08:00

415 lines
14 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_Term_Comment_Misc_Bucket — Catch-all flat storage for unregistered
* term + comment meta keys (v2.12.4 Phase 4).
*
* The "missing piece" that makes wp_termmeta / wp_commentmeta completely
* avoidable. After Phase 03:
*
* - Phase 0 (v2.12.0): cleanup CLI removes historical garbage
* - Phase 1 (v2.12.1): garbage filter blocks new garbage writes
* - Phase 2 (v2.12.2): entity registry routes registered HivePress fields
* to wp_wpdo_term_hp_taxonomy / wp_wpdo_comment_hp_review
* - Phase 3 (v2.12.3): WC term count filter routes product_count_* → wp_options
*
* What's left? **Unregistered keys** — anything written to term/comment meta
* that doesn't match any pattern (e.g., `note_group` on dev10, or any future
* plugin's custom key). Those still land in wp_termmeta / wp_commentmeta.
*
* This class is the catch-all. Filter priority 99 (LAST in the chain) means
* we only handle writes where every other filter has returned null (i.e., no
* match). Two new flat tables provide structurally identical K/V storage
* with explicit ownership.
*
* Filter priority chain (term + comment metadata writes):
*
* priority 9: garbage filter (drop _wxr_ / _demo_ / orphan)
* priority 9: WC term count filter (route product_count_* → wp_options)
* priority 10: Hook Bus / entity (route registered keys → flat tables)
* priority 99: THIS misc bucket (catch-all → wp_wpdo_*_misc)
*
* The chain works because filter callbacks preserve `$check` / `$pre`
* (the accumulator) when they don't match, so we can detect "nobody handled
* this" by checking `$check === null` at our priority.
*
* Storage:
*
* CREATE TABLE wp_wpdo_term_misc (
* term_id BIGINT(20) UNSIGNED NOT NULL,
* meta_key VARCHAR(191) NOT NULL,
* meta_value LONGTEXT,
* updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
* PRIMARY KEY (term_id, meta_key),
* KEY meta_key (meta_key)
* );
* -- (and corresponding wp_wpdo_comment_misc with comment_id)
*
* No reverse-EAV optimization (still K/V storage), but achieves the literal
* goal of "wp_termmeta / wp_commentmeta zero writes" — making them DROPpable
* in v3.0.0.
*
* 🔒 Lessons applied (v2.11.7): added to TMDO_Hook_Bus_Bridge::COEXIST_WHITELIST.
*
* @package WP_Data_Optimizer
* @since 2.12.4
*/
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Catch-all storage for unregistered term + comment meta keys.
*/
final class TMDO_Term_Comment_Misc_Bucket {
/** Option toggle key. */
public const OPT_ENABLED = 'wpdo_term_comment_misc_bucket_enabled';
/** Filter priority — runs LAST in the chain (after Hook Bus at 10). */
public const FILTER_PRIORITY = 99;
/**
* Register the four metadata filters on `init` priority 5.
*
* @return void
*/
public static function init(): void {
if ( ! self::is_enabled() ) {
return;
}
// Term metadata.
add_filter( 'get_term_metadata', array( self::class, 'on_term_read' ), self::FILTER_PRIORITY, 4 );
add_filter( 'add_term_metadata', array( self::class, 'on_term_add' ), self::FILTER_PRIORITY, 5 );
add_filter( 'update_term_metadata', array( self::class, 'on_term_update' ), self::FILTER_PRIORITY, 5 );
add_filter( 'delete_term_metadata', array( self::class, 'on_term_delete' ), self::FILTER_PRIORITY, 5 );
// Comment metadata.
add_filter( 'get_comment_metadata', array( self::class, 'on_comment_read' ), self::FILTER_PRIORITY, 4 );
add_filter( 'add_comment_metadata', array( self::class, 'on_comment_add' ), self::FILTER_PRIORITY, 5 );
add_filter( 'update_comment_metadata', array( self::class, 'on_comment_update' ), self::FILTER_PRIORITY, 5 );
add_filter( 'delete_comment_metadata', array( self::class, 'on_comment_delete' ), self::FILTER_PRIORITY, 5 );
}
/**
* Check the admin toggle. Defaults to enabled.
*
* @return bool
*/
public static function is_enabled(): bool {
return (bool) get_option( self::OPT_ENABLED, '1' );
}
/**
* Term-side fully-qualified misc table name.
*
* @return string
*/
public static function term_table(): string {
global $wpdb;
return $wpdb->prefix . 'wpdo_term_misc';
}
/**
* Comment-side fully-qualified misc table name.
*
* @return string
*/
public static function comment_table(): string {
global $wpdb;
return $wpdb->prefix . 'wpdo_comment_misc';
}
// ── Term metadata callbacks ───────────────────────────────────────────────
/**
* Filter callback: get_term_metadata.
*
* @param mixed $pre Filter accumulator.
* @param int $object_id Term ID.
* @param string $meta_key Meta key being read.
* @param bool $single Whether single value was requested.
* @return mixed
*/
public static function on_term_read( $pre, $object_id, $meta_key, $single ) {
unset( $single );
// Only handle when no earlier filter has resolved this read.
if ( null !== $pre ) {
return $pre;
}
if ( ! is_string( $meta_key ) || '' === $meta_key ) {
return $pre;
}
$value = self::read( self::term_table(), 'term_id', (int) $object_id, $meta_key );
if ( null === $value ) {
return $pre;
}
return array( $value );
}
/**
* Filter callback: add_term_metadata.
*
* @param mixed $check Filter accumulator.
* @param int $object_id Term ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Value.
* @param bool $unique Unique flag (unused).
* @return mixed
*/
public static function on_term_add( $check, $object_id, $meta_key, $meta_value, $unique ) {
unset( $unique );
if ( null !== $check ) {
return $check;
}
if ( ! is_string( $meta_key ) || '' === $meta_key ) {
return $check;
}
self::write( self::term_table(), 'term_id', (int) $object_id, $meta_key, $meta_value );
return true;
}
/**
* Filter callback: update_term_metadata.
*
* @param mixed $check Filter accumulator.
* @param int $object_id Term ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Value.
* @param mixed $prev_value Previous value (unused).
* @return mixed
*/
public static function on_term_update( $check, $object_id, $meta_key, $meta_value, $prev_value ) {
unset( $prev_value );
if ( null !== $check ) {
return $check;
}
if ( ! is_string( $meta_key ) || '' === $meta_key ) {
return $check;
}
self::write( self::term_table(), 'term_id', (int) $object_id, $meta_key, $meta_value );
return true;
}
/**
* Filter callback: delete_term_metadata.
*
* @param mixed $check Filter accumulator.
* @param int $object_id Term ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Value-scoped delete (unused).
* @param bool $delete_all Delete-all flag (unused).
* @return mixed
*/
public static function on_term_delete( $check, $object_id, $meta_key, $meta_value, $delete_all ) {
unset( $meta_value, $delete_all );
if ( null !== $check ) {
return $check;
}
if ( ! is_string( $meta_key ) || '' === $meta_key ) {
return $check;
}
self::delete_row( self::term_table(), 'term_id', (int) $object_id, $meta_key );
return true;
}
// ── Comment metadata callbacks ────────────────────────────────────────────
/**
* Filter callback: get_comment_metadata.
*
* @param mixed $pre Filter accumulator.
* @param int $object_id Comment ID.
* @param string $meta_key Meta key.
* @param bool $single Single flag (unused).
* @return mixed
*/
public static function on_comment_read( $pre, $object_id, $meta_key, $single ) {
unset( $single );
if ( null !== $pre ) {
return $pre;
}
if ( ! is_string( $meta_key ) || '' === $meta_key ) {
return $pre;
}
$value = self::read( self::comment_table(), 'comment_id', (int) $object_id, $meta_key );
if ( null === $value ) {
return $pre;
}
return array( $value );
}
/**
* Filter callback: add_comment_metadata.
*
* @param mixed $check Filter accumulator.
* @param int $object_id Comment ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Value.
* @param bool $unique Unique flag (unused).
* @return mixed
*/
public static function on_comment_add( $check, $object_id, $meta_key, $meta_value, $unique ) {
unset( $unique );
if ( null !== $check ) {
return $check;
}
if ( ! is_string( $meta_key ) || '' === $meta_key ) {
return $check;
}
self::write( self::comment_table(), 'comment_id', (int) $object_id, $meta_key, $meta_value );
return true;
}
/**
* Filter callback: update_comment_metadata.
*
* @param mixed $check Filter accumulator.
* @param int $object_id Comment ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Value.
* @param mixed $prev_value Previous value (unused).
* @return mixed
*/
public static function on_comment_update( $check, $object_id, $meta_key, $meta_value, $prev_value ) {
unset( $prev_value );
if ( null !== $check ) {
return $check;
}
if ( ! is_string( $meta_key ) || '' === $meta_key ) {
return $check;
}
self::write( self::comment_table(), 'comment_id', (int) $object_id, $meta_key, $meta_value );
return true;
}
/**
* Filter callback: delete_comment_metadata.
*
* @param mixed $check Filter accumulator.
* @param int $object_id Comment ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Value-scoped (unused).
* @param bool $delete_all Delete-all flag (unused).
* @return mixed
*/
public static function on_comment_delete( $check, $object_id, $meta_key, $meta_value, $delete_all ) {
unset( $meta_value, $delete_all );
if ( null !== $check ) {
return $check;
}
if ( ! is_string( $meta_key ) || '' === $meta_key ) {
return $check;
}
self::delete_row( self::comment_table(), 'comment_id', (int) $object_id, $meta_key );
return true;
}
// ── Internal storage helpers ──────────────────────────────────────────────
/**
* Read a value from a misc bucket table.
*
* Returns the raw stored value (string), or null when no row exists.
* Caller must wrap in array for the get_*_metadata filter contract.
*
* @param string $table Fully-qualified table name.
* @param string $id_column 'term_id' or 'comment_id'.
* @param int $object_id Entity ID.
* @param string $meta_key Meta key.
* @return string|null
*/
private static function read( string $table, string $id_column, int $object_id, string $meta_key ): ?string {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$value = $wpdb->get_var(
$wpdb->prepare(
"SELECT meta_value FROM `{$table}` WHERE `{$id_column}` = %d AND meta_key = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$object_id,
$meta_key
)
);
// phpcs:enable
return ( null === $value ) ? null : (string) $value;
}
/**
* Write (INSERT or UPDATE) a value to a misc bucket table.
*
* Uses ON DUPLICATE KEY UPDATE — the (id, meta_key) primary key
* means each (entity, meta_key) pair has a single canonical row.
*
* @param string $table Fully-qualified table name.
* @param string $id_column 'term_id' or 'comment_id'.
* @param int $object_id Entity ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Value to store. Non-scalar types are serialized.
* @return void
*/
private static function write( string $table, string $id_column, int $object_id, string $meta_key, $meta_value ): void {
global $wpdb;
// Match WP convention: serialize arrays/objects, scalar values stored as-is.
$serialized = is_scalar( $meta_value ) || null === $meta_value
? (string) $meta_value
: maybe_serialize( $meta_value );
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query(
$wpdb->prepare(
"INSERT INTO `{$table}` (`{$id_column}`, meta_key, meta_value) VALUES (%d, %s, %s)
ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value)",
$object_id,
$meta_key,
$serialized
)
);
// phpcs:enable
}
/**
* Delete the row matching (object_id, meta_key) from a misc bucket table.
*
* @param string $table Fully-qualified table name.
* @param string $id_column 'term_id' or 'comment_id'.
* @param int $object_id Entity ID.
* @param string $meta_key Meta key.
* @return void
*/
private static function delete_row( string $table, string $id_column, int $object_id, string $meta_key ): void {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
$wpdb->prepare(
"DELETE FROM `{$table}` WHERE `{$id_column}` = %d AND meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$object_id,
$meta_key
)
);
// phpcs:enable
}
/**
* Count rows in a misc bucket table (for admin status panel).
*
* @param string $entity_type 'term' or 'comment'.
* @return int
*/
public static function count_rows( string $entity_type ): int {
global $wpdb;
$table = 'comment' === $entity_type ? self::comment_table() : self::term_table();
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$exists = (bool) $wpdb->get_var(
$wpdb->prepare( 'SHOW TABLES LIKE %s', $table )
);
if ( ! $exists ) {
return 0;
}
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
// phpcs:enable
}
}