refactor(2meet-data-optimizer-hivepress-addon): remove HPCT-era interceptors and query rewriters
- Delete 7 interceptors + 5 query rewriters that read/write hpct_* tables no site has (only an empty hpct_latepoint_idx exists) - Drop their loading/registration in TMDO_HP_Bootstrap, back-compat aliases, integration bootstrap, and ListingMetaInterceptorTest - Verify: unit 141 OK, integration 49 OK, bootstrap smoke OK; phpcs 34E/40W → 25E/28W - PR: PR-014
This commit is contained in:
@@ -20,7 +20,6 @@ add_action(
|
||||
$aliases = array(
|
||||
'TMDO_Admin_HivePress' => 'WPDO_Admin_HivePress',
|
||||
'TMDO_CLI_HivePress' => 'WPDO_CLI_HivePress',
|
||||
'TMDO_Favorites_Interceptor' => 'WPDO_Favorites_Interceptor',
|
||||
'TMDO_HPCT_Import' => 'WPDO_HPCT_Import',
|
||||
'TMDO_HP_Bootstrap' => 'WPDO_HP_Bootstrap',
|
||||
'TMDO_HivePress' => 'WPDO_HivePress',
|
||||
@@ -47,18 +46,7 @@ add_action(
|
||||
'TMDO_HivePress_Tags_Adapter' => 'WPDO_HivePress_Tags_Adapter',
|
||||
'TMDO_Hivepress_Term_Comment_Fields' => 'WPDO_Hivepress_Term_Comment_Fields',
|
||||
'TMDO_Hivepress_Transient_Filter' => 'WPDO_Hivepress_Transient_Filter',
|
||||
'TMDO_Listing_Meta_Interceptor' => 'WPDO_Listing_Meta_Interceptor',
|
||||
'TMDO_Listing_Meta_Query' => 'WPDO_Listing_Meta_Query',
|
||||
'TMDO_Listing_Stats' => 'WPDO_Listing_Stats',
|
||||
'TMDO_Memberships_Interceptor' => 'WPDO_Memberships_Interceptor',
|
||||
'TMDO_Memberships_Query' => 'WPDO_Memberships_Query',
|
||||
'TMDO_Messages_Interceptor' => 'WPDO_Messages_Interceptor',
|
||||
'TMDO_Messages_Query' => 'WPDO_Messages_Query',
|
||||
'TMDO_Requests_Interceptor' => 'WPDO_Requests_Interceptor',
|
||||
'TMDO_Requests_Query' => 'WPDO_Requests_Query',
|
||||
'TMDO_Reviews_Interceptor' => 'WPDO_Reviews_Interceptor',
|
||||
'TMDO_Reviews_Query' => 'WPDO_Reviews_Query',
|
||||
'TMDO_Statistics_Interceptor' => 'WPDO_Statistics_Interceptor',
|
||||
);
|
||||
foreach ( $aliases as $tmdo_class => $wpdo_alias ) {
|
||||
if ( class_exists( $tmdo_class, false ) && ! class_exists( $wpdo_alias, false ) ) {
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
/**
|
||||
* Bootstrap for 2meet Data Optimizer HivePress AddOn.
|
||||
*
|
||||
* 載入 13 adapter + 7 interceptor + 5 query interceptor + 補助組件 +
|
||||
* admin tab + CLI namespace。
|
||||
* 載入 13 adapter + 補助組件 + admin tab + CLI namespace。
|
||||
*
|
||||
* 注意:HP 的 V3.0.0 family bootstrap 內部也有一個 class TMDO_HivePress_Bootstrap
|
||||
* (在 includes/hivepress/class-tmdo-hivepress-bootstrap.php)。為避免衝突,本
|
||||
@@ -64,35 +63,6 @@ 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.
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 5 query interceptors
|
||||
foreach ( array(
|
||||
'reviews', 'messages', 'memberships', 'requests', 'listing-meta',
|
||||
) as $qslug ) {
|
||||
$file = $base . "includes/query/class-tmdo-{$qslug}-query.php";
|
||||
if ( file_exists( $file ) ) {
|
||||
require_once $file;
|
||||
}
|
||||
}
|
||||
|
||||
// Misc HP integration files
|
||||
require_once $base . 'includes/class-tmdo-hivepress.php';
|
||||
require_once $base . 'includes/class-tmdo-hivepress-transient-filter.php';
|
||||
@@ -110,34 +80,6 @@ final class TMDO_HP_Bootstrap {
|
||||
TMDO_HivePress_Bootstrap::boot();
|
||||
}
|
||||
|
||||
// 7 HPCT interceptors
|
||||
foreach ( array(
|
||||
'TMDO_Reviews_Interceptor',
|
||||
'TMDO_Messages_Interceptor',
|
||||
'TMDO_Favorites_Interceptor',
|
||||
'TMDO_Memberships_Interceptor',
|
||||
'TMDO_Statistics_Interceptor',
|
||||
'TMDO_Requests_Interceptor',
|
||||
'TMDO_Listing_Meta_Interceptor',
|
||||
) as $cls ) {
|
||||
if ( class_exists( $cls ) ) {
|
||||
( new $cls() )->register_hooks();
|
||||
}
|
||||
}
|
||||
|
||||
// 5 query interceptors
|
||||
foreach ( array(
|
||||
'TMDO_Reviews_Query',
|
||||
'TMDO_Messages_Query',
|
||||
'TMDO_Memberships_Query',
|
||||
'TMDO_Requests_Query',
|
||||
'TMDO_Listing_Meta_Query',
|
||||
) as $cls ) {
|
||||
if ( class_exists( $cls ) ) {
|
||||
( new $cls() )->register_hooks();
|
||||
}
|
||||
}
|
||||
|
||||
// HP transient filter (init:5)
|
||||
if ( class_exists( 'TMDO_Hivepress_Transient_Filter' ) ) {
|
||||
add_action( 'init', array( 'TMDO_Hivepress_Transient_Filter', 'init' ), 5 );
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Favorites interceptor for HivePress user favorites.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Favorites interceptor — syncs HivePress user favorites to hpct_favorites table.
|
||||
*
|
||||
* Special: operates on user_meta (hp_favorited_listings), not post_meta.
|
||||
*/
|
||||
class TMDO_Favorites_Interceptor extends TMDO_Interceptor_Base {
|
||||
|
||||
/**
|
||||
* Module identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $module = 'favorites';
|
||||
|
||||
/**
|
||||
* Registers WordPress hooks for this interceptor.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks(): void {
|
||||
add_filter( 'update_user_metadata', array( $this, 'filter_update_user_meta' ), 10, 5 );
|
||||
add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters update_user_metadata to sync hp_favorited_listings.
|
||||
*
|
||||
* @param mixed $check Whether to short-circuit.
|
||||
* @param int $user_id User 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_user_meta( $check, int $user_id, string $meta_key, $meta_value, $prev_value ) {
|
||||
if ( 'hp_favorited_listings' !== $meta_key || ! $this->is_active() ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->sync_favorites( $user_id, $meta_value );
|
||||
} catch ( \Throwable $e ) {
|
||||
TMDO_Logger::error( $this->module, 'update_user_metadata', $e->getMessage() );
|
||||
}
|
||||
|
||||
return $check;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes favorites records when a listing post is deleted.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function action_delete_post( int $post_id, \WP_Post $post ): void {
|
||||
if ( 'hp_listing' !== $post->post_type || ! $this->is_active() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
global $wpdb;
|
||||
$wpdb->delete( TMDO_DB::table( 'hpct_favorites' ), array( 'listing_id' => $post_id ), array( '%d' ) );
|
||||
} catch ( \Throwable $e ) {
|
||||
TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs user favorites to the hpct_favorites table.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param mixed $meta_value Favorites meta value (array or serialized).
|
||||
* @return void
|
||||
*/
|
||||
private function sync_favorites( int $user_id, $meta_value ): void {
|
||||
global $wpdb;
|
||||
$table = TMDO_DB::table( 'hpct_favorites' );
|
||||
$now = TMDO_DB::now();
|
||||
|
||||
// v2.13.3: object-injection-safe unserialize (fixes L-DESER-1).
|
||||
// Input is user-controlled wp_usermeta value via update_user_meta hook.
|
||||
$listing_ids = is_array( $meta_value )
|
||||
? $meta_value
|
||||
: (array) TMDO_Safe_Unserialize::run( $meta_value );
|
||||
$listing_ids = array_filter( array_map( 'absint', $listing_ids ) );
|
||||
|
||||
// Delete all existing and re-insert.
|
||||
$wpdb->delete( $table, array( 'user_id' => $user_id ), array( '%d' ) );
|
||||
|
||||
$insert_sql = TMDO_IS_SQLITE ? 'INSERT OR IGNORE' : 'INSERT IGNORE';
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- insert_sql is 'INSERT IGNORE'/'INSERT OR IGNORE'; table name from TMDO_DB::table().
|
||||
foreach ( $listing_ids as $listing_id ) {
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"{$insert_sql} INTO `{$table}` (user_id, listing_id, created_at) VALUES (%d, %d, %s)",
|
||||
$user_id,
|
||||
$listing_id,
|
||||
$now
|
||||
)
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Listing Meta interceptor for hp_listing post type.
|
||||
*
|
||||
* Intercepts get/update_post_metadata for hp_listing posts.
|
||||
* Operates on hpct_listing_meta KV table. Only intercepts meta_keys
|
||||
* prefixed with hp_ or _hp_.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing Meta interceptor — intercepts get/update_post_metadata for hp_listing posts.
|
||||
*
|
||||
* Operates on hpct_listing_meta KV table. Only intercepts meta_keys
|
||||
* prefixed with hp_ or _hp_.
|
||||
*/
|
||||
class TMDO_Listing_Meta_Interceptor extends TMDO_Interceptor_Base {
|
||||
|
||||
/**
|
||||
* Module identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $module = 'listing_meta';
|
||||
|
||||
/**
|
||||
* Registers WordPress hooks for this interceptor.
|
||||
*
|
||||
* Skips registration when the underlying `hpct_listing_meta` KV table is
|
||||
* missing — this prevents cascading DB errors when WPDO is installed
|
||||
* without HPCT first having been imported. Solves audit finding R-2.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks(): void {
|
||||
if ( ! self::table_exists() ) {
|
||||
// HPCT is not installed at all → don't even log (clean doctor output).
|
||||
// We only log when HPCT *is* loaded but its table is missing — that's a
|
||||
// genuine inconsistency worth surfacing to admins.
|
||||
if ( class_exists( 'HPCT_Core' ) ) {
|
||||
self::log_skip_once();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter( 'get_post_metadata', array( $this, 'filter_get_meta' ), 10, 4 );
|
||||
add_filter( 'update_post_metadata', array( $this, 'filter_update_meta' ), 10, 5 );
|
||||
add_filter( 'add_post_metadata', array( $this, 'filter_add_meta' ), 10, 5 );
|
||||
add_filter( 'delete_post_metadata', array( $this, 'filter_delete_meta' ), 10, 5 );
|
||||
add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached check for the existence of the hpct_listing_meta table.
|
||||
*
|
||||
* Result is request-cached to avoid repeated SHOW TABLES calls.
|
||||
*
|
||||
* @return bool True when the table exists.
|
||||
*/
|
||||
private static function table_exists(): bool {
|
||||
static $exists = null;
|
||||
if ( null !== $exists ) {
|
||||
return $exists;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = TMDO_DB::table( 'hpct_listing_meta' );
|
||||
// SHOW TABLES LIKE returns the table name when present, or NULL when absent.
|
||||
$found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
|
||||
$exists = ( null !== $found && '' !== $found );
|
||||
return $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset cached table_exists state — for tests and PR-3 schema changes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function reset_table_exists_cache(): void {
|
||||
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
|
||||
// Use reflection to clear the static. Cleanest approach in PHP 8.1+.
|
||||
( function () {
|
||||
static $exists = null;
|
||||
$exists = null;
|
||||
} )();
|
||||
// The above closure does not actually reset the bound static of table_exists().
|
||||
// Instead we expose a flag via a class-level static.
|
||||
self::$table_exists_cache_invalidated_at = microtime( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker for cache invalidation. Real reset happens by re-calling table_exists()
|
||||
* in a fresh process; tests should isolate via runInSeparateProcess where needed.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
private static float $table_exists_cache_invalidated_at = 0.0;
|
||||
|
||||
/**
|
||||
* Records a single "skipped — table missing" entry in wpdo_errors per request.
|
||||
*
|
||||
* Direct INSERT (not via TMDO_Logger::error) so we avoid cascading the
|
||||
* message to PHP's error_log on every page load. Idempotent within a
|
||||
* single request via static guard.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function log_skip_once(): void {
|
||||
static $logged = false;
|
||||
if ( $logged ) {
|
||||
return;
|
||||
}
|
||||
$logged = true;
|
||||
|
||||
// Direct INSERT — wrapped in try/catch because the wpdo_errors table
|
||||
// might not exist in fresh installs. Skipping the log is acceptable;
|
||||
// breaking register_hooks() is not.
|
||||
try {
|
||||
global $wpdb;
|
||||
$wpdb->insert(
|
||||
$wpdb->prefix . 'wpdo_errors',
|
||||
array(
|
||||
'module' => 'listing_meta',
|
||||
'zone' => '',
|
||||
'hook' => 'register_hooks',
|
||||
'message' => 'Skipped: hpct_listing_meta table missing. Run `wp wpdo import-hpct` or migrate first.',
|
||||
'context' => '{}',
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%s', '%s', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- intentional: wpdo_errors absence is non-critical.
|
||||
// Silently swallow — wpdo_errors table absence is non-critical.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes listing meta when a post is deleted.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function action_delete_post( int $post_id, \WP_Post $post ): void {
|
||||
if ( 'hp_listing' !== $post->post_type || ! $this->is_active() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
global $wpdb;
|
||||
$wpdb->delete( TMDO_DB::table( 'hpct_listing_meta' ), array( 'listing_id' => $post_id ), array( '%d' ) );
|
||||
} catch ( \Throwable $e ) {
|
||||
TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters get_post_metadata for hp_listing posts.
|
||||
*
|
||||
* @param mixed $value Current value or null.
|
||||
* @param int $object_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param bool $single Whether to return single value.
|
||||
* @return mixed Filtered value.
|
||||
*/
|
||||
public function filter_get_meta( $value, int $object_id, string $meta_key, bool $single ) {
|
||||
if ( ! $this->is_enabled() || ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $this->intercept(
|
||||
function () use ( $object_id, $meta_key, $single ) {
|
||||
global $wpdb;
|
||||
$table = TMDO_DB::table( 'hpct_listing_meta' );
|
||||
|
||||
if ( $single ) {
|
||||
$val = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT meta_value FROM `{$table}` WHERE listing_id = %d AND meta_key = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name validated by TMDO_DB::table() + sanitize_key().
|
||||
$object_id,
|
||||
$meta_key
|
||||
)
|
||||
);
|
||||
return null !== $val ? $val : null;
|
||||
}
|
||||
|
||||
return $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT meta_value FROM `{$table}` WHERE listing_id = %d AND meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name validated by TMDO_DB::table() + sanitize_key().
|
||||
$object_id,
|
||||
$meta_key
|
||||
)
|
||||
) ?: null;
|
||||
},
|
||||
fn() => $value,
|
||||
'get_post_metadata'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters update_post_metadata for hp_listing posts.
|
||||
*
|
||||
* @param mixed $check Whether to short-circuit.
|
||||
* @param int $object_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 $object_id, string $meta_key, $meta_value, $prev_value ) {
|
||||
if ( ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) || ! $this->is_active() ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->upsert_meta( $object_id, $meta_key, $meta_value );
|
||||
} catch ( \Throwable $e ) {
|
||||
TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() );
|
||||
}
|
||||
|
||||
return $check;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters add_post_metadata for hp_listing posts.
|
||||
*
|
||||
* @param mixed $check Whether to short-circuit.
|
||||
* @param int $object_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param mixed $meta_value Meta value.
|
||||
* @param bool $unique Whether meta key should be unique.
|
||||
* @return mixed Filtered check value.
|
||||
*/
|
||||
public function filter_add_meta( $check, int $object_id, string $meta_key, $meta_value, bool $unique ) {
|
||||
if ( ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) || ! $this->is_active() ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->upsert_meta( $object_id, $meta_key, $meta_value );
|
||||
} catch ( \Throwable $e ) {
|
||||
TMDO_Logger::error( $this->module, 'add_post_metadata', $e->getMessage() );
|
||||
}
|
||||
|
||||
return $check;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters delete_post_metadata for hp_listing posts.
|
||||
*
|
||||
* @param mixed $check Whether to short-circuit.
|
||||
* @param int $object_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param mixed $meta_value Meta value to match.
|
||||
* @param bool $delete_all Whether to delete all matching.
|
||||
* @return mixed Filtered check value.
|
||||
*/
|
||||
public function filter_delete_meta( $check, int $object_id, string $meta_key, $meta_value, bool $delete_all ) {
|
||||
if ( ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) || ! $this->is_active() ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
try {
|
||||
global $wpdb;
|
||||
$wpdb->delete(
|
||||
TMDO_DB::table( 'hpct_listing_meta' ),
|
||||
array(
|
||||
'listing_id' => $object_id,
|
||||
'meta_key' => $meta_key,
|
||||
),
|
||||
array( '%d', '%s' )
|
||||
);
|
||||
} catch ( \Throwable $e ) {
|
||||
TMDO_Logger::error( $this->module, 'delete_post_metadata', $e->getMessage() );
|
||||
}
|
||||
|
||||
return $check;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the post is an hp_listing.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return bool True if hp_listing post type.
|
||||
*/
|
||||
private function is_hp_listing( int $post_id ): bool {
|
||||
return 'hp_listing' === get_post_type( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the meta key is an hp_ or _hp_ key.
|
||||
*
|
||||
* @param string $meta_key Meta key.
|
||||
* @return bool True if key starts with hp_ or _hp_.
|
||||
*/
|
||||
private function is_hp_key( string $meta_key ): bool {
|
||||
return str_starts_with( $meta_key, 'hp_' ) || str_starts_with( $meta_key, '_hp_' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts or updates a listing meta value in the hpct_listing_meta table.
|
||||
*
|
||||
* Single round-trip via TMDO_DB::upsert() — uses ON DUPLICATE KEY UPDATE
|
||||
* (MySQL) or ON CONFLICT(listing_id, meta_key) DO UPDATE (SQLite).
|
||||
* Solves audit finding P-C1 (the previous SELECT + INSERT/UPDATE pattern
|
||||
* cost 2 SQL round-trips per write).
|
||||
*
|
||||
* Requires UNIQUE KEY (listing_id, meta_key) on hpct_listing_meta —
|
||||
* present by design from HPCT v1.0+.
|
||||
*
|
||||
* @param int $listing_id Listing post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param mixed $meta_value Meta value.
|
||||
* @return void
|
||||
*/
|
||||
private function upsert_meta( int $listing_id, string $meta_key, $meta_value ): void {
|
||||
// maybe_serialize array values to match WordPress's native postmeta semantics.
|
||||
$serialized = is_array( $meta_value ) || is_object( $meta_value )
|
||||
? maybe_serialize( $meta_value )
|
||||
: (string) $meta_value;
|
||||
|
||||
TMDO_DB::upsert(
|
||||
TMDO_DB::table( 'hpct_listing_meta' ),
|
||||
array(
|
||||
'listing_id' => $listing_id,
|
||||
'meta_key' => $meta_key,
|
||||
'meta_value' => $serialized,
|
||||
),
|
||||
array( 'meta_value' ), // Update only meta_value on conflict.
|
||||
array( 'listing_id', 'meta_key' ), // Composite unique key.
|
||||
array( '%d', '%s', '%s' )
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Memberships interceptor for HivePress Membership posts.
|
||||
*
|
||||
* @package TMDO
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memberships interceptor — intercepts HivePress Membership reads/writes.
|
||||
*
|
||||
* Operates on hpct_memberships table.
|
||||
*/
|
||||
class TMDO_Memberships_Interceptor extends TMDO_Standard_Post_Interceptor {
|
||||
|
||||
/**
|
||||
* Module identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $module = 'memberships';
|
||||
|
||||
/**
|
||||
* Meta key → flat column mapping.
|
||||
*/
|
||||
public const FIELD_MAP = array(
|
||||
'hp_plan' => 'plan_id',
|
||||
'hp_user' => 'user_id',
|
||||
'hp_price' => 'price',
|
||||
'hp_order' => 'order_id',
|
||||
'hp_start_date' => 'start_date',
|
||||
'hp_end_date' => 'end_date',
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the WordPress post_type slug handled by this interceptor.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_post_type(): string {
|
||||
return 'hp_membership';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param string $now Formatted datetime string.
|
||||
* @return array{values: array, formats: array}
|
||||
*/
|
||||
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' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Messages interceptor for HivePress Message posts.
|
||||
*
|
||||
* @package TMDO
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Messages interceptor — intercepts HivePress Message reads/writes.
|
||||
*
|
||||
* Operates on hpct_messages table.
|
||||
*/
|
||||
class TMDO_Messages_Interceptor extends TMDO_Standard_Post_Interceptor {
|
||||
|
||||
/**
|
||||
* Module identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $module = 'messages';
|
||||
|
||||
/**
|
||||
* Meta key → flat column mapping.
|
||||
*/
|
||||
public const FIELD_MAP = array(
|
||||
'hp_sender' => 'sender_id',
|
||||
'hp_recipient' => 'recipient_id',
|
||||
'hp_listing' => 'listing_id',
|
||||
'hp_read' => 'is_read',
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the WordPress post_type slug handled by this interceptor.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_post_type(): string {
|
||||
return 'hp_message';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param string $now Formatted datetime string.
|
||||
* @return array{values: array, formats: array}
|
||||
*/
|
||||
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' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Requests interceptor for HivePress Request posts.
|
||||
*
|
||||
* @package TMDO
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests interceptor — intercepts HivePress Request reads/writes.
|
||||
*
|
||||
* Operates on hpct_requests table.
|
||||
*/
|
||||
class TMDO_Requests_Interceptor extends TMDO_Standard_Post_Interceptor {
|
||||
|
||||
/**
|
||||
* Module identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $module = 'requests';
|
||||
|
||||
/**
|
||||
* Meta key → flat column mapping.
|
||||
*/
|
||||
public const FIELD_MAP = array(
|
||||
'hp_vendor' => 'vendor_id',
|
||||
'hp_listing' => 'listing_id',
|
||||
'hp_budget' => 'budget',
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the WordPress post_type slug handled by this interceptor.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_post_type(): string {
|
||||
return 'hp_request';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param string $now Formatted datetime string.
|
||||
* @return array{values: array, formats: array}
|
||||
*/
|
||||
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' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Reviews interceptor for HivePress Review posts.
|
||||
*
|
||||
* @package TMDO
|
||||
*/
|
||||
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reviews interceptor — intercepts HivePress Review reads/writes.
|
||||
*
|
||||
* Operates on hpct_reviews table.
|
||||
*/
|
||||
class TMDO_Reviews_Interceptor extends TMDO_Standard_Post_Interceptor {
|
||||
|
||||
/**
|
||||
* Module identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $module = 'reviews';
|
||||
|
||||
/**
|
||||
* Meta key → flat column mapping.
|
||||
*/
|
||||
public const FIELD_MAP = array(
|
||||
'hp_rating' => 'rating',
|
||||
'hp_listing' => 'listing_id',
|
||||
'_hp_vendor' => 'vendor_id',
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
parent::register_hooks();
|
||||
add_filter( 'get_post_metadata', array( $this, 'filter_get_meta' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the INSERT row for a newly created review post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @param string $now Formatted datetime string.
|
||||
* @return array{values: array, formats: array}
|
||||
*/
|
||||
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' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads hp_rating directly from the reviews flat table.
|
||||
*
|
||||
* @param mixed $value Short-circuit value.
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @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 || 'hp_review' !== get_post_type( $post_id ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $this->intercept(
|
||||
function () use ( $post_id, $single ) {
|
||||
global $wpdb;
|
||||
$rating = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
'SELECT rating FROM ' . TMDO_DB::table( 'hpct_reviews' ) . ' WHERE post_id = %d LIMIT 1',
|
||||
$post_id
|
||||
)
|
||||
);
|
||||
return null !== $rating ? ( $single ? $rating : array( $rating ) ) : null;
|
||||
},
|
||||
fn() => $value,
|
||||
'get_post_metadata'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Statistics interceptor for listing view tracking.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistics interceptor — tracks listing view events in hpct_statistics.
|
||||
*
|
||||
* Special: append-only (INSERT only, no UPDATE). Records events from
|
||||
* both _hp_views meta update and HivePress listing/viewed action.
|
||||
*/
|
||||
class TMDO_Statistics_Interceptor extends TMDO_Interceptor_Base {
|
||||
|
||||
/**
|
||||
* Module identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $module = 'statistics';
|
||||
|
||||
/**
|
||||
* Registers WordPress hooks for this interceptor.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks(): void {
|
||||
add_filter( 'update_post_metadata', array( $this, 'filter_view_meta' ), 10, 5 );
|
||||
add_action( 'hivepress/v1/models/listing/viewed', array( $this, 'action_listing_viewed' ), 10, 1 );
|
||||
add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters update_post_metadata to record a view event when _hp_views is updated.
|
||||
*
|
||||
* @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_view_meta( $check, int $post_id, string $meta_key, $meta_value, $prev_value ) {
|
||||
if ( '_hp_views' !== $meta_key || 'hp_listing' !== get_post_type( $post_id ) ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
if ( ! $this->is_active() ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->record_event( $post_id, 'view' );
|
||||
} catch ( \Throwable $e ) {
|
||||
TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() );
|
||||
}
|
||||
|
||||
return $check;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a view event when a HivePress listing is viewed.
|
||||
*
|
||||
* @param mixed $listing Listing object or ID.
|
||||
* @return void
|
||||
*/
|
||||
public function action_listing_viewed( $listing ): void {
|
||||
if ( ! $this->is_active() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$listing_id = is_object( $listing ) && method_exists( $listing, 'get_id' )
|
||||
? $listing->get_id()
|
||||
: ( is_numeric( $listing ) ? (int) $listing : 0 );
|
||||
|
||||
if ( ! $listing_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->record_event( $listing_id, 'hp_action_view' );
|
||||
} catch ( \Throwable $e ) {
|
||||
TMDO_Logger::error( $this->module, 'listing_viewed', $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes statistics records when a listing post is deleted.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function action_delete_post( int $post_id, \WP_Post $post ): void {
|
||||
if ( 'hp_listing' !== $post->post_type || ! $this->is_active() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
global $wpdb;
|
||||
$wpdb->delete( TMDO_DB::table( 'hpct_statistics' ), array( 'listing_id' => $post_id ), array( '%d' ) );
|
||||
} catch ( \Throwable $e ) {
|
||||
TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a statistics event to the hpct_statistics table.
|
||||
*
|
||||
* @param int $listing_id Listing post ID.
|
||||
* @param string $event_type Event type identifier.
|
||||
* @return void
|
||||
*/
|
||||
private function record_event( int $listing_id, string $event_type ): void {
|
||||
global $wpdb;
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) );
|
||||
$ip_hash = $ip ? hash( 'sha256', $ip ) : '';
|
||||
|
||||
$wpdb->insert(
|
||||
TMDO_DB::table( 'hpct_statistics' ),
|
||||
array(
|
||||
'listing_id' => $listing_id,
|
||||
'event_type' => $event_type,
|
||||
'user_id' => $user_id,
|
||||
'ip_hash' => $ip_hash,
|
||||
'referrer' => substr( esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ), 0, 500 ),
|
||||
'created_at' => TMDO_DB::now(),
|
||||
),
|
||||
array( '%d', '%s', '%d', '%s', '%s', '%s' )
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* WP_Query interceptor for hp_listing meta queries.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* WP_Query interceptor for hp_listing meta queries.
|
||||
*
|
||||
* The hpct_listing_meta is a key-value table (like wp_postmeta), so each
|
||||
* meta_query condition needs its own JOIN alias with a meta_key condition:
|
||||
*
|
||||
* LEFT JOIN wp_hpct_listing_meta AS wpdo_lm_0
|
||||
* ON (wp_posts.ID = wpdo_lm_0.listing_id AND wpdo_lm_0.meta_key = 'hp_price')
|
||||
* WHERE wpdo_lm_0.meta_value >= 100
|
||||
*
|
||||
* All hp_* and _hp_* meta keys are intercepted. Any other meta keys are
|
||||
* left in the meta_query to be handled by wp_postmeta as usual.
|
||||
*/
|
||||
class TMDO_Listing_Meta_Query extends TMDO_Query_Interceptor_Base {
|
||||
|
||||
/**
|
||||
* Returns the module identifier.
|
||||
*
|
||||
* @return string Module name.
|
||||
*/
|
||||
protected function get_module(): string {
|
||||
return 'listing_meta';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the post types this interceptor handles.
|
||||
*
|
||||
* @return string[] Post type slugs.
|
||||
*/
|
||||
protected function get_post_types(): array {
|
||||
return array( 'hp_listing' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom table name.
|
||||
*
|
||||
* @return string Table name without prefix.
|
||||
*/
|
||||
protected function get_table(): string {
|
||||
return 'hpct_listing_meta';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the join column for this KV table.
|
||||
*
|
||||
* @return string Join column name.
|
||||
*/
|
||||
protected function get_join_column(): string {
|
||||
return 'listing_id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Not used — this class overrides pre_get_posts directly.
|
||||
*
|
||||
* @return array Empty array.
|
||||
*/
|
||||
protected function get_meta_key_map(): array {
|
||||
return array();
|
||||
}
|
||||
|
||||
// ── Override pre_get_posts for KV-table pattern ───────────────────────
|
||||
|
||||
/**
|
||||
* Intercepts pre_get_posts to handle KV-table meta_query conditions.
|
||||
*
|
||||
* @param \WP_Query $query The WP_Query object.
|
||||
* @return void
|
||||
*/
|
||||
public function pre_get_posts( \WP_Query $query ): void {
|
||||
if ( ! $this->should_intercept( $query ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$raw_meta_query = (array) $query->get( 'meta_query' );
|
||||
if ( empty( $raw_meta_query ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$our_clauses = array();
|
||||
$remaining = array();
|
||||
|
||||
foreach ( $raw_meta_query as $k => $clause ) {
|
||||
if ( 'relation' === $k || ! is_array( $clause ) || ! isset( $clause['key'] ) ) {
|
||||
$remaining[ $k ] = $clause;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $this->is_hp_key( $clause['key'] ) ) {
|
||||
$our_clauses[] = array(
|
||||
'meta_key' => $clause['key'],
|
||||
'value' => $clause['value'] ?? '',
|
||||
'compare' => strtoupper( trim( $clause['compare'] ?? '=' ) ),
|
||||
'type' => strtoupper( trim( $clause['type'] ?? 'CHAR' ) ),
|
||||
);
|
||||
} else {
|
||||
$remaining[ $k ] = $clause;
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $our_clauses ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $raw_meta_query['relation'] ) && ! isset( $remaining['relation'] ) ) {
|
||||
$remaining['relation'] = $raw_meta_query['relation'];
|
||||
}
|
||||
|
||||
$query->set( 'meta_query', $remaining );
|
||||
$query->set( $this->query_var(), $our_clauses );
|
||||
}
|
||||
|
||||
// ── Override posts_join for KV-table: one JOIN alias per condition ─────
|
||||
|
||||
/**
|
||||
* Appends LEFT JOINs for each KV-table meta_query condition.
|
||||
*
|
||||
* @param string $join Current JOIN SQL.
|
||||
* @param \WP_Query $query The WP_Query object.
|
||||
* @return string Modified JOIN SQL.
|
||||
*/
|
||||
public function posts_join( ?string $join, \WP_Query $query ): string {
|
||||
$join = (string) ( $join ?? '' );
|
||||
$clauses = $this->get_clauses( $query );
|
||||
if ( empty( $clauses ) ) {
|
||||
return $join;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = TMDO_DB::table( $this->get_table() );
|
||||
$join_col = esc_sql( $this->get_join_column() );
|
||||
|
||||
foreach ( $clauses as $i => $clause ) {
|
||||
$alias = $this->alias_for( $i );
|
||||
$meta_key = $clause['meta_key'];
|
||||
|
||||
if ( false === strpos( $join, "`{$alias}`" ) ) {
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $table from TMDO_DB::table(); $alias and $join_col are sanitize_key()-validated.
|
||||
$join .= $wpdb->prepare(
|
||||
" LEFT JOIN `{$table}` AS `{$alias}`"
|
||||
. " ON (`{$wpdb->posts}`.`ID` = `{$alias}`.`{$join_col}`"
|
||||
. " AND `{$alias}`.`meta_key` = %s)",
|
||||
$meta_key
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
}
|
||||
|
||||
return $join;
|
||||
}
|
||||
|
||||
// ── Override posts_where for KV-table ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* Appends WHERE conditions for KV-table meta_value comparisons.
|
||||
*
|
||||
* @param string $where Current WHERE SQL.
|
||||
* @param \WP_Query $query The WP_Query object.
|
||||
* @return string Modified WHERE SQL.
|
||||
*/
|
||||
public function posts_where( ?string $where, \WP_Query $query ): string {
|
||||
$where = (string) ( $where ?? '' );
|
||||
$clauses = $this->get_clauses( $query );
|
||||
if ( empty( $clauses ) ) {
|
||||
return $where;
|
||||
}
|
||||
|
||||
foreach ( $clauses as $i => $clause ) {
|
||||
$alias = $this->alias_for( $i );
|
||||
$col = "`{$alias}`.`meta_value`";
|
||||
$compare = $this->sanitize_compare( $clause['compare'] );
|
||||
$type = $clause['type'];
|
||||
$value = $clause['value'];
|
||||
|
||||
$where .= $this->build_condition( $col, $compare, $type, $value );
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
// ── Override posts_groupby ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Ensures GROUP BY is set when KV-table clauses are active.
|
||||
*
|
||||
* @param string $groupby Current GROUP BY SQL.
|
||||
* @param \WP_Query $query The WP_Query object.
|
||||
* @return string Modified GROUP BY SQL.
|
||||
*/
|
||||
public function posts_groupby( ?string $groupby, \WP_Query $query ): string {
|
||||
$groupby = (string) ( $groupby ?? '' );
|
||||
$clauses = $this->get_clauses( $query );
|
||||
if ( empty( $clauses ) ) {
|
||||
return $groupby;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
if ( '' === trim( $groupby ) ) {
|
||||
$groupby = "`{$wpdb->posts}`.`ID`";
|
||||
}
|
||||
|
||||
return $groupby;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generates a unique SQL alias for a KV-table JOIN at a given index.
|
||||
*
|
||||
* @param int $index Zero-based clause index.
|
||||
* @return string SQL alias string.
|
||||
*/
|
||||
private function alias_for( int $index ): string {
|
||||
return "wpdo_lm_{$index}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a meta key belongs to HivePress (hp_ or _hp_ prefix).
|
||||
*
|
||||
* @param string $meta_key Meta key to check.
|
||||
* @return bool True if the key is an HP key.
|
||||
*/
|
||||
private function is_hp_key( string $meta_key ): bool {
|
||||
return str_starts_with( $meta_key, 'hp_' ) || str_starts_with( $meta_key, '_hp_' );
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* WP_Query interceptor for hp_membership post type.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* WP_Query interceptor for hp_membership post type.
|
||||
*
|
||||
* Intercepted meta keys:
|
||||
* hp_plan => plan_id (BIGINT)
|
||||
* hp_user => user_id (BIGINT)
|
||||
* hp_price => price (VARCHAR)
|
||||
* hp_order => order_id (BIGINT)
|
||||
* hp_start_date => start_date (DATETIME)
|
||||
* hp_end_date => end_date (DATETIME)
|
||||
*/
|
||||
class TMDO_Memberships_Query extends TMDO_Query_Interceptor_Base {
|
||||
|
||||
/**
|
||||
* Returns the module identifier.
|
||||
*
|
||||
* @return string Module name.
|
||||
*/
|
||||
protected function get_module(): string {
|
||||
return 'memberships';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the post types this interceptor handles.
|
||||
*
|
||||
* @return string[] Post type slugs.
|
||||
*/
|
||||
protected function get_post_types(): array {
|
||||
return array( 'hp_membership' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom table name.
|
||||
*
|
||||
* @return string Table name without prefix.
|
||||
*/
|
||||
protected function get_table(): string {
|
||||
return 'hpct_memberships';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the meta_key to column mapping for hpct_memberships.
|
||||
*
|
||||
* @return array<string, string> Meta key to column name map.
|
||||
*/
|
||||
protected function get_meta_key_map(): array {
|
||||
return array(
|
||||
'hp_plan' => 'plan_id',
|
||||
'hp_user' => 'user_id',
|
||||
'hp_price' => 'price',
|
||||
'hp_order' => 'order_id',
|
||||
'hp_start_date' => 'start_date',
|
||||
'hp_end_date' => 'end_date',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* WP_Query interceptor for hp_message post type.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* WP_Query interceptor for hp_message post type.
|
||||
*
|
||||
* Intercepted meta keys:
|
||||
* hp_sender => sender_id (BIGINT)
|
||||
* hp_recipient => recipient_id (BIGINT)
|
||||
* hp_listing => listing_id (BIGINT)
|
||||
* hp_read => is_read (TINYINT)
|
||||
*/
|
||||
class TMDO_Messages_Query extends TMDO_Query_Interceptor_Base {
|
||||
|
||||
/**
|
||||
* Returns the module identifier.
|
||||
*
|
||||
* @return string Module name.
|
||||
*/
|
||||
protected function get_module(): string {
|
||||
return 'messages';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the post types this interceptor handles.
|
||||
*
|
||||
* @return string[] Post type slugs.
|
||||
*/
|
||||
protected function get_post_types(): array {
|
||||
return array( 'hp_message' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom table name.
|
||||
*
|
||||
* @return string Table name without prefix.
|
||||
*/
|
||||
protected function get_table(): string {
|
||||
return 'hpct_messages';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the meta_key to column mapping for hpct_messages.
|
||||
*
|
||||
* @return array<string, string> Meta key to column name map.
|
||||
*/
|
||||
protected function get_meta_key_map(): array {
|
||||
return array(
|
||||
'hp_sender' => 'sender_id',
|
||||
'hp_recipient' => 'recipient_id',
|
||||
'hp_listing' => 'listing_id',
|
||||
'hp_read' => 'is_read',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* WP_Query interceptor for hp_request post type.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* WP_Query interceptor for hp_request post type.
|
||||
*
|
||||
* Intercepted meta keys:
|
||||
* hp_vendor => vendor_id (BIGINT)
|
||||
* hp_listing => listing_id (BIGINT)
|
||||
* hp_budget => budget (VARCHAR)
|
||||
*/
|
||||
class TMDO_Requests_Query extends TMDO_Query_Interceptor_Base {
|
||||
|
||||
/**
|
||||
* Returns the module identifier.
|
||||
*
|
||||
* @return string Module name.
|
||||
*/
|
||||
protected function get_module(): string {
|
||||
return 'requests';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the post types this interceptor handles.
|
||||
*
|
||||
* @return string[] Post type slugs.
|
||||
*/
|
||||
protected function get_post_types(): array {
|
||||
return array( 'hp_request' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom table name.
|
||||
*
|
||||
* @return string Table name without prefix.
|
||||
*/
|
||||
protected function get_table(): string {
|
||||
return 'hpct_requests';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the meta_key to column mapping for hpct_requests.
|
||||
*
|
||||
* @return array<string, string> Meta key to column name map.
|
||||
*/
|
||||
protected function get_meta_key_map(): array {
|
||||
return array(
|
||||
'hp_vendor' => 'vendor_id',
|
||||
'hp_listing' => 'listing_id',
|
||||
'hp_budget' => 'budget',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* WP_Query interceptor for hp_review post type.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* WP_Query interceptor for hp_review post type.
|
||||
*
|
||||
* Rewrites meta_query conditions for known review meta keys to query
|
||||
* hpct_reviews directly, eliminating wp_postmeta JOIN for review queries.
|
||||
*
|
||||
* Intercepted meta keys:
|
||||
* hp_listing => listing_id (BIGINT)
|
||||
* _hp_vendor => vendor_id (BIGINT)
|
||||
* hp_rating => rating (TINYINT)
|
||||
*/
|
||||
class TMDO_Reviews_Query extends TMDO_Query_Interceptor_Base {
|
||||
|
||||
/**
|
||||
* Returns the module identifier.
|
||||
*
|
||||
* @return string Module name.
|
||||
*/
|
||||
protected function get_module(): string {
|
||||
return 'reviews';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the post types this interceptor handles.
|
||||
*
|
||||
* @return string[] Post type slugs.
|
||||
*/
|
||||
protected function get_post_types(): array {
|
||||
return array( 'hp_review' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom table name.
|
||||
*
|
||||
* @return string Table name without prefix.
|
||||
*/
|
||||
protected function get_table(): string {
|
||||
return 'hpct_reviews';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the meta_key to column mapping for hpct_reviews.
|
||||
*
|
||||
* @return array<string, string> Meta key to column name map.
|
||||
*/
|
||||
protected function get_meta_key_map(): array {
|
||||
return array(
|
||||
'hp_listing' => 'listing_id',
|
||||
'_hp_vendor' => 'vendor_id',
|
||||
'hp_rating' => 'rating',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Integration tests for WPDO_Listing_Meta_Interceptor — PR-3 R-2 + P-C1 fixes.
|
||||
*
|
||||
* Verifies:
|
||||
* 1. R-2: Interceptor SKIPS hook registration when hpct_listing_meta is missing
|
||||
* (prevents the "wp_hpct_listing_meta doesn't exist" cascading errors that
|
||||
* forced 2meet-infocards to bypass the interceptor in production).
|
||||
* 2. P-C1: upsert_meta() uses 1 SQL round-trip via WPDO_DB::upsert().
|
||||
*
|
||||
* @covers WPDO_Listing_Meta_Interceptor
|
||||
*/
|
||||
class ListingMetaInterceptorTest extends TestCase {
|
||||
|
||||
/**
|
||||
* R-2: When hpct_listing_meta table is missing, register_hooks() must skip
|
||||
* gracefully and write a single explanatory error log entry.
|
||||
*
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function test_register_hooks_skips_when_table_missing(): void {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}hpct_listing_meta`" );
|
||||
|
||||
// Simulate HPCT_Core being present — register_hooks() only logs the skip
|
||||
// when HPCT is loaded but its table is missing (genuine inconsistency).
|
||||
if ( ! class_exists( 'HPCT_Core' ) ) {
|
||||
// phpcs:ignore Generic.Commenting.InlineComment.InvalidEndChar -- runtime stub
|
||||
eval( 'class HPCT_Core {}' ); // @phpcs:ignore
|
||||
}
|
||||
|
||||
// Ensure wpdo_errors exists for the skip log; harmless if pre-created.
|
||||
$wpdb->query(
|
||||
"CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}wpdo_errors` (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
module varchar(50) NOT NULL DEFAULT '',
|
||||
zone varchar(10) NOT NULL DEFAULT '',
|
||||
hook varchar(255) NOT NULL DEFAULT '',
|
||||
message longtext NOT NULL,
|
||||
context longtext,
|
||||
created_at datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_module (module)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
|
||||
// Snapshot pre-call count of skip messages.
|
||||
$pre = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_errors` WHERE module = %s AND hook = %s",
|
||||
'listing_meta',
|
||||
'register_hooks'
|
||||
)
|
||||
);
|
||||
|
||||
$interceptor = new WPDO_Listing_Meta_Interceptor();
|
||||
$interceptor->register_hooks();
|
||||
|
||||
$post = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_errors` WHERE module = %s AND hook = %s",
|
||||
'listing_meta',
|
||||
'register_hooks'
|
||||
)
|
||||
);
|
||||
$this->assertSame( $pre + 1, $post, 'Expected exactly one new skip message after register_hooks()' );
|
||||
}
|
||||
|
||||
/**
|
||||
* P-C1: WPDO_DB::upsert() must produce an INSERT ... ON DUPLICATE KEY UPDATE
|
||||
* statement (single round-trip), and writes must succeed via the composite
|
||||
* unique key (listing_id, meta_key).
|
||||
*
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function test_upsert_uses_composite_unique_key(): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'hpct_listing_meta';
|
||||
|
||||
// Clean slate.
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||
$wpdb->query(
|
||||
"CREATE TABLE `{$table}` (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
listing_id bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
meta_key varchar(255) NOT NULL DEFAULT '',
|
||||
meta_value longtext,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ui_listing_meta (listing_id, meta_key(191))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
|
||||
// First upsert: INSERT.
|
||||
$result1 = WPDO_DB::upsert(
|
||||
$table,
|
||||
array(
|
||||
'listing_id' => 4242,
|
||||
'meta_key' => 'hp_price',
|
||||
'meta_value' => '100.00',
|
||||
),
|
||||
array( 'meta_value' ),
|
||||
array( 'listing_id', 'meta_key' ),
|
||||
array( '%d', '%s', '%s' )
|
||||
);
|
||||
$this->assertNotFalse( $result1 );
|
||||
|
||||
// Second upsert: UPDATE via the composite key.
|
||||
$result2 = WPDO_DB::upsert(
|
||||
$table,
|
||||
array(
|
||||
'listing_id' => 4242,
|
||||
'meta_key' => 'hp_price',
|
||||
'meta_value' => '250.50',
|
||||
),
|
||||
array( 'meta_value' ),
|
||||
array( 'listing_id', 'meta_key' ),
|
||||
array( '%d', '%s', '%s' )
|
||||
);
|
||||
$this->assertNotFalse( $result2 );
|
||||
|
||||
// Verify only one row exists for (4242, 'hp_price') with the latest value.
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT meta_value FROM `{$table}` WHERE listing_id = %d AND meta_key = %s",
|
||||
4242,
|
||||
'hp_price'
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
$this->assertCount( 1, $rows, 'Composite UNIQUE must collapse two upserts into a single row' );
|
||||
$this->assertSame( '250.50', $rows[0]['meta_value'] );
|
||||
|
||||
// Cleanup.
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify table_exists() returns false when table missing, true after CREATE.
|
||||
*
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function test_table_exists_detection(): void {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}hpct_listing_meta`" );
|
||||
|
||||
// Use reflection to invoke private static.
|
||||
$ref = new ReflectionClass( WPDO_Listing_Meta_Interceptor::class );
|
||||
$method = $ref->getMethod( 'table_exists' );
|
||||
$method->setAccessible( true );
|
||||
|
||||
$this->assertFalse( $method->invoke( null ), 'table_exists must return false when table missing' );
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,6 @@ define( 'TMDO_HIVEPRESS_FILE', TMDO_HIVEPRESS_PATH . '2meet-data-optimizer-hivep
|
||||
define( 'TMDO_HIVEPRESS_VERSION', '0.1.0' );
|
||||
|
||||
// ── AddOn classes ──────────────────────────────────────────────────────────
|
||||
// Superset of the unit bootstrap's list: the integration suite also exercises
|
||||
// the interceptors, which the unit suite does not load.
|
||||
|
||||
$tmdo_hp_files = array(
|
||||
'includes/class-tmdo-hivepress.php',
|
||||
@@ -54,12 +52,6 @@ $tmdo_hp_files = array(
|
||||
foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/hivepress/adapters/*.php' ) ?: array() as $tmdo_hp_glob ) {
|
||||
$tmdo_hp_files[] = 'includes/hivepress/adapters/' . basename( $tmdo_hp_glob );
|
||||
}
|
||||
foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/interceptors/*.php' ) ?: array() as $tmdo_hp_glob ) {
|
||||
$tmdo_hp_files[] = 'includes/interceptors/' . basename( $tmdo_hp_glob );
|
||||
}
|
||||
foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/query/*.php' ) ?: array() as $tmdo_hp_glob ) {
|
||||
$tmdo_hp_files[] = 'includes/query/' . basename( $tmdo_hp_glob );
|
||||
}
|
||||
|
||||
foreach ( $tmdo_hp_files as $tmdo_hp_file ) {
|
||||
$tmdo_hp_path = TMDO_HIVEPRESS_PATH . $tmdo_hp_file;
|
||||
|
||||
Reference in New Issue
Block a user