b4400a68e5
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
264 lines
8.4 KiB
PHP
264 lines
8.4 KiB
PHP
<?php
|
|
/**
|
|
* Messages adapter — `hivepress-messages`.
|
|
*
|
|
* The Message model extends Comment (`comment_type = hp_message`) and stores
|
|
* the recipient_id in `comment_karma` — a creative re-use of an unused column,
|
|
* but it leaves the field unindexed for "messages received by user N"
|
|
* queries. Result: every dashboard "unread count" badge does an O(N)
|
|
* `WHERE comment_type = 'hp_message' AND comment_karma = N` scan over
|
|
* the entire wp_comments table.
|
|
*
|
|
* Fix: shadow table `wp_wpdo_comment_hp_message` with proper recipient_id
|
|
* column + index. Adapter mirrors every hp_message write into the shadow,
|
|
* and the comment-router rewrites recipient/unread queries to hit the index.
|
|
*
|
|
* Schema:
|
|
*
|
|
* CREATE TABLE wp_wpdo_comment_hp_message (
|
|
* comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
|
|
* sender_id BIGINT UNSIGNED NOT NULL,
|
|
* recipient_id BIGINT UNSIGNED NOT NULL,
|
|
* listing_id BIGINT UNSIGNED NOT NULL,
|
|
* is_read TINYINT(1) NOT NULL DEFAULT 0,
|
|
* sent_at DATETIME NOT NULL,
|
|
* KEY idx_recipient_unread (recipient_id, is_read),
|
|
* KEY idx_thread (listing_id, sent_at)
|
|
* );
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
* @since 3.0.0
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
if ( ! class_exists( 'TMDO_HivePress_Messages_Adapter' ) ) {
|
|
|
|
/**
|
|
* Messages addon adapter.
|
|
*/
|
|
final class TMDO_HivePress_Messages_Adapter implements TMDO_HivePress_Adapter {
|
|
|
|
use TMDO_HivePress_Adapter_Trait;
|
|
|
|
/** Comment_type owned by this adapter. */
|
|
public const COMMENT_TYPE = 'hp_message';
|
|
|
|
/** Shadow table name (without wpdb prefix). */
|
|
public const TABLE = 'wpdo_comment_hp_message';
|
|
|
|
/**
|
|
* Constructor — bind register hooks via inherited trait.
|
|
*/
|
|
public function __construct() {
|
|
$this->bind_anti_eav_hooks();
|
|
}
|
|
|
|
/** Adapter slug (matches wp.org plugin slug). */
|
|
public function plugin_slug(): string {
|
|
return 'hivepress-messages';
|
|
}
|
|
|
|
/** Class probed for addon presence. */
|
|
public function detection_class(): string {
|
|
return 'HivePress\\Messages\\Plugin';
|
|
}
|
|
|
|
/** Version constant probed for addon presence. */
|
|
public function detection_const(): string {
|
|
return 'HIVEPRESS_MESSAGES_VERSION';
|
|
}
|
|
|
|
/** Minimum addon version supported. */
|
|
public function minimum_addon_version(): string {
|
|
return '1.4.0';
|
|
}
|
|
|
|
/**
|
|
* Declare the shadow table to the Custom_Table_Registry.
|
|
*
|
|
* @param TMDO_Custom_Table_Registry $registry Registry singleton.
|
|
*/
|
|
public function on_register_custom_tables( TMDO_Custom_Table_Registry $registry ): void {
|
|
$this->register_custom_table(
|
|
$registry,
|
|
array(
|
|
'table_name' => self::TABLE,
|
|
'primary_key' => 'comment_id',
|
|
'post_type_link' => null,
|
|
'expected_columns' => array(
|
|
'comment_id' => 'BIGINT UNSIGNED NOT NULL PRIMARY KEY',
|
|
'sender_id' => 'BIGINT UNSIGNED NOT NULL',
|
|
'recipient_id' => 'BIGINT UNSIGNED NOT NULL',
|
|
'listing_id' => 'BIGINT UNSIGNED NOT NULL',
|
|
'is_read' => 'TINYINT(1) NOT NULL DEFAULT 0',
|
|
'sent_at' => 'DATETIME NOT NULL',
|
|
),
|
|
'indexes' => array(
|
|
'idx_recipient_unread' => '(recipient_id, is_read)',
|
|
'idx_thread' => '(listing_id, sent_at)',
|
|
),
|
|
'doctor_callback' => array( $this, 'doctor_check' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Bind sync hooks on hp_message comment insert / update / delete.
|
|
*
|
|
* The recipient_id is stored in comment_karma by HivePress; we mirror
|
|
* it into a properly-named indexed column. is_read tracks comment_approved
|
|
* (HP uses 0=unread / 1=read) — same column, mirrored.
|
|
*/
|
|
public function on_register_event_hooks(): void {
|
|
add_action( 'wp_insert_comment', array( $this, 'mirror_insert' ), 10, 2 );
|
|
add_action( 'wp_set_comment_status', array( $this, 'mirror_status_change' ), 10, 2 );
|
|
add_action( 'deleted_comment', array( $this, 'mirror_delete' ), 10, 2 );
|
|
}
|
|
|
|
/**
|
|
* Mirror a new hp_message into the shadow table.
|
|
*
|
|
* @param int $comment_id Comment ID.
|
|
* @param object|\WP_Comment $comment Comment object.
|
|
*/
|
|
public function mirror_insert( int $comment_id, $comment ): void {
|
|
if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) {
|
|
return;
|
|
}
|
|
global $wpdb;
|
|
$sender_id = (int) ( $comment->user_id ?? 0 );
|
|
$recipient_id = (int) ( $comment->comment_karma ?? 0 );
|
|
$listing_id = (int) ( $comment->comment_post_ID ?? 0 );
|
|
$is_read = (int) ( $comment->comment_approved ?? 0 );
|
|
if ( $sender_id <= 0 || $recipient_id <= 0 ) {
|
|
// Malformed message — nothing useful to mirror.
|
|
return;
|
|
}
|
|
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$wpdb->prepare(
|
|
'INSERT INTO `' . $wpdb->prefix . self::TABLE . '` (comment_id, sender_id, recipient_id, listing_id, is_read, sent_at) VALUES (%d, %d, %d, %d, %d, %s) ON DUPLICATE KEY UPDATE is_read = VALUES(is_read)',
|
|
$comment_id,
|
|
$sender_id,
|
|
$recipient_id,
|
|
$listing_id,
|
|
$is_read,
|
|
$comment->comment_date ?? current_time( 'mysql' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update is_read when a message is marked read (comment_approved flip).
|
|
*
|
|
* @param int $comment_id Comment ID.
|
|
* @param string $status New status ('approve', 'hold', 'spam', 'trash').
|
|
*/
|
|
public function mirror_status_change( int $comment_id, string $status ): void {
|
|
global $wpdb;
|
|
// Look up the comment to confirm it's hp_message before touching.
|
|
if ( ! function_exists( 'get_comment' ) ) {
|
|
return;
|
|
}
|
|
$comment = get_comment( $comment_id );
|
|
if ( ! $comment || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) {
|
|
return;
|
|
}
|
|
$is_read = ( 'approve' === $status ) ? 1 : 0;
|
|
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prefix . self::TABLE,
|
|
array( 'is_read' => $is_read ),
|
|
array( 'comment_id' => $comment_id ),
|
|
array( '%d' ),
|
|
array( '%d' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Remove shadow row on hp_message deletion.
|
|
*
|
|
* @param int $comment_id Comment ID.
|
|
* @param object|\WP_Comment $comment Comment object.
|
|
*/
|
|
public function mirror_delete( int $comment_id, $comment ): void {
|
|
if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) {
|
|
return;
|
|
}
|
|
global $wpdb;
|
|
$wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prefix . self::TABLE,
|
|
array( 'comment_id' => $comment_id ),
|
|
array( '%d' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Doctor probe — verify shadow table.
|
|
*
|
|
* @return array{ok:bool, message:string, details?:array<string,mixed>}
|
|
*/
|
|
public function doctor_check(): array {
|
|
global $wpdb;
|
|
if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) {
|
|
return array(
|
|
'ok' => false,
|
|
'message' => 'wpdb global not available',
|
|
);
|
|
}
|
|
$table = $wpdb->prefix . self::TABLE;
|
|
try {
|
|
$exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
|
|
if ( ! $exists ) {
|
|
return array(
|
|
'ok' => false,
|
|
'message' => sprintf( 'hivepress-messages: shadow table %s missing', $table ),
|
|
'details' => array( 'table' => $table ),
|
|
);
|
|
}
|
|
$rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
return array(
|
|
'ok' => true,
|
|
'message' => sprintf( 'hivepress-messages: %s rows=%d', $table, $rows ),
|
|
'details' => array(
|
|
'table' => $table,
|
|
'rows' => $rows,
|
|
),
|
|
);
|
|
} catch ( \Throwable $e ) {
|
|
return array(
|
|
'ok' => false,
|
|
'message' => sprintf( 'hivepress-messages doctor failed: %s', $e->getMessage() ),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 8-D self-score.
|
|
*
|
|
* D1: 1.0 — adapter never calls *_meta()
|
|
* D2: 1.0 — uses wpdb prefix exclusively
|
|
* D3: 1.0 — shadow table covers recipient/unread access
|
|
* D4: 1.0 — expected_columns + indexes registered
|
|
* D5: 1.0 — listens to wp_insert_comment + status change for sync
|
|
* D6: 1.0 — zero options/transients
|
|
* D7: 1.0 — only queries own shadow table
|
|
* D8: 1.0 — idx_recipient_unread covers the hot dashboard query
|
|
*/
|
|
public function suitability_score(): array {
|
|
return $this->compose_score( array() );
|
|
}
|
|
|
|
/**
|
|
* FSM modules this adapter contributes.
|
|
*
|
|
* @return array<int,string>
|
|
*/
|
|
public function migrations(): array {
|
|
return array( 'comment_hp_message' );
|
|
}
|
|
}
|
|
|
|
} // end if ( ! class_exists )
|