d36bb954d1
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
413 lines
14 KiB
PHP
413 lines
14 KiB
PHP
<?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 0–3:
|
||
*
|
||
* - 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
|
||
*/
|
||
|
||
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
|
||
}
|
||
}
|