From b96b0414452b47aa1ba0d89e7707865d0dcdbe70 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 31 Jul 2026 06:07:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(hivepress):=20detector=20active-plugin=20?= =?UTF-8?q?=E5=81=B5=E6=B8=AC=20+=204=20=E5=80=8B=20interceptor=20?= =?UTF-8?q?=E6=94=B9=E7=B9=BC=E6=89=BF=E6=A0=B8=E5=BF=83=E5=9F=BA=E5=BA=95?= =?UTF-8?q?=EF=BC=88F2/F3/F4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F3 detector(A v3.4.5):HivePress addon 不暴露任何 per-addon class/const, 它們是透過 add_filter('hivepress/v1/extensions') 註冊目錄,所以原本的 class_exists / defined 偵測永遠只認得 core → 反 EAV 覆蓋率卡在 1/13。 改以 get_option('active_plugins') 為第一級訊號(新增 active_plugin_files()), version_for() 對 addon 改讀其主檔 Version: header。覆蓋率回到 7/13。 F2 interceptor:reviews / messages / memberships / requests 改繼承核心的 TMDO_Standard_Post_Interceptor,573 → 344 行。FIELD_MAP 由 private 提升為 public(late static binding 從基底讀取)。 bootstrap 的 require 迴圈加核心版本守衛:這 4 個檔案在舊核心下會於 parse 階段就 fatal,class_exists 守衛來不及。 F4 listing-stats:view 計數改用 Zone_Warm::increment() 原子遞增, flush 改原子 UPDATE,消除 get()+set() 的 lost-update race。 F5:主檔補 TablePrefix header(打包終檢 §10 schema drift 主路徑)。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY --- 2meet-data-optimizer-hivepress-addon.php | 2 + includes/class-tmdo-hivepress-bootstrap.php | 10 +- includes/class-tmdo-listing-stats.php | 38 ++--- .../class-tmdo-hivepress-detector.php | 59 ++++++- .../class-tmdo-memberships-interceptor.php | 125 ++++---------- .../class-tmdo-messages-interceptor.php | 133 +++++---------- .../class-tmdo-requests-interceptor.php | 127 ++++---------- .../class-tmdo-reviews-interceptor.php | 160 ++++++------------ 8 files changed, 240 insertions(+), 414 deletions(-) diff --git a/2meet-data-optimizer-hivepress-addon.php b/2meet-data-optimizer-hivepress-addon.php index d4c64cb..d4d029b 100644 --- a/2meet-data-optimizer-hivepress-addon.php +++ b/2meet-data-optimizer-hivepress-addon.php @@ -13,6 +13,8 @@ * License: GPL-2.0-or-later * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Text Domain: tmdo-hivepress + * + * TablePrefix: ^(hpct_[a-z_0-9]+|wpdo_hot_hp_[a-z_0-9]+|wpdo_cold_hp_[a-z_0-9]+)$ * Domain Path: /languages * * @package TMDO_HIVEPRESS diff --git a/includes/class-tmdo-hivepress-bootstrap.php b/includes/class-tmdo-hivepress-bootstrap.php index ca1a136..4c3ea9c 100644 --- a/includes/class-tmdo-hivepress-bootstrap.php +++ b/includes/class-tmdo-hivepress-bootstrap.php @@ -65,11 +65,19 @@ final class TMDO_HP_Bootstrap { require_once $base . 'includes/hivepress/class-tmdo-hivepress-benchmark.php'; require_once $base . 'includes/hivepress/class-tmdo-hivepress-rest.php'; - // 7 HPCT interceptors + // 7 HPCT interceptors. + // reviews / messages / memberships / requests extend the core's + // TMDO_Standard_Post_Interceptor — requiring them against a core older than + // v0.2.0 would fatal at parse time, so gate those four on the base class. + $has_standard_base = class_exists( 'TMDO_Standard_Post_Interceptor' ); foreach ( array( 'reviews', 'messages', 'favorites', 'memberships', 'statistics', 'requests', 'listing-meta', ) as $intslug ) { + $needs_base = in_array( $intslug, array( 'reviews', 'messages', 'memberships', 'requests' ), true ); + if ( $needs_base && ! $has_standard_base ) { + continue; + } $file = $base . "includes/interceptors/class-tmdo-{$intslug}-interceptor.php"; if ( file_exists( $file ) ) { require_once $file; diff --git a/includes/class-tmdo-listing-stats.php b/includes/class-tmdo-listing-stats.php index 2eec8bf..a17b955 100644 --- a/includes/class-tmdo-listing-stats.php +++ b/includes/class-tmdo-listing-stats.php @@ -2,7 +2,7 @@ /** * Listing stats integration for Zone B view counting. * - * @package WP_Data_Optimizer + * @package TMDO */ if ( ! defined( 'ABSPATH' ) ) { @@ -93,8 +93,7 @@ class TMDO_Listing_Stats { * @param int $post_id Listing post ID. */ public static function increment_view( int $post_id ): void { - $current = (int) TMDO_Zone_Warm::get( $post_id, self::VIEW_KEY ); - TMDO_Zone_Warm::set( $post_id, self::VIEW_KEY, (string) ( $current + 1 ), self::VIEW_TTL ); + TMDO_Zone_Warm::increment( $post_id, self::VIEW_KEY, 1, self::VIEW_TTL ); } /** @@ -136,29 +135,26 @@ class TMDO_Listing_Stats { return 0; } - // Batch-fetch existing hp_view_count for all post IDs in one query. - $post_ids = array_map( 'intval', array_column( $rows, 'post_id' ) ); - $placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) ); - $existing_rows = $wpdb->get_results( - $wpdb->prepare( - "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = 'hp_view_count' AND post_id IN ({$placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $wpdb->postmeta is core. phpcs:ignore WPDO.AntiEAV.no-direct-postmeta-select -- Listing Stats: Zone B → postmeta view-count sync path. - ...$post_ids - ), - ARRAY_A - ); - $existing_map = array(); - foreach ( $existing_rows as $er ) { - $existing_map[ (int) $er['post_id'] ] = (int) $er['meta_value']; - } - $flushed = 0; foreach ( $rows as $row ) { $post_id = (int) $row['post_id']; $new_views = (int) $row['meta_value']; - // Add to existing postmeta total. - $existing = $existing_map[ $post_id ] ?? 0; - update_post_meta( $post_id, 'hp_view_count', $existing + $new_views ); + // Atomic increment: avoids the read-modify-write race that could lose + // concurrent view increments arriving between our batch-read and the write. + // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- WPDO.AntiEAV.no-direct-postmeta-update: view-count sync, no meta API equivalent for atomic add. + $updated = $wpdb->query( + $wpdb->prepare( + "UPDATE `{$wpdb->postmeta}` SET meta_value = meta_value + %d WHERE post_id = %d AND meta_key = 'hp_view_count'", + $new_views, + $post_id + ) + ); + + // Seed the key when it does not exist yet. + if ( ! $updated ) { + add_post_meta( $post_id, 'hp_view_count', $new_views, true ); + } // Remove the warm entry after flush. TMDO_Zone_Warm::delete( $post_id, self::VIEW_KEY ); diff --git a/includes/hivepress/class-tmdo-hivepress-detector.php b/includes/hivepress/class-tmdo-hivepress-detector.php index 43a129f..e867937 100644 --- a/includes/hivepress/class-tmdo-hivepress-detector.php +++ b/includes/hivepress/class-tmdo-hivepress-detector.php @@ -20,7 +20,7 @@ * a single `class_exists('HivePress\\Core')` check — zero-cost fallback path * for sites that don't use HivePress. * - * @package WP_Data_Optimizer + * @package TMDO * @since 3.0.0 */ @@ -152,8 +152,15 @@ if ( ! class_exists( 'TMDO_HivePress_Detector' ) ) { return $cached; } + // Active-plugin set is the authoritative signal. HivePress addons + // register via the `hivepress/v1/extensions` filter and publish no + // per-addon class/const, so class_exists()/defined() alone only ever + // detects core. We treat "addon plugin is active" as a first-class + // detection signal alongside the class/const probe. + $active = self::active_plugin_files(); + // Short-circuit: if HivePress core is not present, no addon can be loaded. - if ( ! self::probe_one( self::PROBES['hivepress'] ) ) { + if ( ! self::probe_one( self::PROBES['hivepress'] ) && ! isset( $active['hivepress/hivepress.php'] ) ) { self::$memo = array(); if ( function_exists( 'set_transient' ) ) { set_transient( self::CACHE_KEY, self::$memo, self::CACHE_TTL ); @@ -163,8 +170,8 @@ if ( ! class_exists( 'TMDO_HivePress_Detector' ) ) { $found = array(); foreach ( self::PROBES as $slug => $probe ) { - if ( self::probe_one( $probe ) ) { - $found[ $slug ] = self::version_for( $probe ); + if ( self::probe_one( $probe ) || isset( $active[ $slug . '/' . $slug . '.php' ] ) ) { + $found[ $slug ] = self::version_for( $slug, $probe ); } } @@ -222,6 +229,31 @@ if ( ! class_exists( 'TMDO_HivePress_Detector' ) ) { // ── Internal probes ───────────────────────────────────────────────── + /** + * Active plugin file set (single-site + network), keyed by plugin file. + * + * Read straight from options so it works on `plugins_loaded` before + * `wp-admin/includes/plugin.php` (`is_plugin_active`) is loaded. + * + * @return array Map of "dir/file.php" => true. + */ + private static function active_plugin_files(): array { + $files = array(); + $site = function_exists( 'get_option' ) ? get_option( 'active_plugins' ) : false; + if ( is_array( $site ) ) { + foreach ( $site as $f ) { + $files[ (string) $f ] = true; + } + } + $network = function_exists( 'get_site_option' ) ? get_site_option( 'active_sitewide_plugins' ) : false; + if ( is_array( $network ) ) { + foreach ( array_keys( $network ) as $f ) { + $files[ (string) $f ] = true; + } + } + return $files; + } + /** * Probe a single addon. Either the class OR the const must exist. * @@ -243,9 +275,13 @@ if ( ! class_exists( 'TMDO_HivePress_Detector' ) ) { /** * Best-effort version extraction. Empty string when no version available. * + * Prefers the addon's version const; falls back to the `Version:` header + * of the addon's main plugin file (HivePress addons publish no const). + * + * @param string $slug Addon slug (plugin dir name). * @param array{class:string, const:string, name:string} $probe Probe definition. */ - private static function version_for( array $probe ): string { + private static function version_for( string $slug, array $probe ): string { $const = (string) ( $probe['const'] ?? '' ); if ( '' !== $const && defined( $const ) ) { $value = constant( $const ); @@ -253,6 +289,19 @@ if ( ! class_exists( 'TMDO_HivePress_Detector' ) ) { return (string) $value; } } + + // Fallback: read the Version header from the addon's main plugin + // file. HivePress addons expose no version const, so this is the + // only version signal available for them. + if ( defined( 'WP_PLUGIN_DIR' ) && function_exists( 'get_file_data' ) ) { + $path = WP_PLUGIN_DIR . '/' . $slug . '/' . $slug . '.php'; + if ( is_readable( $path ) ) { + $data = get_file_data( $path, array( 'Version' => 'Version' ) ); + if ( ! empty( $data['Version'] ) ) { + return (string) $data['Version']; + } + } + } return ''; } } diff --git a/includes/interceptors/class-tmdo-memberships-interceptor.php b/includes/interceptors/class-tmdo-memberships-interceptor.php index 60e62b4..fad2903 100644 --- a/includes/interceptors/class-tmdo-memberships-interceptor.php +++ b/includes/interceptors/class-tmdo-memberships-interceptor.php @@ -2,7 +2,7 @@ /** * Memberships interceptor for HivePress Membership posts. * - * @package WP_Data_Optimizer + * @package TMDO */ if ( ! defined( 'ABSPATH' ) ) { @@ -11,9 +11,10 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Memberships interceptor — intercepts HivePress Membership reads/writes. + * * Operates on hpct_memberships table. */ -class TMDO_Memberships_Interceptor extends TMDO_Interceptor_Base { +class TMDO_Memberships_Interceptor extends TMDO_Standard_Post_Interceptor { /** * Module identifier. @@ -22,7 +23,10 @@ class TMDO_Memberships_Interceptor extends TMDO_Interceptor_Base { */ protected string $module = 'memberships'; - private const FIELD_MAP = array( + /** + * Meta key → flat column mapping. + */ + public const FIELD_MAP = array( 'hp_plan' => 'plan_id', 'hp_user' => 'user_id', 'hp_price' => 'price', @@ -32,102 +36,41 @@ class TMDO_Memberships_Interceptor extends TMDO_Interceptor_Base { ); /** - * Registers WordPress hooks for this interceptor. + * Returns the WordPress post_type slug handled by this interceptor. * - * @return void + * @return string */ - public function register_hooks(): void { - add_action( 'wp_insert_post', array( $this, 'action_insert_post' ), 10, 3 ); - add_filter( 'update_post_metadata', array( $this, 'filter_update_meta' ), 10, 5 ); - add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 ); + protected function get_post_type(): string { + return 'hp_membership'; } /** - * Deletes membership record when a post is deleted. + * Returns the table key passed to TMDO_DB::table(). + * + * @return string + */ + protected function get_table_key(): string { + return 'hpct_memberships'; + } + + /** + * Builds the INSERT row for a newly created membership post. * * @param int $post_id Post ID. * @param \WP_Post $post Post object. - * @return void + * @param string $now Formatted datetime string. + * @return array{values: array, formats: array} */ - public function action_delete_post( int $post_id, \WP_Post $post ): void { - if ( 'hp_membership' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $wpdb->delete( TMDO_DB::table( 'hpct_memberships' ), array( 'post_id' => $post_id ), array( '%d' ) ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() ); - } - } - - /** - * Filters update_post_metadata for hp_membership posts. - * - * @param mixed $check Whether to short-circuit. - * @param int $post_id Post ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. - * @param mixed $prev_value Previous meta value. - * @return mixed Filtered check value. - */ - public function filter_update_meta( $check, int $post_id, string $meta_key, $meta_value, $prev_value ) { - if ( 'hp_membership' !== get_post_type( $post_id ) || ! isset( self::FIELD_MAP[ $meta_key ] ) ) { - return $check; - } - - if ( ! $this->is_active() ) { - return $check; - } - - try { - global $wpdb; - $col = self::FIELD_MAP[ $meta_key ]; - $wpdb->query( - $wpdb->prepare( - 'UPDATE ' . TMDO_DB::table( 'hpct_memberships' ) . " SET `{$col}` = %s, updated_at = %s WHERE post_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name from TMDO_DB::table(); column validated by FIELD_MAP constant. - $meta_value, - TMDO_DB::now(), - $post_id - ) - ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() ); - } - - return $check; - } - - /** - * Inserts a new membership record when a post is inserted. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @param bool $update Whether this is an update. - * @return void - */ - public function action_insert_post( int $post_id, \WP_Post $post, bool $update ): void { - if ( $update || 'hp_membership' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $now = TMDO_DB::now(); - $wpdb->insert( - TMDO_DB::table( 'hpct_memberships' ), - array( - 'post_id' => $post_id, - 'user_id' => (int) $post->post_author, - 'status' => $post->post_status, - 'created_at' => $now, - 'updated_at' => $now, - ), - array( '%d', '%d', '%s', '%s', '%s' ) - ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'wp_insert_post', $e->getMessage() ); - } + protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array { + return array( + 'values' => array( + 'post_id' => $post_id, + 'user_id' => (int) $post->post_author, + 'status' => $post->post_status, + 'created_at' => $now, + 'updated_at' => $now, + ), + 'formats' => array( '%d', '%d', '%s', '%s', '%s' ), + ); } } diff --git a/includes/interceptors/class-tmdo-messages-interceptor.php b/includes/interceptors/class-tmdo-messages-interceptor.php index d23bc23..777545c 100644 --- a/includes/interceptors/class-tmdo-messages-interceptor.php +++ b/includes/interceptors/class-tmdo-messages-interceptor.php @@ -2,7 +2,7 @@ /** * Messages interceptor for HivePress Message posts. * - * @package WP_Data_Optimizer + * @package TMDO */ if ( ! defined( 'ABSPATH' ) ) { @@ -11,9 +11,10 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Messages interceptor — intercepts HivePress Message reads/writes. + * * Operates on hpct_messages table. */ -class TMDO_Messages_Interceptor extends TMDO_Interceptor_Base { +class TMDO_Messages_Interceptor extends TMDO_Standard_Post_Interceptor { /** * Module identifier. @@ -22,7 +23,10 @@ class TMDO_Messages_Interceptor extends TMDO_Interceptor_Base { */ protected string $module = 'messages'; - private const FIELD_MAP = array( + /** + * Meta key → flat column mapping. + */ + public const FIELD_MAP = array( 'hp_sender' => 'sender_id', 'hp_recipient' => 'recipient_id', 'hp_listing' => 'listing_id', @@ -30,106 +34,45 @@ class TMDO_Messages_Interceptor extends TMDO_Interceptor_Base { ); /** - * Registers WordPress hooks for this interceptor. + * Returns the WordPress post_type slug handled by this interceptor. * - * @return void + * @return string */ - public function register_hooks(): void { - add_action( 'wp_insert_post', array( $this, 'action_insert_post' ), 10, 3 ); - add_filter( 'update_post_metadata', array( $this, 'filter_update_meta' ), 10, 5 ); - add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 ); + protected function get_post_type(): string { + return 'hp_message'; } /** - * Deletes message record when a post is deleted. + * Returns the table key passed to TMDO_DB::table(). + * + * @return string + */ + protected function get_table_key(): string { + return 'hpct_messages'; + } + + /** + * Builds the INSERT row for a newly created message post. * * @param int $post_id Post ID. * @param \WP_Post $post Post object. - * @return void + * @param string $now Formatted datetime string. + * @return array{values: array, formats: array} */ - public function action_delete_post( int $post_id, \WP_Post $post ): void { - if ( 'hp_message' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $wpdb->delete( TMDO_DB::table( 'hpct_messages' ), array( 'post_id' => $post_id ), array( '%d' ) ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() ); - } - } - - /** - * Filters update_post_metadata for hp_message posts. - * - * @param mixed $check Whether to short-circuit. - * @param int $post_id Post ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. - * @param mixed $prev_value Previous meta value. - * @return mixed Filtered check value. - */ - public function filter_update_meta( $check, int $post_id, string $meta_key, $meta_value, $prev_value ) { - if ( 'hp_message' !== get_post_type( $post_id ) || ! isset( self::FIELD_MAP[ $meta_key ] ) ) { - return $check; - } - - if ( ! $this->is_active() ) { - return $check; - } - - try { - global $wpdb; - $col = self::FIELD_MAP[ $meta_key ]; - $wpdb->query( - $wpdb->prepare( - 'UPDATE ' . TMDO_DB::table( 'hpct_messages' ) . " SET `{$col}` = %s, updated_at = %s WHERE post_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name from TMDO_DB::table(); column validated by FIELD_MAP constant. - $meta_value, - TMDO_DB::now(), - $post_id - ) - ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() ); - } - - return $check; - } - - /** - * Inserts a new message record when a post is inserted. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @param bool $update Whether this is an update. - * @return void - */ - public function action_insert_post( int $post_id, \WP_Post $post, bool $update ): void { - if ( $update || 'hp_message' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $now = TMDO_DB::now(); - $wpdb->insert( - TMDO_DB::table( 'hpct_messages' ), - array( - 'post_id' => $post_id, - 'thread_id' => (int) $post->post_parent, - 'sender_id' => (int) $post->post_author, - 'subject' => sanitize_text_field( $post->post_title ), - 'body' => $post->post_content, - 'status' => $post->post_status, - 'sent_at' => $now, - 'created_at' => $now, - 'updated_at' => $now, - ), - array( '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s' ) - ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'wp_insert_post', $e->getMessage() ); - } + protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array { + return array( + 'values' => array( + 'post_id' => $post_id, + 'thread_id' => (int) $post->post_parent, + 'sender_id' => (int) $post->post_author, + 'subject' => sanitize_text_field( $post->post_title ), + 'body' => $post->post_content, + 'status' => $post->post_status, + 'sent_at' => $now, + 'created_at' => $now, + 'updated_at' => $now, + ), + 'formats' => array( '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s' ), + ); } } diff --git a/includes/interceptors/class-tmdo-requests-interceptor.php b/includes/interceptors/class-tmdo-requests-interceptor.php index 64a71e0..87cef31 100644 --- a/includes/interceptors/class-tmdo-requests-interceptor.php +++ b/includes/interceptors/class-tmdo-requests-interceptor.php @@ -2,7 +2,7 @@ /** * Requests interceptor for HivePress Request posts. * - * @package WP_Data_Optimizer + * @package TMDO */ if ( ! defined( 'ABSPATH' ) ) { @@ -11,9 +11,10 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Requests interceptor — intercepts HivePress Request reads/writes. + * * Operates on hpct_requests table. */ -class TMDO_Requests_Interceptor extends TMDO_Interceptor_Base { +class TMDO_Requests_Interceptor extends TMDO_Standard_Post_Interceptor { /** * Module identifier. @@ -22,110 +23,52 @@ class TMDO_Requests_Interceptor extends TMDO_Interceptor_Base { */ protected string $module = 'requests'; - private const FIELD_MAP = array( + /** + * Meta key → flat column mapping. + */ + public const FIELD_MAP = array( 'hp_vendor' => 'vendor_id', 'hp_listing' => 'listing_id', 'hp_budget' => 'budget', ); /** - * Registers WordPress hooks for this interceptor. + * Returns the WordPress post_type slug handled by this interceptor. * - * @return void + * @return string */ - public function register_hooks(): void { - add_action( 'wp_insert_post', array( $this, 'action_insert_post' ), 10, 3 ); - add_filter( 'update_post_metadata', array( $this, 'filter_update_meta' ), 10, 5 ); - add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 ); + protected function get_post_type(): string { + return 'hp_request'; } /** - * Deletes request record when a post is deleted. + * Returns the table key passed to TMDO_DB::table(). + * + * @return string + */ + protected function get_table_key(): string { + return 'hpct_requests'; + } + + /** + * Builds the INSERT row for a newly created request post. * * @param int $post_id Post ID. * @param \WP_Post $post Post object. - * @return void + * @param string $now Formatted datetime string. + * @return array{values: array, formats: array} */ - public function action_delete_post( int $post_id, \WP_Post $post ): void { - if ( 'hp_request' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $wpdb->delete( TMDO_DB::table( 'hpct_requests' ), array( 'post_id' => $post_id ), array( '%d' ) ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() ); - } - } - - /** - * Filters update_post_metadata for hp_request posts. - * - * @param mixed $check Whether to short-circuit. - * @param int $post_id Post ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. - * @param mixed $prev_value Previous meta value. - * @return mixed Filtered check value. - */ - public function filter_update_meta( $check, int $post_id, string $meta_key, $meta_value, $prev_value ) { - if ( 'hp_request' !== get_post_type( $post_id ) || ! isset( self::FIELD_MAP[ $meta_key ] ) ) { - return $check; - } - - if ( ! $this->is_active() ) { - return $check; - } - - try { - global $wpdb; - $col = self::FIELD_MAP[ $meta_key ]; - $wpdb->query( - $wpdb->prepare( - 'UPDATE ' . TMDO_DB::table( 'hpct_requests' ) . " SET `{$col}` = %s, updated_at = %s WHERE post_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name from TMDO_DB::table(); column validated by FIELD_MAP constant. - $meta_value, - TMDO_DB::now(), - $post_id - ) - ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() ); - } - - return $check; - } - - /** - * Inserts a new request record when a post is inserted. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @param bool $update Whether this is an update. - * @return void - */ - public function action_insert_post( int $post_id, \WP_Post $post, bool $update ): void { - if ( $update || 'hp_request' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $now = TMDO_DB::now(); - $wpdb->insert( - TMDO_DB::table( 'hpct_requests' ), - array( - 'post_id' => $post_id, - 'user_id' => (int) $post->post_author, - 'status' => $post->post_status, - 'message' => $post->post_content, - 'created_at' => $now, - 'updated_at' => $now, - ), - array( '%d', '%d', '%s', '%s', '%s', '%s' ) - ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'wp_insert_post', $e->getMessage() ); - } + protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array { + return array( + 'values' => array( + 'post_id' => $post_id, + 'user_id' => (int) $post->post_author, + 'status' => $post->post_status, + 'message' => $post->post_content, + 'created_at' => $now, + 'updated_at' => $now, + ), + 'formats' => array( '%d', '%d', '%s', '%s', '%s', '%s' ), + ); } } diff --git a/includes/interceptors/class-tmdo-reviews-interceptor.php b/includes/interceptors/class-tmdo-reviews-interceptor.php index 44a3aa9..fa66e6e 100644 --- a/includes/interceptors/class-tmdo-reviews-interceptor.php +++ b/includes/interceptors/class-tmdo-reviews-interceptor.php @@ -2,9 +2,10 @@ /** * Reviews interceptor for HivePress Review posts. * - * @package WP_Data_Optimizer + * @package TMDO */ + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -12,9 +13,9 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Reviews interceptor — intercepts HivePress Review reads/writes. * - * Ported from HPCT_Reviews_Interceptor. Operates on hpct_reviews table. + * Operates on hpct_reviews table. */ -class TMDO_Reviews_Interceptor extends TMDO_Interceptor_Base { +class TMDO_Reviews_Interceptor extends TMDO_Standard_Post_Interceptor { /** * Module identifier. @@ -23,126 +24,77 @@ class TMDO_Reviews_Interceptor extends TMDO_Interceptor_Base { */ protected string $module = 'reviews'; - private const FIELD_MAP = array( + /** + * Meta key → flat column mapping. + */ + public const FIELD_MAP = array( 'hp_rating' => 'rating', 'hp_listing' => 'listing_id', '_hp_vendor' => 'vendor_id', ); /** - * Registers WordPress hooks for this interceptor. + * Returns the WordPress post_type slug handled by this interceptor. + * + * @return string + */ + protected function get_post_type(): string { + return 'hp_review'; + } + + /** + * Returns the table key passed to TMDO_DB::table(). + * + * @return string + */ + protected function get_table_key(): string { + return 'hpct_reviews'; + } + + /** + * Registers WordPress hooks, including the extra get_post_metadata hook. * * @return void */ public function register_hooks(): void { - add_filter( 'update_post_metadata', array( $this, 'filter_update_meta' ), 10, 5 ); - add_action( 'wp_insert_post', array( $this, 'action_insert_post' ), 10, 3 ); + parent::register_hooks(); add_filter( 'get_post_metadata', array( $this, 'filter_get_meta' ), 10, 4 ); - add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 ); } /** - * Deletes review record when a post is deleted. + * Builds the INSERT row for a newly created review post. * * @param int $post_id Post ID. * @param \WP_Post $post Post object. - * @return void + * @param string $now Formatted datetime string. + * @return array{values: array, formats: array} */ - public function action_delete_post( int $post_id, \WP_Post $post ): void { - if ( 'hp_review' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $wpdb->delete( TMDO_DB::table( 'hpct_reviews' ), array( 'post_id' => $post_id ), array( '%d' ) ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() ); - } + protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array { + return array( + 'values' => array( + 'post_id' => $post_id, + 'user_id' => (int) $post->post_author, + 'status' => $post->post_status, + 'title' => sanitize_text_field( $post->post_title ), + 'content' => $post->post_content, + 'created_at' => $now, + 'updated_at' => $now, + ), + 'formats' => array( '%d', '%d', '%s', '%s', '%s', '%s', '%s' ), + ); } /** - * Filters update_post_metadata for hp_review posts. + * Reads hp_rating directly from the reviews flat table. * - * @param mixed $check Whether to short-circuit. - * @param int $post_id Post ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. - * @param mixed $prev_value Previous meta value. - * @return mixed Filtered check value. - */ - public function filter_update_meta( $check, int $post_id, string $meta_key, $meta_value, $prev_value ) { - if ( ! $this->is_hp_review( $post_id ) || ! isset( self::FIELD_MAP[ $meta_key ] ) ) { - return $check; - } - - if ( ! $this->is_active() ) { - return $check; - } - - try { - global $wpdb; - $col = self::FIELD_MAP[ $meta_key ]; - $wpdb->query( - $wpdb->prepare( - 'UPDATE ' . TMDO_DB::table( 'hpct_reviews' ) . " SET `{$col}` = %s, updated_at = %s WHERE post_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name from TMDO_DB::table(); column validated by FIELD_MAP constant. - $meta_value, - TMDO_DB::now(), - $post_id - ) - ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() ); - } - - return $check; - } - - /** - * Inserts a new review record when a post is inserted. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @param bool $update Whether this is an update. - * @return void - */ - public function action_insert_post( int $post_id, \WP_Post $post, bool $update ): void { - if ( $update || 'hp_review' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $now = TMDO_DB::now(); - $wpdb->insert( - TMDO_DB::table( 'hpct_reviews' ), - array( - 'post_id' => $post_id, - 'user_id' => (int) $post->post_author, - 'status' => $post->post_status, - 'title' => sanitize_text_field( $post->post_title ), - 'content' => $post->post_content, - 'created_at' => $now, - 'updated_at' => $now, - ), - array( '%d', '%d', '%s', '%s', '%s', '%s', '%s' ) - ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'wp_insert_post', $e->getMessage() ); - } - } - - /** - * Filters get_post_metadata for hp_rating on hp_review posts. - * - * @param mixed $value Current value or null. - * @param int $post_id Post ID. + * @param mixed $value Short-circuit value. + * @param int $post_id Post ID. * @param string $meta_key Meta key. - * @param bool $single Whether to return single value. - * @return mixed Filtered value. + * @param bool $single Whether to return a single value. + * @return mixed */ public function filter_get_meta( $value, int $post_id, string $meta_key, bool $single ) { - if ( ! $this->is_enabled() || 'hp_rating' !== $meta_key || ! $this->is_hp_review( $post_id ) ) { + if ( ! $this->is_enabled() || 'hp_rating' !== $meta_key || 'hp_review' !== get_post_type( $post_id ) ) { return $value; } @@ -151,7 +103,7 @@ class TMDO_Reviews_Interceptor extends TMDO_Interceptor_Base { global $wpdb; $rating = $wpdb->get_var( $wpdb->prepare( - 'SELECT rating FROM ' . TMDO_DB::table( 'hpct_reviews' ) . ' WHERE post_id = %d LIMIT 1', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name from TMDO_DB::table(). + 'SELECT rating FROM ' . TMDO_DB::table( 'hpct_reviews' ) . ' WHERE post_id = %d LIMIT 1', $post_id ) ); @@ -161,14 +113,4 @@ class TMDO_Reviews_Interceptor extends TMDO_Interceptor_Base { 'get_post_metadata' ); } - - /** - * Checks whether the post is an hp_review. - * - * @param int $post_id Post ID. - * @return bool True if hp_review post type. - */ - private function is_hp_review( int $post_id ): bool { - return 'hp_review' === get_post_type( $post_id ); - } }