Merge pull request 'hardening 2.0: 2meet-data-optimizer-hivepress-addon (staged rollout, dev22 first)' (#1) from hardening/2.0 into main
This commit was merged in pull request #1.
This commit is contained in:
@@ -138,7 +138,7 @@ wp wpdo hivepress benchmark</pre>
|
|||||||
. esc_html__( '偵測到衝突外掛:', 'tmdo-hivepress' ) . '</strong> '
|
. esc_html__( '偵測到衝突外掛:', 'tmdo-hivepress' ) . '</strong> '
|
||||||
. esc_html( implode( ', ', $conflicts ) )
|
. esc_html( implode( ', ', $conflicts ) )
|
||||||
. '</p><p>'
|
. '</p><p>'
|
||||||
. esc_html__( 'WPDO HivePress integration 已自動退讓。請執行 `wp wpdo hivepress migrate-from-hpct` 後停用舊外掛。', 'tmdo-hivepress' )
|
. esc_html__( 'WPDO HivePress integration 已自動退讓。停用舊外掛後即由 WPDO 接手(舊外掛自有資料表的資料不會匯入)。', 'tmdo-hivepress' )
|
||||||
. '</p></div>';
|
. '</p></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ add_action(
|
|||||||
$aliases = array(
|
$aliases = array(
|
||||||
'TMDO_Admin_HivePress' => 'WPDO_Admin_HivePress',
|
'TMDO_Admin_HivePress' => 'WPDO_Admin_HivePress',
|
||||||
'TMDO_CLI_HivePress' => 'WPDO_CLI_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_HP_Bootstrap' => 'WPDO_HP_Bootstrap',
|
||||||
'TMDO_HivePress' => 'WPDO_HivePress',
|
'TMDO_HivePress' => 'WPDO_HivePress',
|
||||||
'TMDO_HivePress_Attribute_Bridge' => 'WPDO_HivePress_Attribute_Bridge',
|
'TMDO_HivePress_Attribute_Bridge' => 'WPDO_HivePress_Attribute_Bridge',
|
||||||
@@ -29,9 +27,9 @@ add_action(
|
|||||||
'TMDO_HivePress_Blocks_Adapter' => 'WPDO_HivePress_Blocks_Adapter',
|
'TMDO_HivePress_Blocks_Adapter' => 'WPDO_HivePress_Blocks_Adapter',
|
||||||
'TMDO_HivePress_Bookings_Adapter' => 'WPDO_HivePress_Bookings_Adapter',
|
'TMDO_HivePress_Bookings_Adapter' => 'WPDO_HivePress_Bookings_Adapter',
|
||||||
'TMDO_HivePress_Bootstrap' => 'WPDO_HivePress_Bootstrap',
|
'TMDO_HivePress_Bootstrap' => 'WPDO_HivePress_Bootstrap',
|
||||||
'TMDO_HivePress_Comment_Router' => 'WPDO_HivePress_Comment_Router',
|
|
||||||
'TMDO_HivePress_Conflict_Guard' => 'WPDO_HivePress_Conflict_Guard',
|
'TMDO_HivePress_Conflict_Guard' => 'WPDO_HivePress_Conflict_Guard',
|
||||||
'TMDO_HivePress_Core_Adapter' => 'WPDO_HivePress_Core_Adapter',
|
'TMDO_HivePress_Core_Adapter' => 'WPDO_HivePress_Core_Adapter',
|
||||||
|
'TMDO_HivePress_Entity_Groups' => 'WPDO_HivePress_Entity_Groups',
|
||||||
'TMDO_HivePress_Cron_Optimizer' => 'WPDO_HivePress_Cron_Optimizer',
|
'TMDO_HivePress_Cron_Optimizer' => 'WPDO_HivePress_Cron_Optimizer',
|
||||||
'TMDO_HivePress_Detector' => 'WPDO_HivePress_Detector',
|
'TMDO_HivePress_Detector' => 'WPDO_HivePress_Detector',
|
||||||
'TMDO_HivePress_Favorites_Adapter' => 'WPDO_HivePress_Favorites_Adapter',
|
'TMDO_HivePress_Favorites_Adapter' => 'WPDO_HivePress_Favorites_Adapter',
|
||||||
@@ -48,18 +46,7 @@ add_action(
|
|||||||
'TMDO_HivePress_Tags_Adapter' => 'WPDO_HivePress_Tags_Adapter',
|
'TMDO_HivePress_Tags_Adapter' => 'WPDO_HivePress_Tags_Adapter',
|
||||||
'TMDO_Hivepress_Term_Comment_Fields' => 'WPDO_Hivepress_Term_Comment_Fields',
|
'TMDO_Hivepress_Term_Comment_Fields' => 'WPDO_Hivepress_Term_Comment_Fields',
|
||||||
'TMDO_Hivepress_Transient_Filter' => 'WPDO_Hivepress_Transient_Filter',
|
'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_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 ) {
|
foreach ( $aliases as $tmdo_class => $wpdo_alias ) {
|
||||||
if ( class_exists( $tmdo_class, false ) && ! class_exists( $wpdo_alias, false ) ) {
|
if ( class_exists( $tmdo_class, false ) && ! class_exists( $wpdo_alias, false ) ) {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Bootstrap for 2meet Data Optimizer HivePress AddOn.
|
* Bootstrap for 2meet Data Optimizer HivePress AddOn.
|
||||||
*
|
*
|
||||||
* 載入 13 adapter + 7 interceptor + 5 query interceptor + 補助組件 +
|
* 載入 13 adapter + 補助組件 + admin tab + CLI namespace。
|
||||||
* admin tab + CLI namespace。
|
|
||||||
*
|
*
|
||||||
* 注意:HP 的 V3.0.0 family bootstrap 內部也有一個 class TMDO_HivePress_Bootstrap
|
* 注意:HP 的 V3.0.0 family bootstrap 內部也有一個 class TMDO_HivePress_Bootstrap
|
||||||
* (在 includes/hivepress/class-tmdo-hivepress-bootstrap.php)。為避免衝突,本
|
* (在 includes/hivepress/class-tmdo-hivepress-bootstrap.php)。為避免衝突,本
|
||||||
@@ -44,7 +43,6 @@ final class TMDO_HP_Bootstrap {
|
|||||||
require_once $base . 'includes/hivepress/class-tmdo-hivepress-detector.php';
|
require_once $base . 'includes/hivepress/class-tmdo-hivepress-detector.php';
|
||||||
require_once $base . 'includes/hivepress/class-tmdo-hivepress-conflict-guard.php';
|
require_once $base . 'includes/hivepress/class-tmdo-hivepress-conflict-guard.php';
|
||||||
require_once $base . 'includes/hivepress/class-tmdo-hivepress-bootstrap.php';
|
require_once $base . 'includes/hivepress/class-tmdo-hivepress-bootstrap.php';
|
||||||
require_once $base . 'includes/hivepress/class-tmdo-hivepress-comment-router.php';
|
|
||||||
require_once $base . 'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php';
|
require_once $base . 'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php';
|
||||||
|
|
||||||
// 13 adapters
|
// 13 adapters
|
||||||
@@ -65,40 +63,11 @@ final class TMDO_HP_Bootstrap {
|
|||||||
require_once $base . 'includes/hivepress/class-tmdo-hivepress-benchmark.php';
|
require_once $base . 'includes/hivepress/class-tmdo-hivepress-benchmark.php';
|
||||||
require_once $base . 'includes/hivepress/class-tmdo-hivepress-rest.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
|
// Misc HP integration files
|
||||||
require_once $base . 'includes/class-tmdo-hivepress.php';
|
require_once $base . 'includes/class-tmdo-hivepress.php';
|
||||||
require_once $base . 'includes/class-tmdo-hivepress-transient-filter.php';
|
require_once $base . 'includes/class-tmdo-hivepress-transient-filter.php';
|
||||||
require_once $base . 'includes/class-tmdo-hpct-import.php';
|
|
||||||
require_once $base . 'includes/class-tmdo-hivepress-term-comment-fields.php';
|
require_once $base . 'includes/class-tmdo-hivepress-term-comment-fields.php';
|
||||||
|
require_once $base . 'includes/class-tmdo-hivepress-entity-groups.php';
|
||||||
require_once $base . 'includes/class-tmdo-listing-stats.php';
|
require_once $base . 'includes/class-tmdo-listing-stats.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,34 +80,6 @@ final class TMDO_HP_Bootstrap {
|
|||||||
TMDO_HivePress_Bootstrap::boot();
|
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)
|
// HP transient filter (init:5)
|
||||||
if ( class_exists( 'TMDO_Hivepress_Transient_Filter' ) ) {
|
if ( class_exists( 'TMDO_Hivepress_Transient_Filter' ) ) {
|
||||||
add_action( 'init', array( 'TMDO_Hivepress_Transient_Filter', 'init' ), 5 );
|
add_action( 'init', array( 'TMDO_Hivepress_Transient_Filter', 'init' ), 5 );
|
||||||
@@ -156,6 +97,9 @@ final class TMDO_HP_Bootstrap {
|
|||||||
TMDO_Hivepress_Term_Comment_Fields::register();
|
TMDO_Hivepress_Term_Comment_Fields::register();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post + user entity groups (hp_listing_core / hp_request_core / hp_vendor_core / hp_user).
|
||||||
|
TMDO_HivePress_Entity_Groups::register();
|
||||||
|
|
||||||
// Admin tab
|
// Admin tab
|
||||||
if ( is_admin() ) {
|
if ( is_admin() ) {
|
||||||
$admin_file = TMDO_HIVEPRESS_PATH . 'admin/class-tmdo-admin-hivepress.php';
|
$admin_file = TMDO_HIVEPRESS_PATH . 'admin/class-tmdo-admin-hivepress.php';
|
||||||
|
|||||||
@@ -0,0 +1,377 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* HivePress entity groups owned by the HivePress AddOn.
|
||||||
|
*
|
||||||
|
* Groups → flat tables:
|
||||||
|
* post:hp_listing_core → wp_wpdo_post_hp_listing_core (hp_listing)
|
||||||
|
* post:hp_request_core → wp_wpdo_post_hp_request_core (hp_request)
|
||||||
|
* post:hp_vendor_core → wp_wpdo_post_hp_vendor_core (hp_vendor)
|
||||||
|
* user:hp_user → wp_wpdo_user_hp_user
|
||||||
|
*
|
||||||
|
* Fields only the Hot zone used to cover (ADR-003), as their own groups:
|
||||||
|
* post:hp_draft_flags (hp_listing, hp_request)
|
||||||
|
* post:hp_booking_core (hp_booking)
|
||||||
|
* post:hp_membership_plan_core (hp_membership_plan)
|
||||||
|
* post:hp_membership_core (hp_membership)
|
||||||
|
* They register FIRST so the legacy last-registered owner of hp_expired_time
|
||||||
|
* stays hp_request_core.
|
||||||
|
*
|
||||||
|
* The term/comment groups (hp_taxonomy, hp_review) live in
|
||||||
|
* TMDO_Hivepress_Term_Comment_Fields.
|
||||||
|
*
|
||||||
|
* ⚠️ These definitions must stay byte-identical to the ones live sites
|
||||||
|
* persisted in wpdo_registry_meta (originally registered by core's
|
||||||
|
* TMDO_Post_Fields / TMDO_Member_Fields). Any drift changes the schema hash
|
||||||
|
* and triggers ALTER TABLE on flat tables that may hold aeav_only data.
|
||||||
|
* The three post groups share keys (hp_status, hp_user, …): each declares
|
||||||
|
* its post type so a post's key resolves to its own type's group, while
|
||||||
|
* lookups without a post type keep last-registered-wins — keep their order.
|
||||||
|
*
|
||||||
|
* @package TMDO_HIVEPRESS
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the HivePress post + user entity groups.
|
||||||
|
*/
|
||||||
|
final class TMDO_HivePress_Entity_Groups {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook into WPDO entity field registration and the REST default post type.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register(): void {
|
||||||
|
add_action( 'wpdo_register_entity_fields', array( __CLASS__, 'register_entity_fields' ) );
|
||||||
|
add_filter( 'tmdo_rest_default_post_type', array( __CLASS__, 'rest_default_post_type' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /wpdo/v1/listings serves HivePress listings when no post type is given.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function rest_default_post_type(): string {
|
||||||
|
return 'hp_listing';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the groups (order matters, see file header).
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register_entity_fields(): void {
|
||||||
|
if ( ! class_exists( 'TMDO_Entity_Registry' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::register_hp_draft_flags_group();
|
||||||
|
self::register_hp_booking_core_group();
|
||||||
|
self::register_hp_membership_plan_core_group();
|
||||||
|
self::register_hp_membership_core_group();
|
||||||
|
self::register_hp_listing_core_group();
|
||||||
|
self::register_hp_request_core_group();
|
||||||
|
self::register_hp_vendor_core_group();
|
||||||
|
self::register_hp_user_group();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Group definitions ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draft flag shared by listings and requests (user-dashboard filtering).
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function register_hp_draft_flags_group(): void {
|
||||||
|
TMDO_Entity_Registry::register_group(
|
||||||
|
'post',
|
||||||
|
'hp_draft_flags',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'key' => 'hp_drafted',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Drafted flag (0/1)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array( 'post_types' => array( 'hp_listing', 'hp_request' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HivePress Bookings (post_type=hp_booking): slot-availability lookups.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function register_hp_booking_core_group(): void {
|
||||||
|
TMDO_Entity_Registry::register_group(
|
||||||
|
'post',
|
||||||
|
'hp_booking_core',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'key' => 'hp_start_time',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Booking start unix ts',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_end_time',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Booking end unix ts',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_quantity',
|
||||||
|
'type' => 'integer',
|
||||||
|
'label' => 'Places booked',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array( 'post_types' => array( 'hp_booking' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HivePress Memberships plan (post_type=hp_membership_plan).
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function register_hp_membership_plan_core_group(): void {
|
||||||
|
TMDO_Entity_Registry::register_group(
|
||||||
|
'post',
|
||||||
|
'hp_membership_plan_core',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'key' => 'hp_expire_period',
|
||||||
|
'type' => 'integer',
|
||||||
|
'label' => 'Plan expiry period (days)',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_primary',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Primary plan flag (0/1)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array( 'post_types' => array( 'hp_membership_plan' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HivePress Memberships subscription (post_type=hp_membership).
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function register_hp_membership_core_group(): void {
|
||||||
|
TMDO_Entity_Registry::register_group(
|
||||||
|
'post',
|
||||||
|
'hp_membership_core',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'key' => 'hp_expired_time',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Expiry unix ts',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array( 'post_types' => array( 'hp_membership' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HivePress listing core meta keys (post_type=hp_listing).
|
||||||
|
* Aligns with the existing wpdo_hot_hp_listing flat table; v2.9.5 will
|
||||||
|
* copy-then-cutover the 230 dev10 rows to this group's table.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function register_hp_listing_core_group(): void {
|
||||||
|
TMDO_Entity_Registry::register_group(
|
||||||
|
'post',
|
||||||
|
'hp_listing_core',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'key' => 'hp_price',
|
||||||
|
'type' => 'decimal',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Listing price',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_status',
|
||||||
|
'type' => 'enum',
|
||||||
|
'searchable' => true,
|
||||||
|
'options' => array( 'publish', 'draft', 'pending', 'expired', 'private' ),
|
||||||
|
'label' => 'Listing status',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_featured',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Featured flag (0/1)',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_verified',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Verified flag (0/1)',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_vendor',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Vendor user ID',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_expired_time',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Expiry unix ts',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_featured_time',
|
||||||
|
'type' => 'integer',
|
||||||
|
'label' => 'Featured-until unix ts',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_view_count',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'View counter',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_rating',
|
||||||
|
'type' => 'decimal',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Average rating',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_rating_count',
|
||||||
|
'type' => 'integer',
|
||||||
|
'label' => 'Rating count',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_hourly_rate',
|
||||||
|
'type' => 'decimal',
|
||||||
|
'label' => 'Hourly rate',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array( 'post_types' => array( 'hp_listing' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* HivePress request core meta keys (post_type=hp_request).
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function register_hp_request_core_group(): void {
|
||||||
|
TMDO_Entity_Registry::register_group(
|
||||||
|
'post',
|
||||||
|
'hp_request_core',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'key' => 'hp_status',
|
||||||
|
'type' => 'enum',
|
||||||
|
'searchable' => true,
|
||||||
|
'options' => array( 'publish', 'draft', 'pending', 'expired' ),
|
||||||
|
'label' => 'Request status',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_user',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Request author user ID',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_expired_time',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Expiry unix ts',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_budget',
|
||||||
|
'type' => 'decimal',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Budget amount',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_view_count',
|
||||||
|
'type' => 'integer',
|
||||||
|
'label' => 'View counter',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array( 'post_types' => array( 'hp_request' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* HivePress vendor core meta keys (post_type=hp_vendor).
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function register_hp_vendor_core_group(): void {
|
||||||
|
TMDO_Entity_Registry::register_group(
|
||||||
|
'post',
|
||||||
|
'hp_vendor_core',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'key' => 'hp_user',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Vendor user ID (linked WP user)',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_verified',
|
||||||
|
'type' => 'integer',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Verified flag (0/1)',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_hourly_rate',
|
||||||
|
'type' => 'decimal',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Hourly rate',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_rating_count',
|
||||||
|
'type' => 'integer',
|
||||||
|
'label' => 'Rating count',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_rating',
|
||||||
|
'type' => 'decimal',
|
||||||
|
'searchable' => true,
|
||||||
|
'label' => 'Average rating',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array( 'post_types' => array( 'hp_vendor' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* HivePress per-user fields (favorites + avatar attachment).
|
||||||
|
*
|
||||||
|
* Hp_favorited_listings is a serialized array of post IDs in legacy storage;
|
||||||
|
* the migration engine safe_unserialize()s it (allowed_classes=false to block
|
||||||
|
* PHP-object injection) then the json type encodes back to a JSON array column.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function register_hp_user_group(): void {
|
||||||
|
TMDO_Entity_Registry::register_group(
|
||||||
|
'user',
|
||||||
|
'hp_user',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'key' => 'hp_favorited_listings',
|
||||||
|
'type' => 'json',
|
||||||
|
'label' => 'HivePress favorited listing IDs (array)',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'hp_image',
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => 'HivePress avatar attachment ID',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* HPCT Import integration for WP Data Optimizer.
|
|
||||||
*
|
|
||||||
* @package WP_Data_Optimizer
|
|
||||||
*/
|
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HPCT Import — reads hp-custom-tables' feature flags and migration records,
|
|
||||||
* maps them to WPDO's 7-state lifecycle, and copies progress data.
|
|
||||||
*
|
|
||||||
* After import, WPDO takes ownership of all 9 HPCT modules and their
|
|
||||||
* corresponding hpct_* tables. The user is advised to deactivate hp-custom-tables.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* - Admin UI: Tools > WP Data Optimizer > HPCT Import tab
|
|
||||||
* - CLI: wp wpdo import-hpct
|
|
||||||
*/
|
|
||||||
class TMDO_HPCT_Import {
|
|
||||||
|
|
||||||
/** Option key that marks import as completed. */
|
|
||||||
public const IMPORTED_OPTION = 'wpdo_hpct_imported';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if HPCT import has already been completed.
|
|
||||||
*/
|
|
||||||
public static function is_imported(): bool {
|
|
||||||
return (bool) get_option( self::IMPORTED_OPTION, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if hp-custom-tables is active and has data to import.
|
|
||||||
*/
|
|
||||||
public static function can_import(): bool {
|
|
||||||
if ( self::is_imported() ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TMDO_Compatibility::is_hpct_active();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preview: return what will be imported without actually doing it.
|
|
||||||
*
|
|
||||||
* @return array{modules: array, migration_records: int, has_data: bool}
|
|
||||||
*/
|
|
||||||
public static function preview(): array {
|
|
||||||
$hpct_features = get_option( 'hpct_features', array() );
|
|
||||||
if ( ! is_array( $hpct_features ) ) {
|
|
||||||
$hpct_features = array();
|
|
||||||
}
|
|
||||||
|
|
||||||
$modules = array();
|
|
||||||
foreach ( TMDO_Feature_Flags::HPCT_MODULES as $module ) {
|
|
||||||
$hpct_status = $hpct_features[ $module ] ?? 'disabled';
|
|
||||||
$wpdo_state = TMDO_Feature_Flags::map_hpct_status( $hpct_status );
|
|
||||||
|
|
||||||
$modules[] = array(
|
|
||||||
'module' => $module,
|
|
||||||
'hpct_status' => $hpct_status,
|
|
||||||
'wpdo_state' => $wpdo_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count HPCT migration records.
|
|
||||||
global $wpdb;
|
|
||||||
$hpct_migrations_table = $wpdb->prefix . 'hpct_migrations';
|
|
||||||
$migration_count = 0;
|
|
||||||
|
|
||||||
// Check if hpct_migrations table exists.
|
|
||||||
if ( TMDO_IS_SQLITE ) {
|
|
||||||
$table_exists = $wpdb->get_var(
|
|
||||||
$wpdb->prepare(
|
|
||||||
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = %s",
|
|
||||||
$hpct_migrations_table
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$table_exists = $wpdb->get_var(
|
|
||||||
$wpdb->prepare(
|
|
||||||
'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = %s',
|
|
||||||
$hpct_migrations_table
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $table_exists ) {
|
|
||||||
$migration_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$hpct_migrations_table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix . 'hpct_migrations'.
|
|
||||||
}
|
|
||||||
|
|
||||||
return array(
|
|
||||||
'modules' => $modules,
|
|
||||||
'migration_records' => $migration_count,
|
|
||||||
'has_data' => ! empty( $hpct_features ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the import.
|
|
||||||
*
|
|
||||||
* @return array{success: bool, message: string, imported: array}
|
|
||||||
*/
|
|
||||||
public static function run(): array {
|
|
||||||
if ( self::is_imported() ) {
|
|
||||||
return array(
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'HPCT import has already been completed.',
|
|
||||||
'imported' => array(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Read HPCT feature flags.
|
|
||||||
$hpct_features = get_option( 'hpct_features', array() );
|
|
||||||
if ( ! is_array( $hpct_features ) ) {
|
|
||||||
$hpct_features = array();
|
|
||||||
}
|
|
||||||
|
|
||||||
$imported = array();
|
|
||||||
|
|
||||||
// Step 2: Map each HPCT module state to WPDO state.
|
|
||||||
foreach ( TMDO_Feature_Flags::HPCT_MODULES as $module ) {
|
|
||||||
$hpct_status = $hpct_features[ $module ] ?? 'disabled';
|
|
||||||
$wpdo_state = TMDO_Feature_Flags::map_hpct_status( $hpct_status );
|
|
||||||
|
|
||||||
TMDO_Feature_Flags::set( $module, $wpdo_state );
|
|
||||||
|
|
||||||
$imported[] = array(
|
|
||||||
'module' => $module,
|
|
||||||
'hpct_status' => $hpct_status,
|
|
||||||
'wpdo_state' => $wpdo_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Copy HPCT migration records to WPDO.
|
|
||||||
self::copy_migration_records();
|
|
||||||
|
|
||||||
// Step 4: Mark import as completed.
|
|
||||||
update_option( self::IMPORTED_OPTION, true );
|
|
||||||
|
|
||||||
return array(
|
|
||||||
'success' => true,
|
|
||||||
'message' => sprintf(
|
|
||||||
'Successfully imported %d HPCT modules. You can now deactivate hp-custom-tables.',
|
|
||||||
count( $imported )
|
|
||||||
),
|
|
||||||
'imported' => $imported,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy HPCT migration progress records to wpdo_migrations.
|
|
||||||
*/
|
|
||||||
private static function copy_migration_records(): void {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$hpct_table = $wpdb->prefix . 'hpct_migrations';
|
|
||||||
$wpdo_table = TMDO_DB::table( 'wpdo_migrations' );
|
|
||||||
$now = TMDO_DB::now();
|
|
||||||
|
|
||||||
// Check if hpct_migrations table exists.
|
|
||||||
$exists = false;
|
|
||||||
if ( TMDO_IS_SQLITE ) {
|
|
||||||
$exists = (bool) $wpdb->get_var(
|
|
||||||
$wpdb->prepare(
|
|
||||||
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = %s",
|
|
||||||
$hpct_table
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$exists = (bool) $wpdb->get_var(
|
|
||||||
$wpdb->prepare(
|
|
||||||
'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = %s',
|
|
||||||
$hpct_table
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! $exists ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$hpct_records = $wpdb->get_results( "SELECT * FROM `{$hpct_table}`", ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix . 'hpct_migrations'.
|
|
||||||
if ( ! $hpct_records ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ( $hpct_records as $record ) {
|
|
||||||
$module = $record['module'] ?? '';
|
|
||||||
if ( ! $module ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map HPCT status to WPDO state.
|
|
||||||
$hpct_status = $record['status'] ?? 'pending';
|
|
||||||
$state = self::map_migration_status( $hpct_status );
|
|
||||||
|
|
||||||
// Check if WPDO already has a record for this module.
|
|
||||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from TMDO_DB::table().
|
|
||||||
$existing = $wpdb->get_var(
|
|
||||||
$wpdb->prepare(
|
|
||||||
"SELECT id FROM `{$wpdo_table}` WHERE module = %s",
|
|
||||||
$module
|
|
||||||
)
|
|
||||||
);
|
|
||||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
|
|
||||||
$data = array(
|
|
||||||
'module' => $module,
|
|
||||||
'zone' => '',
|
|
||||||
'state' => $state,
|
|
||||||
'total_rows' => (int) ( $record['total_rows'] ?? 0 ),
|
|
||||||
'processed_rows' => (int) ( $record['processed_rows'] ?? 0 ),
|
|
||||||
'last_offset' => (int) ( $record['last_offset'] ?? 0 ),
|
|
||||||
'error_count' => (int) ( $record['error_count'] ?? 0 ),
|
|
||||||
'started_at' => $record['started_at'] ?? null,
|
|
||||||
'completed_at' => $record['completed_at'] ?? null,
|
|
||||||
'updated_at' => $now,
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( $existing ) {
|
|
||||||
$wpdb->update( $wpdo_table, $data, array( 'id' => (int) $existing ) );
|
|
||||||
} else {
|
|
||||||
$data['created_at'] = $now;
|
|
||||||
$wpdb->insert( $wpdo_table, $data );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map HPCT migration record status to WPDO migration state.
|
|
||||||
*
|
|
||||||
* @param string $hpct_status HPCT migration status string.
|
|
||||||
* @return string Corresponding WPDO migration state.
|
|
||||||
*/
|
|
||||||
private static function map_migration_status( string $hpct_status ): string {
|
|
||||||
return match ( $hpct_status ) {
|
|
||||||
'pending' => 'idle',
|
|
||||||
'running' => 'backfill',
|
|
||||||
'completed' => 'verify',
|
|
||||||
'failed' => 'idle',
|
|
||||||
default => 'idle',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,17 +8,13 @@
|
|||||||
* Booking model (post_type=hp_booking) — fields are best-effort from the
|
* Booking model (post_type=hp_booking) — fields are best-effort from the
|
||||||
* addon's documented field aliases:
|
* addon's documented field aliases:
|
||||||
*
|
*
|
||||||
* start_time / end_time → unix timestamp range queries (hot)
|
* start_time / end_time → unix timestamp range queries
|
||||||
* status → enum filter (hot)
|
* quantity → places booked, hp_quantity (only written when
|
||||||
* guests → numeric count (hot for slot-availability)
|
* "Allow multiple places" is on)
|
||||||
* listing → post_parent (already a posts column)
|
* listing → post_parent (already a posts column)
|
||||||
*
|
*
|
||||||
* Hot-zone target: `wp_wpdo_hot_hp_booking` with covering indexes on
|
* These keys are flattened by the `hp_booking_core` entity group
|
||||||
* `(listing_id, start_time)` for slot-availability queries and
|
* (TMDO_HivePress_Entity_Groups); the adapter registers no zone fields.
|
||||||
* `(status, start_time)` for vendor dashboard listings.
|
|
||||||
*
|
|
||||||
* The schema MAY drift from real Booking model when the addon is finally
|
|
||||||
* installed; reconcile with `wp wpdo doctor` then.
|
|
||||||
*
|
*
|
||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
@@ -64,73 +60,6 @@ if ( ! class_exists( 'TMDO_HivePress_Bookings_Adapter' ) ) {
|
|||||||
return '1.0.0';
|
return '1.0.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register hot fields for hp_booking post type.
|
|
||||||
*
|
|
||||||
* @param TMDO_Schema_Registry $registry Schema registry singleton.
|
|
||||||
*/
|
|
||||||
public function on_register_fields( TMDO_Schema_Registry $registry ): void {
|
|
||||||
$registry->register_many(
|
|
||||||
$this->plugin_slug(),
|
|
||||||
array(
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_booking',
|
|
||||||
'meta_key' => 'hp_start_time',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_start_time',
|
|
||||||
'indexed' => true,
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_booking',
|
|
||||||
'meta_key' => 'hp_end_time',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_end_time',
|
|
||||||
'indexed' => true,
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_booking',
|
|
||||||
'meta_key' => 'hp_guests',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'int(11) NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_guests',
|
|
||||||
'indexed' => false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Doctor probe — verify hot table when addon is active.
|
|
||||||
*
|
|
||||||
* @return array{ok:bool, message:string}
|
|
||||||
*/
|
|
||||||
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 . 'wpdo_hot_hp_booking';
|
|
||||||
try {
|
|
||||||
$exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
|
|
||||||
return array(
|
|
||||||
'ok' => $exists,
|
|
||||||
'message' => $exists
|
|
||||||
? sprintf( 'hivepress-bookings: %s present', $table )
|
|
||||||
: sprintf( 'hivepress-bookings: %s missing (addon installed but not migrated)', $table ),
|
|
||||||
);
|
|
||||||
} catch ( \Throwable $e ) {
|
|
||||||
return array(
|
|
||||||
'ok' => false,
|
|
||||||
'message' => sprintf( 'hivepress-bookings doctor failed: %s', $e->getMessage() ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 8-D self-score.
|
* 8-D self-score.
|
||||||
*
|
*
|
||||||
@@ -145,15 +74,6 @@ if ( ! class_exists( 'TMDO_HivePress_Bookings_Adapter' ) ) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* FSM modules this adapter contributes.
|
|
||||||
*
|
|
||||||
* @return array<int,string>
|
|
||||||
*/
|
|
||||||
public function migrations(): array {
|
|
||||||
return array( 'hot_hp_booking' );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // end if ( ! class_exists )
|
} // end if ( ! class_exists )
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
/**
|
/**
|
||||||
* Core HivePress adapter — `hivepress` (the parent plugin).
|
* Core HivePress adapter — `hivepress` (the parent plugin).
|
||||||
*
|
*
|
||||||
* Owns three primary HivePress models:
|
* Covers the primary HivePress models:
|
||||||
*
|
*
|
||||||
* - `Listing` (post_type=hp_listing) — Hot zone columns for search/filter
|
* - `Listing` (post_type=hp_listing) — cold blob for the gallery.
|
||||||
* (drafted, featured, verified, expired_time, featured_time).
|
* - `Vendor` (post_type=hp_vendor) — cold blob for the image.
|
||||||
* - `Vendor` (post_type=hp_vendor) — Hot column `verified`; cold blob
|
* - `User` (entity=user) — no zone fields; its `hp_user`
|
||||||
* for image / description (display-only).
|
* entity group is registered by TMDO_HivePress_Entity_Groups.
|
||||||
* - `User` (entity=user) — Entity group `hp_user_core` for
|
*
|
||||||
* `hp_verified` and the WP-stock `first_name` / `last_name` /
|
* Search/filter keys are flattened by TMDO_HivePress_Entity_Groups
|
||||||
* `description`.
|
* (ADR-003: no Hot zone fields).
|
||||||
*
|
*
|
||||||
* This adapter SUPERSEDES the legacy `class-tmdo-hivepress.php` (242 lines).
|
* This adapter SUPERSEDES the legacy `class-tmdo-hivepress.php` (242 lines).
|
||||||
* Behavioural parity:
|
* Behavioural parity:
|
||||||
@@ -24,14 +24,9 @@
|
|||||||
* adapter only registers `hivepress` core fields.
|
* adapter only registers `hivepress` core fields.
|
||||||
* - Vendor cold fields (`hp_image`, `hp_images`) — preserved.
|
* - Vendor cold fields (`hp_image`, `hp_images`) — preserved.
|
||||||
*
|
*
|
||||||
* NEW additions vs legacy:
|
* (This adapter used to register a second `hp_user` definition —
|
||||||
*
|
* hp_verified / hp_avatar_id, keys HivePress never writes — which the
|
||||||
* - `hp_drafted` (Listing) — Hot tinyint(1) for user-dashboard filtering.
|
* registry silently dropped; TMDO_HivePress_Entity_Groups owns it now.)
|
||||||
* - `hp_expired_time` / `hp_featured_time` (Listing) — Hot bigint indexed
|
|
||||||
* for cron expiry scan acceleration (replaces postmeta full scan).
|
|
||||||
* - `hp_user_core` entity group — first_name / last_name / hp_verified
|
|
||||||
* promoted from usermeta to wp_wpdo_user_hp_user (already created by
|
|
||||||
* TMDO_Member_Fields v2.7.0).
|
|
||||||
*
|
*
|
||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
@@ -115,37 +110,10 @@ if ( ! class_exists( 'TMDO_HivePress_Core_Adapter' ) ) {
|
|||||||
* @param TMDO_Schema_Registry $registry Schema registry singleton.
|
* @param TMDO_Schema_Registry $registry Schema registry singleton.
|
||||||
*/
|
*/
|
||||||
public function on_register_fields( TMDO_Schema_Registry $registry ): void {
|
public function on_register_fields( TMDO_Schema_Registry $registry ): void {
|
||||||
// ── Listing hot fields (search/filter accelerators) ──
|
// ── Cold blobs (display-only) ──
|
||||||
$registry->register_many(
|
$registry->register_many(
|
||||||
$this->plugin_slug(),
|
$this->plugin_slug(),
|
||||||
array(
|
array(
|
||||||
// Drafted — used in user dashboard "my drafts" filter.
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_listing',
|
|
||||||
'meta_key' => 'hp_drafted',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'tinyint(1) NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_drafted',
|
|
||||||
'indexed' => true,
|
|
||||||
),
|
|
||||||
// Expired time — replaces `meta_query` clause in hourly cron.
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_listing',
|
|
||||||
'meta_key' => 'hp_expired_time',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_expired_time',
|
|
||||||
'indexed' => true,
|
|
||||||
),
|
|
||||||
// Featured time — same expiry pattern as above.
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_listing',
|
|
||||||
'meta_key' => 'hp_featured_time',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_featured_time',
|
|
||||||
'indexed' => true,
|
|
||||||
),
|
|
||||||
// Vendor cold blob — image attachment id (display-only).
|
// Vendor cold blob — image attachment id (display-only).
|
||||||
array(
|
array(
|
||||||
'post_type' => 'hp_vendor',
|
'post_type' => 'hp_vendor',
|
||||||
@@ -166,47 +134,6 @@ if ( ! class_exists( 'TMDO_HivePress_Core_Adapter' ) ) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register entity field group for HP User model.
|
|
||||||
*
|
|
||||||
* The `hp_user` group is already created by `TMDO_Member_Fields`
|
|
||||||
* (v2.7.0). We add adapter ownership here for cross-cutting tooling
|
|
||||||
* (`wp wpdo hivepress doctor` reports owner; admin UI shows source).
|
|
||||||
*
|
|
||||||
* The `register_entity_fields()` helper from `TMDO_Anti_EAV_Aware`
|
|
||||||
* delegates to `TMDO_Entity_Registry::register_group()` which has
|
|
||||||
* internal dedup — so re-registering with same group name is a no-op.
|
|
||||||
*/
|
|
||||||
public function on_register_entity_fields(): void {
|
|
||||||
if ( ! class_exists( 'TMDO_Entity_Registry' ) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirror the (canonical) group definition from TMDO_Member_Fields::HP_USER_FIELDS.
|
|
||||||
// Source of truth lives in TMDO_Member_Fields; we register defensively for the
|
|
||||||
// case where Member_Fields didn't load (custom builds without that integration).
|
|
||||||
$this->register_entity_fields(
|
|
||||||
'user',
|
|
||||||
'hp_user',
|
|
||||||
array(
|
|
||||||
array(
|
|
||||||
'key' => 'hp_verified',
|
|
||||||
'type' => 'boolean',
|
|
||||||
'default' => false,
|
|
||||||
'searchable' => true,
|
|
||||||
'label' => 'HivePress KYC verified',
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'key' => 'hp_avatar_id',
|
|
||||||
'type' => 'integer',
|
|
||||||
'default' => 0,
|
|
||||||
'searchable' => false,
|
|
||||||
'label' => 'HivePress avatar attachment id',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Query optimization (preserved from legacy class-tmdo-hivepress.php) ──
|
// ── Query optimization (preserved from legacy class-tmdo-hivepress.php) ──
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -318,57 +245,6 @@ if ( ! class_exists( 'TMDO_HivePress_Core_Adapter' ) ) {
|
|||||||
|
|
||||||
// ── Doctor / score / migrations ────────────────────────────────────
|
// ── Doctor / score / migrations ────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Health probe: verify hot tables exist + sample row count.
|
|
||||||
*
|
|
||||||
* @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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$details = array();
|
|
||||||
$ok = true;
|
|
||||||
foreach ( self::HOT_MODELS as $post_type ) {
|
|
||||||
$table = $wpdb->prefix . 'wpdo_hot_' . sanitize_key( $post_type );
|
|
||||||
try {
|
|
||||||
$exists = (bool) $wpdb->get_var(
|
|
||||||
$wpdb->prepare( 'SHOW TABLES LIKE %s', $table )
|
|
||||||
);
|
|
||||||
$details[ $post_type ] = array(
|
|
||||||
'table' => $table,
|
|
||||||
'exists' => $exists,
|
|
||||||
);
|
|
||||||
if ( $exists ) {
|
|
||||||
$details[ $post_type ]['rows'] = (int) $wpdb->get_var(
|
|
||||||
"SELECT COUNT(*) FROM `{$table}`" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Sanitized via sanitize_key + wpdb prefix.
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$ok = false;
|
|
||||||
}
|
|
||||||
} catch ( \Throwable $e ) {
|
|
||||||
$details[ $post_type ] = array(
|
|
||||||
'table' => $table,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
);
|
|
||||||
$ok = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return array(
|
|
||||||
'ok' => $ok,
|
|
||||||
'message' => $ok
|
|
||||||
? 'hivepress core: hot tables OK'
|
|
||||||
: 'hivepress core: one or more hot tables missing or unreadable',
|
|
||||||
'details' => $details,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 8-dimension self-score for the core adapter.
|
* 8-dimension self-score for the core adapter.
|
||||||
*
|
*
|
||||||
@@ -390,18 +266,6 @@ if ( ! class_exists( 'TMDO_HivePress_Core_Adapter' ) ) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* FSM modules this adapter contributes.
|
|
||||||
*
|
|
||||||
* @return array<int,string>
|
|
||||||
*/
|
|
||||||
public function migrations(): array {
|
|
||||||
return array(
|
|
||||||
'hot_hp_listing',
|
|
||||||
'hot_hp_vendor',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // end if ( ! class_exists )
|
} // end if ( ! class_exists )
|
||||||
|
|||||||
@@ -8,27 +8,9 @@
|
|||||||
* - listing → comment_post_ID
|
* - listing → comment_post_ID
|
||||||
* - added_at → comment_date
|
* - added_at → comment_date
|
||||||
*
|
*
|
||||||
* Problem: `wp_comments` has no UNIQUE constraint on (user_id, comment_post_ID, comment_type).
|
* Nothing is stored in comment meta, so there is no EAV surface to flatten:
|
||||||
* A double-tap on the favourite button can race and create two duplicate
|
* the adapter only reports detection and doctor status. (It used to mirror
|
||||||
* favourite rows. HivePress relies on application-level dedup (SELECT before
|
* favorites into `wpdo_comment_hp_favorite`, a table nothing ever created.)
|
||||||
* INSERT) which is not race-safe.
|
|
||||||
*
|
|
||||||
* Fix: shadow table `wp_wpdo_comment_hp_favorite` with UNIQUE(user_id, listing_id).
|
|
||||||
* Adapter mirrors every favorite write into the shadow table; the comment
|
|
||||||
* router rewrites "is X favorited by user Y" reads to hit the shadow table
|
|
||||||
* (1 indexed lookup vs WP_Comment_Query meta scan).
|
|
||||||
*
|
|
||||||
* Schema (declared via expected_columns; created by TMDO_Schema_Manager
|
|
||||||
* when adapter mode flips to dual_write):
|
|
||||||
*
|
|
||||||
* CREATE TABLE wp_wpdo_comment_hp_favorite (
|
|
||||||
* comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
|
|
||||||
* user_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
* listing_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
* created_at DATETIME NOT NULL,
|
|
||||||
* UNIQUE KEY unique_user_listing (user_id, listing_id),
|
|
||||||
* KEY idx_listing (listing_id)
|
|
||||||
* );
|
|
||||||
*
|
*
|
||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
@@ -41,18 +23,15 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
if ( ! class_exists( 'TMDO_HivePress_Favorites_Adapter' ) ) {
|
if ( ! class_exists( 'TMDO_HivePress_Favorites_Adapter' ) ) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Favorites addon adapter.
|
* Favorites addon adapter (detection and doctor only).
|
||||||
*/
|
*/
|
||||||
final class TMDO_HivePress_Favorites_Adapter implements TMDO_HivePress_Adapter {
|
final class TMDO_HivePress_Favorites_Adapter implements TMDO_HivePress_Adapter {
|
||||||
|
|
||||||
use TMDO_HivePress_Adapter_Trait;
|
use TMDO_HivePress_Adapter_Trait;
|
||||||
|
|
||||||
/** Comment_type owned by this adapter. */
|
/** Comment_type of the Favorite model. */
|
||||||
public const COMMENT_TYPE = 'hp_favorite';
|
public const COMMENT_TYPE = 'hp_favorite';
|
||||||
|
|
||||||
/** Shadow table name (without wpdb prefix). */
|
|
||||||
public const TABLE = 'wpdo_comment_hp_favorite';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor — bind register hooks via inherited trait.
|
* Constructor — bind register hooks via inherited trait.
|
||||||
*/
|
*/
|
||||||
@@ -79,156 +58,6 @@ if ( ! class_exists( 'TMDO_HivePress_Favorites_Adapter' ) ) {
|
|||||||
public function minimum_addon_version(): string {
|
public function minimum_addon_version(): string {
|
||||||
return '1.2.0';
|
return '1.2.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',
|
|
||||||
'user_id' => 'BIGINT UNSIGNED NOT NULL',
|
|
||||||
'listing_id' => 'BIGINT UNSIGNED NOT NULL',
|
|
||||||
'created_at' => 'DATETIME NOT NULL',
|
|
||||||
),
|
|
||||||
'indexes' => array(
|
|
||||||
'unique_user_listing' => 'UNIQUE (user_id, listing_id)',
|
|
||||||
'idx_listing' => '(listing_id)',
|
|
||||||
),
|
|
||||||
'doctor_callback' => array( $this, 'doctor_check' ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync wp_comments → shadow table on insert.
|
|
||||||
*
|
|
||||||
* Bound during `on_register_event_hooks()`. The comment-router uses
|
|
||||||
* the shadow table for "is favorited" lookups; sync keeps it consistent.
|
|
||||||
*/
|
|
||||||
public function on_register_event_hooks(): void {
|
|
||||||
add_action( 'wp_insert_comment', array( $this, 'mirror_insert' ), 10, 2 );
|
|
||||||
add_action( 'deleted_comment', array( $this, 'mirror_delete' ), 10, 2 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mirror a newly-inserted hp_favorite 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;
|
|
||||||
$user_id = (int) ( $comment->user_id ?? 0 );
|
|
||||||
$listing_id = (int) ( $comment->comment_post_ID ?? 0 );
|
|
||||||
if ( $user_id <= 0 || $listing_id <= 0 ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
$wpdb->prepare(
|
|
||||||
'INSERT IGNORE INTO `' . $wpdb->prefix . self::TABLE . '` (comment_id, user_id, listing_id, created_at) VALUES (%d, %d, %d, %s)',
|
|
||||||
$comment_id,
|
|
||||||
$user_id,
|
|
||||||
$listing_id,
|
|
||||||
$comment->comment_date ?? current_time( 'mysql' )
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the shadow row when a hp_favorite comment is deleted.
|
|
||||||
*
|
|
||||||
* @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 exists.
|
|
||||||
*
|
|
||||||
* @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-favorites: 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-favorites: %s rows=%d', $table, $rows ),
|
|
||||||
'details' => array(
|
|
||||||
'table' => $table,
|
|
||||||
'rows' => $rows,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch ( \Throwable $e ) {
|
|
||||||
return array(
|
|
||||||
'ok' => false,
|
|
||||||
'message' => sprintf( 'hivepress-favorites doctor failed: %s', $e->getMessage() ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 8-D self-score.
|
|
||||||
*
|
|
||||||
* D1: 1.0 — adapter never calls *_meta()
|
|
||||||
* D2: 1.0 — uses wpdb prefix, no hardcoded table literals
|
|
||||||
* D3: 1.0 — shadow table covers the model
|
|
||||||
* D4: 1.0 — expected_columns + indexes registered
|
|
||||||
* D5: 1.0 — listens to wp_insert_comment for cross-write consistency
|
|
||||||
* D6: 1.0 — zero options/transients
|
|
||||||
* D7: 1.0 — only queries own shadow table
|
|
||||||
* D8: 1.0 — UNIQUE(user_id, listing_id) covering index
|
|
||||||
*/
|
|
||||||
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_favorite' );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // end if ( ! class_exists )
|
} // end if ( ! class_exists )
|
||||||
|
|||||||
@@ -3,16 +3,14 @@
|
|||||||
* Marketplace adapter — `hivepress-marketplace` (DETECT-ONLY in v3.0.0).
|
* Marketplace adapter — `hivepress-marketplace` (DETECT-ONLY in v3.0.0).
|
||||||
*
|
*
|
||||||
* The marketplace addon turns listings into purchasable items, integrating
|
* The marketplace addon turns listings into purchasable items, integrating
|
||||||
* with WooCommerce. It adds:
|
* with WooCommerce. Its filterable, sortable listing attribute `price` is
|
||||||
|
* stored as `hp_price` → decimal(10,2), used in price-range filters. It is
|
||||||
|
* flattened by the `hp_listing_core` entity group
|
||||||
|
* (TMDO_HivePress_Entity_Groups); the adapter registers no zone fields.
|
||||||
*
|
*
|
||||||
* hp_purchase_price → decimal(10,2), used in price-range filters (HOT)
|
* Its other listing attributes (price tiers / extras / discounts, quantity,
|
||||||
* hp_purchase_count → counter (WARM zone, low-criticality)
|
* subscription period, …) are not search filters, and it keeps no per-listing
|
||||||
*
|
* sales counter.
|
||||||
* Note: `hp_purchase_price` is also conditionally registered by the existing
|
|
||||||
* legacy hivepress integration (Sprint 1 left that path) — the dedup in
|
|
||||||
* Schema_Registry is provider+key based, so registering again under the
|
|
||||||
* `hivepress-marketplace` provider is safe and lets `wp wpdo doctor` show
|
|
||||||
* the right ownership.
|
|
||||||
*
|
*
|
||||||
* Detect-only: not installed in dev environment. Schema reserved for the
|
* Detect-only: not installed in dev environment. Schema reserved for the
|
||||||
* day the addon ships.
|
* day the addon ships.
|
||||||
@@ -61,63 +59,6 @@ if ( ! class_exists( 'TMDO_HivePress_Marketplace_Adapter' ) ) {
|
|||||||
return '1.0.0';
|
return '1.0.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register marketplace hot fields on the listing post type.
|
|
||||||
*
|
|
||||||
* @param TMDO_Schema_Registry $registry Schema registry singleton.
|
|
||||||
*/
|
|
||||||
public function on_register_fields( TMDO_Schema_Registry $registry ): void {
|
|
||||||
$registry->register_many(
|
|
||||||
$this->plugin_slug(),
|
|
||||||
array(
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_listing',
|
|
||||||
'meta_key' => 'hp_purchase_price',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'decimal(10,2) DEFAULT NULL',
|
|
||||||
'column' => 'hp_purchase_price',
|
|
||||||
'indexed' => true,
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_listing',
|
|
||||||
'meta_key' => 'hp_purchase_count',
|
|
||||||
'zone' => 'warm',
|
|
||||||
'ttl' => null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Doctor probe — verify hot column exists on listing hot table.
|
|
||||||
*
|
|
||||||
* @return array{ok:bool, message:string}
|
|
||||||
*/
|
|
||||||
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 . 'wpdo_hot_hp_listing';
|
|
||||||
try {
|
|
||||||
$exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
|
|
||||||
return array(
|
|
||||||
'ok' => $exists,
|
|
||||||
'message' => $exists
|
|
||||||
? 'hivepress-marketplace: shares wp_wpdo_hot_hp_listing'
|
|
||||||
: 'hivepress-marketplace: hot listing table missing',
|
|
||||||
);
|
|
||||||
} catch ( \Throwable $e ) {
|
|
||||||
return array(
|
|
||||||
'ok' => false,
|
|
||||||
'message' => sprintf( 'hivepress-marketplace doctor failed: %s', $e->getMessage() ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 8-D self-score.
|
* 8-D self-score.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,18 +5,13 @@
|
|||||||
* Two post-type models:
|
* Two post-type models:
|
||||||
*
|
*
|
||||||
* - Membership_Plan (post_type=hp_membership_plan)
|
* - Membership_Plan (post_type=hp_membership_plan)
|
||||||
* hot fields: hp_expire_period (int), hp_primary (bool)
|
* hp_expire_period (int), hp_primary (bool)
|
||||||
*
|
*
|
||||||
* - Membership (post_type=hp_membership)
|
* - Membership (post_type=hp_membership)
|
||||||
* hot fields: hp_expired_time (unix ts) — checked on every page load
|
* hp_expired_time (unix ts) — checked on every page load
|
||||||
*
|
*
|
||||||
* Both fields registered to Hot zone so:
|
* Flattened by the `hp_membership_plan_core` / `hp_membership_core` entity
|
||||||
* - `hp_expired_time < NOW()` lookups become indexed range scans
|
* groups (TMDO_HivePress_Entity_Groups); the adapter registers no zone fields.
|
||||||
* (replaces O(N) postmeta scan on each request).
|
|
||||||
* - `hp_primary = 1` lookups for "default plan" become O(1).
|
|
||||||
*
|
|
||||||
* Tables `wp_wpdo_hot_hp_membership_plan` and `wp_wpdo_hot_hp_membership`
|
|
||||||
* are created by `TMDO_Schema_Manager` from the registered hot columns.
|
|
||||||
*
|
*
|
||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
@@ -62,81 +57,6 @@ if ( ! class_exists( 'TMDO_HivePress_Memberships_Adapter' ) ) {
|
|||||||
return '2.0.0';
|
return '2.0.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register hot fields for both post types.
|
|
||||||
*
|
|
||||||
* @param TMDO_Schema_Registry $registry Schema registry singleton.
|
|
||||||
*/
|
|
||||||
public function on_register_fields( TMDO_Schema_Registry $registry ): void {
|
|
||||||
$registry->register_many(
|
|
||||||
$this->plugin_slug(),
|
|
||||||
array(
|
|
||||||
// Plan fields.
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_membership_plan',
|
|
||||||
'meta_key' => 'hp_expire_period',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'int(11) NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_expire_period',
|
|
||||||
'indexed' => false,
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_membership_plan',
|
|
||||||
'meta_key' => 'hp_primary',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'tinyint(1) NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_primary',
|
|
||||||
'indexed' => true,
|
|
||||||
),
|
|
||||||
// Membership (subscription) field — hottest path: every page load.
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_membership',
|
|
||||||
'meta_key' => 'hp_expired_time',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_expired_time',
|
|
||||||
'indexed' => true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Doctor probe — verify both hot tables.
|
|
||||||
*
|
|
||||||
* @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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$details = array();
|
|
||||||
$ok = true;
|
|
||||||
foreach ( array( 'hp_membership_plan', 'hp_membership' ) as $pt ) {
|
|
||||||
$table = $wpdb->prefix . 'wpdo_hot_' . sanitize_key( $pt );
|
|
||||||
try {
|
|
||||||
$exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
|
|
||||||
$details[ $pt ]['table'] = $table;
|
|
||||||
$details[ $pt ]['exists'] = $exists;
|
|
||||||
if ( ! $exists ) {
|
|
||||||
$ok = false;
|
|
||||||
}
|
|
||||||
} catch ( \Throwable $e ) {
|
|
||||||
$details[ $pt ]['error'] = $e->getMessage();
|
|
||||||
$ok = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return array(
|
|
||||||
'ok' => $ok,
|
|
||||||
'message' => $ok ? 'hivepress-memberships: hot tables OK' : 'hivepress-memberships: hot tables missing',
|
|
||||||
'details' => $details,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 8-D self-score.
|
* 8-D self-score.
|
||||||
*
|
*
|
||||||
@@ -152,18 +72,6 @@ if ( ! class_exists( 'TMDO_HivePress_Memberships_Adapter' ) ) {
|
|||||||
public function suitability_score(): array {
|
public function suitability_score(): array {
|
||||||
return $this->compose_score( array() );
|
return $this->compose_score( array() );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* FSM modules this adapter contributes.
|
|
||||||
*
|
|
||||||
* @return array<int,string>
|
|
||||||
*/
|
|
||||||
public function migrations(): array {
|
|
||||||
return array(
|
|
||||||
'hot_hp_membership_plan',
|
|
||||||
'hot_hp_membership',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // end if ( ! class_exists )
|
} // end if ( ! class_exists )
|
||||||
|
|||||||
@@ -3,28 +3,12 @@
|
|||||||
* Messages adapter — `hivepress-messages`.
|
* Messages adapter — `hivepress-messages`.
|
||||||
*
|
*
|
||||||
* The Message model extends Comment (`comment_type = hp_message`) and stores
|
* 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,
|
* the recipient in `comment_karma`, the sender in `user_id` and the read flag
|
||||||
* but it leaves the field unindexed for "messages received by user N"
|
* in `comment_approved`.
|
||||||
* 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
|
* Nothing is stored in comment meta, so there is no EAV surface to flatten:
|
||||||
* column + index. Adapter mirrors every hp_message write into the shadow,
|
* the adapter only reports detection and doctor status. (It used to mirror
|
||||||
* and the comment-router rewrites recipient/unread queries to hit the index.
|
* messages into `wpdo_comment_hp_message`, a table nothing ever created.)
|
||||||
*
|
|
||||||
* 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
|
* @package WP_Data_Optimizer
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
@@ -37,18 +21,15 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
if ( ! class_exists( 'TMDO_HivePress_Messages_Adapter' ) ) {
|
if ( ! class_exists( 'TMDO_HivePress_Messages_Adapter' ) ) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Messages addon adapter.
|
* Messages addon adapter (detection and doctor only).
|
||||||
*/
|
*/
|
||||||
final class TMDO_HivePress_Messages_Adapter implements TMDO_HivePress_Adapter {
|
final class TMDO_HivePress_Messages_Adapter implements TMDO_HivePress_Adapter {
|
||||||
|
|
||||||
use TMDO_HivePress_Adapter_Trait;
|
use TMDO_HivePress_Adapter_Trait;
|
||||||
|
|
||||||
/** Comment_type owned by this adapter. */
|
/** Comment_type of the Message model. */
|
||||||
public const COMMENT_TYPE = 'hp_message';
|
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.
|
* Constructor — bind register hooks via inherited trait.
|
||||||
*/
|
*/
|
||||||
@@ -75,189 +56,6 @@ if ( ! class_exists( 'TMDO_HivePress_Messages_Adapter' ) ) {
|
|||||||
public function minimum_addon_version(): string {
|
public function minimum_addon_version(): string {
|
||||||
return '1.4.0';
|
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 )
|
} // end if ( ! class_exists )
|
||||||
|
|||||||
@@ -5,25 +5,13 @@
|
|||||||
* Two related models:
|
* Two related models:
|
||||||
*
|
*
|
||||||
* - Request (post_type=hp_request)
|
* - Request (post_type=hp_request)
|
||||||
* hot fields: hp_drafted (bool), hp_expired_time (unix ts)
|
* hp_drafted / hp_expired_time are flattened by the `hp_draft_flags`
|
||||||
* — same access pattern as Listing (user dashboard + expiry cron)
|
* and `hp_request_core` entity groups (TMDO_HivePress_Entity_Groups).
|
||||||
*
|
*
|
||||||
* - Offer (comment_type=hp_offer, parent post_type=hp_request)
|
* - Offer (comment_type=hp_offer, parent post_type=hp_request)
|
||||||
* comment-column based; benefit comes from comment-router using
|
* stored in comment columns only — no comment meta to flatten.
|
||||||
* (comment_post_ID, comment_type) covering index + dedicated shadow
|
* (The adapter used to mirror offers into `wpdo_comment_hp_offer`,
|
||||||
* table for "approved offers per request" aggregation.
|
* a table nothing ever created.)
|
||||||
*
|
|
||||||
* Shadow table for offers (analogous to favorites pattern):
|
|
||||||
*
|
|
||||||
* CREATE TABLE wp_wpdo_comment_hp_offer (
|
|
||||||
* comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
|
|
||||||
* request_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
* bidder_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
* approved TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
* created_at DATETIME NOT NULL,
|
|
||||||
* KEY idx_request_approved (request_id, approved),
|
|
||||||
* KEY idx_bidder (bidder_id)
|
|
||||||
* );
|
|
||||||
*
|
*
|
||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
@@ -45,9 +33,6 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) {
|
|||||||
/** Comment_type for the Offer side of Requests. */
|
/** Comment_type for the Offer side of Requests. */
|
||||||
public const OFFER_COMMENT_TYPE = 'hp_offer';
|
public const OFFER_COMMENT_TYPE = 'hp_offer';
|
||||||
|
|
||||||
/** Shadow table for offers (without wpdb prefix). */
|
|
||||||
public const OFFER_TABLE = 'wpdo_comment_hp_offer';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor — bind register hooks via inherited trait.
|
* Constructor — bind register hooks via inherited trait.
|
||||||
*/
|
*/
|
||||||
@@ -74,194 +59,6 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) {
|
|||||||
public function minimum_addon_version(): string {
|
public function minimum_addon_version(): string {
|
||||||
return '1.2.0';
|
return '1.2.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register hot fields for hp_request post type.
|
|
||||||
*
|
|
||||||
* @param TMDO_Schema_Registry $registry Schema registry singleton.
|
|
||||||
*/
|
|
||||||
public function on_register_fields( TMDO_Schema_Registry $registry ): void {
|
|
||||||
$registry->register_many(
|
|
||||||
$this->plugin_slug(),
|
|
||||||
array(
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_request',
|
|
||||||
'meta_key' => 'hp_drafted',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'tinyint(1) NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_drafted',
|
|
||||||
'indexed' => true,
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'post_type' => 'hp_request',
|
|
||||||
'meta_key' => 'hp_expired_time',
|
|
||||||
'zone' => 'hot',
|
|
||||||
'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0',
|
|
||||||
'column' => 'hp_expired_time',
|
|
||||||
'indexed' => true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Declare offer shadow table.
|
|
||||||
*
|
|
||||||
* @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::OFFER_TABLE,
|
|
||||||
'primary_key' => 'comment_id',
|
|
||||||
'post_type_link' => 'hp_request',
|
|
||||||
'expected_columns' => array(
|
|
||||||
'comment_id' => 'BIGINT UNSIGNED NOT NULL PRIMARY KEY',
|
|
||||||
'request_id' => 'BIGINT UNSIGNED NOT NULL',
|
|
||||||
'bidder_id' => 'BIGINT UNSIGNED NOT NULL',
|
|
||||||
'approved' => 'TINYINT(1) NOT NULL DEFAULT 0',
|
|
||||||
'created_at' => 'DATETIME NOT NULL',
|
|
||||||
),
|
|
||||||
'indexes' => array(
|
|
||||||
'idx_request_approved' => '(request_id, approved)',
|
|
||||||
'idx_bidder' => '(bidder_id)',
|
|
||||||
),
|
|
||||||
'doctor_callback' => array( $this, 'doctor_check' ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mirror new hp_offer comments into the shadow table.
|
|
||||||
*/
|
|
||||||
public function on_register_event_hooks(): void {
|
|
||||||
add_action( 'wp_insert_comment', array( $this, 'mirror_offer_insert' ), 10, 2 );
|
|
||||||
add_action( 'edit_comment', array( $this, 'mirror_offer_update' ), 10, 2 );
|
|
||||||
add_action( 'deleted_comment', array( $this, 'mirror_offer_delete' ), 10, 2 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a hp_offer mirror row.
|
|
||||||
*
|
|
||||||
* @param int $comment_id Comment ID.
|
|
||||||
* @param object|\WP_Comment $comment Comment object.
|
|
||||||
*/
|
|
||||||
public function mirror_offer_insert( int $comment_id, $comment ): void {
|
|
||||||
if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::OFFER_COMMENT_TYPE ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
global $wpdb;
|
|
||||||
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
$wpdb->prepare(
|
|
||||||
'INSERT INTO `' . $wpdb->prefix . self::OFFER_TABLE . '` (comment_id, request_id, bidder_id, approved, created_at) VALUES (%d, %d, %d, %d, %s) ON DUPLICATE KEY UPDATE approved = VALUES(approved)',
|
|
||||||
$comment_id,
|
|
||||||
(int) ( $comment->comment_post_ID ?? 0 ),
|
|
||||||
(int) ( $comment->user_id ?? 0 ),
|
|
||||||
(int) ( $comment->comment_approved ?? 0 ),
|
|
||||||
$comment->comment_date ?? current_time( 'mysql' )
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update mirror row when comment is edited (approval flip is the hot path).
|
|
||||||
*
|
|
||||||
* @param int $comment_id Comment ID.
|
|
||||||
* @param array $data Comment data array passed by edit_comment.
|
|
||||||
*/
|
|
||||||
public function mirror_offer_update( int $comment_id, $data ): void {
|
|
||||||
unset( $data );
|
|
||||||
global $wpdb;
|
|
||||||
// Re-fetch comment object for accurate state then re-mirror via insert path.
|
|
||||||
if ( function_exists( 'get_comment' ) ) {
|
|
||||||
$comment = get_comment( $comment_id );
|
|
||||||
if ( $comment ) {
|
|
||||||
$this->mirror_offer_insert( $comment_id, $comment );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset( $wpdb );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove mirror row on hp_offer deletion.
|
|
||||||
*
|
|
||||||
* @param int $comment_id Comment ID.
|
|
||||||
* @param object|\WP_Comment $comment Comment object.
|
|
||||||
*/
|
|
||||||
public function mirror_offer_delete( int $comment_id, $comment ): void {
|
|
||||||
if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::OFFER_COMMENT_TYPE ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
global $wpdb;
|
|
||||||
$wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
||||||
$wpdb->prefix . self::OFFER_TABLE,
|
|
||||||
array( 'comment_id' => $comment_id ),
|
|
||||||
array( '%d' )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Doctor probe — request hot table + offer 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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$details = array();
|
|
||||||
$ok = true;
|
|
||||||
foreach ( array(
|
|
||||||
'hp_request_hot' => $wpdb->prefix . 'wpdo_hot_hp_request',
|
|
||||||
'hp_offer_shadow' => $wpdb->prefix . self::OFFER_TABLE,
|
|
||||||
) as $key => $table ) {
|
|
||||||
try {
|
|
||||||
$exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
|
|
||||||
$details[ $key ] = array(
|
|
||||||
'table' => $table,
|
|
||||||
'exists' => $exists,
|
|
||||||
);
|
|
||||||
if ( ! $exists ) {
|
|
||||||
$ok = false;
|
|
||||||
}
|
|
||||||
} catch ( \Throwable $e ) {
|
|
||||||
$details[ $key ] = array(
|
|
||||||
'table' => $table,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
);
|
|
||||||
$ok = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return array(
|
|
||||||
'ok' => $ok,
|
|
||||||
'message' => $ok ? 'hivepress-requests: tables OK' : 'hivepress-requests: one or more tables missing',
|
|
||||||
'details' => $details,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 8-D self-score.
|
|
||||||
*/
|
|
||||||
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(
|
|
||||||
'hot_hp_request',
|
|
||||||
'comment_hp_offer',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // end if ( ! class_exists )
|
} // end if ( ! class_exists )
|
||||||
|
|||||||
@@ -10,14 +10,12 @@
|
|||||||
* - approved → comment_approved
|
* - approved → comment_approved
|
||||||
* - listing → comment_post_ID
|
* - listing → comment_post_ID
|
||||||
*
|
*
|
||||||
* No postmeta is used; no entity-bridge group needed. Hot-path optimization
|
* No postmeta is used; no entity-bridge group needed. Queries use the core
|
||||||
* comes from `TMDO_HivePress_Comment_Router` rewriting comment-query SQL to
|
* `wp_comments` indexes on `comment_post_ID` / `comment_type`, and the
|
||||||
* use `comment_post_ID + comment_type + comment_approved` index, and from
|
* `wp_wpdo_comment_hp_review` flat table is maintained by the WPDO Comment
|
||||||
* the `wp_wpdo_comment_hp_review` flat table that WPDO Comment entity bridge
|
* entity bridge (since v2.13.x).
|
||||||
* already maintains (since v2.13.x).
|
|
||||||
*
|
*
|
||||||
* This adapter's role:
|
* This adapter's role:
|
||||||
* - Declare the comment_type to the comment-router
|
|
||||||
* - Doctor probe of the entity-bridge table
|
* - Doctor probe of the entity-bridge table
|
||||||
* - Self-report 8-D suitability score
|
* - Self-report 8-D suitability score
|
||||||
*
|
*
|
||||||
@@ -124,7 +122,7 @@ if ( ! class_exists( 'TMDO_HivePress_Reviews_Adapter' ) ) {
|
|||||||
* D5: 1.0 — no cross-adapter writes needed yet
|
* D5: 1.0 — no cross-adapter writes needed yet
|
||||||
* D6: 1.0 — zero options/transients written
|
* D6: 1.0 — zero options/transients written
|
||||||
* D7: 1.0 — only queries own entity-bridge table
|
* D7: 1.0 — only queries own entity-bridge table
|
||||||
* D8: 1.0 — comment-router uses (comment_post_ID, comment_type) covering index
|
* D8: 1.0 — reads use the core wp_comments (comment_post_ID, comment_type) indexes
|
||||||
*/
|
*/
|
||||||
public function suitability_score(): array {
|
public function suitability_score(): array {
|
||||||
return $this->compose_score( array() );
|
return $this->compose_score( array() );
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
/**
|
/**
|
||||||
* Social Links adapter — `hivepress-social-links`.
|
* Social Links adapter — `hivepress-social-links`.
|
||||||
*
|
*
|
||||||
* Adds social URL fields to vendor profiles (Facebook, Instagram, Twitter,
|
* The addon adds one attribute per social link the admin enables, separately
|
||||||
* LinkedIn, YouTube, etc.). Stored in postmeta as `hp_social_*` keys.
|
* for listings (`hp_listing_social_links`) and vendors (`hp_vendor_social_links`).
|
||||||
|
* Each attribute is stored in postmeta as `hp_{name}` (e.g. `hp_facebook`).
|
||||||
*
|
*
|
||||||
* Display-only — never used in search/filter — so the right zone is Cold
|
* Display-only — never used in search/filter — so the right zone is Cold
|
||||||
* (object cache + JSON blob fallback). Listing page reads ~7 social URLs;
|
* (object cache + JSON blob fallback): one blob per post replaces one
|
||||||
* caching them as a single blob keyed on vendor_id eliminates 7 postmeta
|
* postmeta lookup per link on every render.
|
||||||
* lookups per render.
|
|
||||||
*
|
*
|
||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
@@ -27,25 +27,6 @@ if ( ! class_exists( 'TMDO_HivePress_Social_Links_Adapter' ) ) {
|
|||||||
|
|
||||||
use TMDO_HivePress_Adapter_Trait;
|
use TMDO_HivePress_Adapter_Trait;
|
||||||
|
|
||||||
/**
|
|
||||||
* Social URL meta keys owned by this addon.
|
|
||||||
*
|
|
||||||
* Sources (HivePress 1.0.3 default field set):
|
|
||||||
* facebook_url, instagram_url, twitter_url, linkedin_url,
|
|
||||||
* youtube_url, telegram_url, whatsapp_url
|
|
||||||
*
|
|
||||||
* @var array<int,string>
|
|
||||||
*/
|
|
||||||
private const SOCIAL_KEYS = array(
|
|
||||||
'hp_facebook_url',
|
|
||||||
'hp_instagram_url',
|
|
||||||
'hp_twitter_url',
|
|
||||||
'hp_linkedin_url',
|
|
||||||
'hp_youtube_url',
|
|
||||||
'hp_telegram_url',
|
|
||||||
'hp_whatsapp_url',
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor — bind register hooks via inherited trait.
|
* Constructor — bind register hooks via inherited trait.
|
||||||
*/
|
*/
|
||||||
@@ -74,24 +55,44 @@ if ( ! class_exists( 'TMDO_HivePress_Social_Links_Adapter' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register vendor social URL fields to Cold zone.
|
* Register the enabled social link fields to the Cold zone.
|
||||||
*
|
*
|
||||||
* @param TMDO_Schema_Registry $registry Schema registry singleton.
|
* @param TMDO_Schema_Registry $registry Schema registry singleton.
|
||||||
*/
|
*/
|
||||||
public function on_register_fields( TMDO_Schema_Registry $registry ): void {
|
public function on_register_fields( TMDO_Schema_Registry $registry ): void {
|
||||||
$fields = array();
|
$fields = array();
|
||||||
foreach ( self::SOCIAL_KEYS as $key ) {
|
foreach ( $this->enabled_meta_keys() as $post_type => $keys ) {
|
||||||
$fields[] = array(
|
foreach ( $keys as $key ) {
|
||||||
'post_type' => 'hp_vendor',
|
$fields[] = array(
|
||||||
'meta_key' => $key,
|
'post_type' => $post_type,
|
||||||
'zone' => 'cold',
|
'meta_key' => $key,
|
||||||
'cache_group' => 'wpdo_cold_hp_vendor',
|
'zone' => 'cold',
|
||||||
'cache_ttl' => HOUR_IN_SECONDS,
|
'cache_group' => 'wpdo_cold_' . $post_type,
|
||||||
);
|
'cache_ttl' => HOUR_IN_SECONDS,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$registry->register_many( $this->plugin_slug(), $fields );
|
$registry->register_many( $this->plugin_slug(), $fields );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta keys of the social links enabled in the addon settings.
|
||||||
|
*
|
||||||
|
* @return array<string, array<int, string>> post_type => meta keys.
|
||||||
|
*/
|
||||||
|
private function enabled_meta_keys(): array {
|
||||||
|
$keys = array();
|
||||||
|
foreach ( array( 'listing', 'vendor' ) as $model ) {
|
||||||
|
foreach ( (array) get_option( 'hp_' . $model . '_social_links', array() ) as $name ) {
|
||||||
|
$name = sanitize_key( (string) $name );
|
||||||
|
if ( '' !== $name ) {
|
||||||
|
$keys[ 'hp_' . $model ][] = 'hp_' . $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $keys;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Doctor probe — cold zone uses cache; verify cache layer is reachable.
|
* Doctor probe — cold zone uses cache; verify cache layer is reachable.
|
||||||
*
|
*
|
||||||
@@ -100,7 +101,7 @@ if ( ! class_exists( 'TMDO_HivePress_Social_Links_Adapter' ) ) {
|
|||||||
public function doctor_check(): array {
|
public function doctor_check(): array {
|
||||||
return array(
|
return array(
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'message' => sprintf( 'hivepress-social-links: %d cold fields registered', count( self::SOCIAL_KEYS ) ),
|
'message' => sprintf( 'hivepress-social-links: %d cold fields registered', array_sum( array_map( 'count', $this->enabled_meta_keys() ) ) ),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,12 +118,9 @@ if ( ! class_exists( 'TMDO_HivePress_Bootstrap' ) ) {
|
|||||||
self::bind_adapter( $slug, $version );
|
self::bind_adapter( $slug, $version );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: register cross-cutting components (router + cron optimizer).
|
// Step 5: register the cron optimizer. It is gated by its own opt-in
|
||||||
// Both are gated by their own opt-in option and only activate when
|
// option and only activates when the operator flips it; safe to call
|
||||||
// the operator flips it; safe to call here unconditionally.
|
// here unconditionally.
|
||||||
if ( class_exists( 'TMDO_HivePress_Comment_Router' ) ) {
|
|
||||||
TMDO_HivePress_Comment_Router::register();
|
|
||||||
}
|
|
||||||
if ( class_exists( 'TMDO_HivePress_Cron_Optimizer' ) ) {
|
if ( class_exists( 'TMDO_HivePress_Cron_Optimizer' ) ) {
|
||||||
TMDO_HivePress_Cron_Optimizer::register();
|
TMDO_HivePress_Cron_Optimizer::register();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Comment query router for HivePress comment-type models.
|
|
||||||
*
|
|
||||||
* Rewrites `WP_Comment_Query` SQL clauses to JOIN the per-addon shadow tables
|
|
||||||
* built by Sprint 2 adapters. Currently handles four `comment_type` values:
|
|
||||||
*
|
|
||||||
* hp_message → wp_wpdo_comment_hp_message (recipient_id, is_read indexed)
|
|
||||||
* hp_favorite → wp_wpdo_comment_hp_favorite (UNIQUE user_id+listing_id)
|
|
||||||
* hp_offer → wp_wpdo_comment_hp_offer (request_id, approved indexed)
|
|
||||||
* hp_review → wp_wpdo_comment_hp_review (managed by entity-bridge)
|
|
||||||
*
|
|
||||||
* Disabled by default. Operator opts in via:
|
|
||||||
*
|
|
||||||
* wp option update wpdo_hivepress_comment_router_enabled 1
|
|
||||||
*
|
|
||||||
* Once enabled, comment queries with `type` matching one of the above gain a
|
|
||||||
* shadow-table LEFT JOIN at `comments_clauses` filter time. The JOIN is
|
|
||||||
* deliberately LEFT so missing shadow rows (during dual_write phase before
|
|
||||||
* backfill completes) don't drop legitimate comments.
|
|
||||||
*
|
|
||||||
* Per Karpathy guideline: this router only handles the four comment_types
|
|
||||||
* Sprint 2 actually needs — no extension hooks for "future addons".
|
|
||||||
*
|
|
||||||
* @package WP_Data_Optimizer
|
|
||||||
* @since 3.0.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! class_exists( 'TMDO_HivePress_Comment_Router' ) ) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Comment query rewriter.
|
|
||||||
*
|
|
||||||
* Single-instance, registered via static `register()`. Stateless apart from
|
|
||||||
* the option-cached enabled flag.
|
|
||||||
*/
|
|
||||||
final class TMDO_HivePress_Comment_Router {
|
|
||||||
|
|
||||||
/** Option key controlling the rewriter. */
|
|
||||||
public const OPTION_ENABLED = 'wpdo_hivepress_comment_router_enabled';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of comment_type → shadow table (without wpdb prefix).
|
|
||||||
*
|
|
||||||
* @var array<string,string>
|
|
||||||
*/
|
|
||||||
private const SHADOW_MAP = array(
|
|
||||||
'hp_message' => 'wpdo_comment_hp_message',
|
|
||||||
'hp_favorite' => 'wpdo_comment_hp_favorite',
|
|
||||||
'hp_offer' => 'wpdo_comment_hp_offer',
|
|
||||||
'hp_review' => 'wpdo_comment_hp_review',
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the router is bound this request.
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
private static bool $bound = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind the comments_clauses filter (idempotent).
|
|
||||||
*
|
|
||||||
* Bootstrap calls this from each adapter's on_register_query_hooks()
|
|
||||||
* so router activation follows the same gate as adapter binding.
|
|
||||||
*/
|
|
||||||
public static function register(): void {
|
|
||||||
if ( self::$bound ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ( ! self::is_enabled() ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
add_filter( 'comments_clauses', array( __CLASS__, 'rewrite' ), 20, 2 );
|
|
||||||
self::$bound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset internal state (test only).
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
public static function reset_for_tests(): void {
|
|
||||||
self::$bound = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether router is enabled via the operator option.
|
|
||||||
*/
|
|
||||||
public static function is_enabled(): bool {
|
|
||||||
if ( ! function_exists( 'get_option' ) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (bool) (int) get_option( self::OPTION_ENABLED, 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter callback: rewrite comments_clauses for known hp comment types.
|
|
||||||
*
|
|
||||||
* @param array $clauses SQL clauses (join, where, fields, ...).
|
|
||||||
* @param \WP_Comment_Query $query The query object.
|
|
||||||
* @return array Possibly-modified clauses.
|
|
||||||
*/
|
|
||||||
public static function rewrite( array $clauses, $query ): array {
|
|
||||||
$type = self::query_comment_type( $query );
|
|
||||||
if ( '' === $type || ! isset( self::SHADOW_MAP[ $type ] ) ) {
|
|
||||||
return $clauses;
|
|
||||||
}
|
|
||||||
|
|
||||||
global $wpdb;
|
|
||||||
$shadow = $wpdb->prefix . self::SHADOW_MAP[ $type ];
|
|
||||||
$alias = 'wpdo_shadow';
|
|
||||||
|
|
||||||
// LEFT JOIN keeps row visibility when shadow lags (during backfill).
|
|
||||||
$join = ( $clauses['join'] ?? '' ) . " LEFT JOIN `{$shadow}` AS {$alias} ON {$alias}.comment_id = {$wpdb->comments}.comment_ID";
|
|
||||||
|
|
||||||
// Filter pushdown: hp_message recipient lookup uses comment_karma in HP.
|
|
||||||
// We rewrite WHERE clauses that filter by comment_karma (= recipient hack)
|
|
||||||
// to use the indexed shadow column.
|
|
||||||
$where = (string) ( $clauses['where'] ?? '' );
|
|
||||||
if ( 'hp_message' === $type && '' !== $where ) {
|
|
||||||
$where = preg_replace(
|
|
||||||
'/' . preg_quote( $wpdb->comments, '/' ) . '\.comment_karma\s*=\s*(\d+)/',
|
|
||||||
$alias . '.recipient_id = $1',
|
|
||||||
$where
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$clauses['join'] = $join;
|
|
||||||
$clauses['where'] = $where;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter: wpdo_hivepress_comment_router_clauses
|
|
||||||
*
|
|
||||||
* Allows fine-grained tweaks per comment_type. Intended for adapter
|
|
||||||
* tests + future custom-column lookups.
|
|
||||||
*
|
|
||||||
* @param array $clauses Modified clauses.
|
|
||||||
* @param string $type Matched comment_type.
|
|
||||||
* @param string $alias Shadow table alias.
|
|
||||||
*/
|
|
||||||
return apply_filters( 'wpdo_hivepress_comment_router_clauses', $clauses, $type, $alias );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract the comment_type from a WP_Comment_Query.
|
|
||||||
*
|
|
||||||
* @param \WP_Comment_Query $query Query object.
|
|
||||||
*/
|
|
||||||
private static function query_comment_type( $query ): string {
|
|
||||||
if ( ! is_object( $query ) || ! isset( $query->query_vars ) ) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
$type = $query->query_vars['type'] ?? '';
|
|
||||||
if ( is_array( $type ) ) {
|
|
||||||
$type = reset( $type );
|
|
||||||
}
|
|
||||||
return is_string( $type ) ? $type : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // end if ( ! class_exists )
|
|
||||||
@@ -10,9 +10,8 @@
|
|||||||
*
|
*
|
||||||
* Strategy: detect, log, render admin notice with guidance, AND short-circuit
|
* Strategy: detect, log, render admin notice with guidance, AND short-circuit
|
||||||
* the WPDO HivePress bootstrap via `wpdo_hivepress_should_bind` filter so
|
* the WPDO HivePress bootstrap via `wpdo_hivepress_should_bind` filter so
|
||||||
* adapters are NOT bound. Operator must explicitly run
|
* adapters are NOT bound. The operator deactivates the legacy plugins to let
|
||||||
* `wp wpdo hivepress migrate-from-hpct` to switch to WPDO and deactivate
|
* WPDO take over; data kept in their own tables is not imported.
|
||||||
* the legacy plugins.
|
|
||||||
*
|
*
|
||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
@@ -163,7 +162,7 @@ if ( ! class_exists( 'TMDO_HivePress_Conflict_Guard' ) ) {
|
|||||||
|
|
||||||
$message = sprintf(
|
$message = sprintf(
|
||||||
/* translators: %s: comma-separated list of conflicting plugin names. */
|
/* translators: %s: comma-separated list of conflicting plugin names. */
|
||||||
__( 'wp-data-optimizer detected legacy HivePress data-layer plugins: %s. WPDO HivePress integration is paused to prevent dual-write corruption. Run `wp wpdo hivepress migrate-from-hpct` to migrate, then deactivate the legacy plugins.', 'tmdo-hivepress' ),
|
__( 'wp-data-optimizer detected legacy HivePress data-layer plugins: %s. WPDO HivePress integration is paused to prevent dual-write corruption. Deactivate the legacy plugins to let WPDO take over (data in their own tables is not imported).', 'tmdo-hivepress' ),
|
||||||
implode( ', ', $names )
|
implode( ', ', $names )
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -206,7 +205,7 @@ if ( ! class_exists( 'TMDO_HivePress_Conflict_Guard' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$message = sprintf(
|
$message = sprintf(
|
||||||
'Conflict detected: %s. wp-data-optimizer fully supersedes these. Run: wp wpdo hivepress migrate-from-hpct',
|
'Conflict detected: %s. wp-data-optimizer fully supersedes these. Deactivate them to let WPDO take over.',
|
||||||
implode( ', ', $conflicts )
|
implode( ', ', $conflicts )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ if ( ! interface_exists( 'TMDO_HivePress_Adapter' ) ) {
|
|||||||
*
|
*
|
||||||
* Adapters with hot zone fields used in search/filter MUST register a
|
* Adapters with hot zone fields used in search/filter MUST register a
|
||||||
* `pre_get_posts` / `pre_get_comments` filter that defers to
|
* `pre_get_posts` / `pre_get_comments` filter that defers to
|
||||||
* `TMDO_HivePress_Query_Router` / `..._Comment_Router`.
|
* `TMDO_HivePress_Query_Router`.
|
||||||
*
|
*
|
||||||
* Called on `init:5` after Schema_Registry is fully populated.
|
* Called on `init:5` after Schema_Registry is fully populated.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
-2
@@ -38,7 +38,6 @@ $tmdo_hp_files = array(
|
|||||||
'includes/hivepress/class-tmdo-hivepress-detector.php',
|
'includes/hivepress/class-tmdo-hivepress-detector.php',
|
||||||
'includes/hivepress/class-tmdo-hivepress-conflict-guard.php',
|
'includes/hivepress/class-tmdo-hivepress-conflict-guard.php',
|
||||||
'includes/hivepress/class-tmdo-hivepress-bootstrap.php',
|
'includes/hivepress/class-tmdo-hivepress-bootstrap.php',
|
||||||
'includes/hivepress/class-tmdo-hivepress-comment-router.php',
|
|
||||||
'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php',
|
'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php',
|
||||||
'includes/hivepress/class-tmdo-hivepress-attribute-bridge.php',
|
'includes/hivepress/class-tmdo-hivepress-attribute-bridge.php',
|
||||||
'includes/hivepress/class-tmdo-hivepress-suitability-scorer.php',
|
'includes/hivepress/class-tmdo-hivepress-suitability-scorer.php',
|
||||||
@@ -46,7 +45,7 @@ $tmdo_hp_files = array(
|
|||||||
'includes/hivepress/class-tmdo-hivepress-rest.php',
|
'includes/hivepress/class-tmdo-hivepress-rest.php',
|
||||||
'includes/class-tmdo-hivepress-transient-filter.php',
|
'includes/class-tmdo-hivepress-transient-filter.php',
|
||||||
'includes/class-tmdo-hivepress-term-comment-fields.php',
|
'includes/class-tmdo-hivepress-term-comment-fields.php',
|
||||||
'includes/class-tmdo-hpct-import.php',
|
'includes/class-tmdo-hivepress-entity-groups.php',
|
||||||
'includes/class-tmdo-listing-stats.php',
|
'includes/class-tmdo-listing-stats.php',
|
||||||
'admin/class-tmdo-admin-hivepress.php',
|
'admin/class-tmdo-admin-hivepress.php',
|
||||||
'cli/class-tmdo-cli-hivepress.php',
|
'cli/class-tmdo-cli-hivepress.php',
|
||||||
|
|||||||
@@ -12,147 +12,17 @@ use PHPUnit\Framework\TestCase;
|
|||||||
* Exercises against real MariaDB (via tests/integration/bootstrap.php) the
|
* Exercises against real MariaDB (via tests/integration/bootstrap.php) the
|
||||||
* scenarios that unit tests can only stub:
|
* scenarios that unit tests can only stub:
|
||||||
*
|
*
|
||||||
* - Shadow table CREATE actually works with the declared expected_columns
|
|
||||||
* - UNIQUE constraint on (user_id, listing_id) really prevents dup favorites
|
|
||||||
* - mirror_insert() roundtrips through real $wpdb (SQL syntax valid)
|
|
||||||
* - 7-state FSM lifecycle moves WPDO_Feature_Flags correctly
|
* - 7-state FSM lifecycle moves WPDO_Feature_Flags correctly
|
||||||
*
|
*
|
||||||
* @group integration
|
* @group integration
|
||||||
*/
|
*/
|
||||||
class HivepressIntegrationTest extends TestCase {
|
class HivepressIntegrationTest extends TestCase {
|
||||||
|
|
||||||
/** Test-scope shadow table for favorites. */
|
|
||||||
private const TABLE_FAVORITE = 'wp_itest_wpdo_comment_hp_favorite';
|
|
||||||
|
|
||||||
/** Test-scope shadow table for messages. */
|
|
||||||
private const TABLE_MESSAGE = 'wp_itest_wpdo_comment_hp_message';
|
|
||||||
|
|
||||||
public static function setUpBeforeClass(): void {
|
|
||||||
// HivePress integration files are loaded at file-top level so the
|
|
||||||
// Testable* subclass definitions below can resolve their parents at
|
|
||||||
// parse time. Schema setup happens here.
|
|
||||||
global $wpdb;
|
|
||||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_FAVORITE . '`' );
|
|
||||||
$wpdb->query( "CREATE TABLE `" . self::TABLE_FAVORITE . "` (
|
|
||||||
comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
|
|
||||||
user_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
listing_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
created_at DATETIME NOT NULL,
|
|
||||||
UNIQUE KEY unique_user_listing (user_id, listing_id),
|
|
||||||
KEY idx_listing (listing_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" );
|
|
||||||
|
|
||||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_MESSAGE . '`' );
|
|
||||||
$wpdb->query( "CREATE TABLE `" . self::TABLE_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)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function tearDownAfterClass(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_FAVORITE . '`' );
|
|
||||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_MESSAGE . '`' );
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function setUp(): void {
|
protected function setUp(): void {
|
||||||
global $wpdb;
|
|
||||||
$wpdb->query( 'TRUNCATE TABLE `' . self::TABLE_FAVORITE . '`' );
|
|
||||||
$wpdb->query( 'TRUNCATE TABLE `' . self::TABLE_MESSAGE . '`' );
|
|
||||||
// Clear FSM module state between tests.
|
// Clear FSM module state between tests.
|
||||||
$GLOBALS['_wp_options']['wpdo_features'] = array();
|
$GLOBALS['_wp_options']['wpdo_features'] = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Schema tests ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public function test_favorite_table_has_unique_user_listing_constraint(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$row = $wpdb->get_row( 'SHOW CREATE TABLE `' . self::TABLE_FAVORITE . '`', 'ARRAY_A' );
|
|
||||||
$this->assertNotNull( $row );
|
|
||||||
$ddl = $row['Create Table'] ?? '';
|
|
||||||
$this->assertStringContainsString( 'UNIQUE KEY', $ddl );
|
|
||||||
$this->assertStringContainsString( 'unique_user_listing', $ddl );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_message_table_has_recipient_unread_index(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$row = $wpdb->get_row( 'SHOW CREATE TABLE `' . self::TABLE_MESSAGE . '`', 'ARRAY_A' );
|
|
||||||
$this->assertNotNull( $row );
|
|
||||||
$ddl = $row['Create Table'] ?? '';
|
|
||||||
$this->assertStringContainsString( 'idx_recipient_unread', $ddl );
|
|
||||||
$this->assertStringContainsString( 'recipient_id', $ddl );
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── mirror_insert real roundtrip ────────────────────────────────────────
|
|
||||||
|
|
||||||
public function test_favorite_mirror_insert_writes_real_row(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$adapter = new WPDO_HivePress_Favorites_Adapter();
|
|
||||||
|
|
||||||
$comment = new stdClass();
|
|
||||||
$comment->comment_type = 'hp_favorite';
|
|
||||||
$comment->user_id = 11;
|
|
||||||
$comment->comment_post_ID = 222;
|
|
||||||
$comment->comment_date = '2026-05-04 12:00:00';
|
|
||||||
|
|
||||||
$adapter->mirror_insert( 1001, $comment );
|
|
||||||
|
|
||||||
$rows = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_FAVORITE . '`' );
|
|
||||||
$this->assertSame( 1, $rows );
|
|
||||||
|
|
||||||
$saved = $wpdb->get_row( 'SELECT * FROM `' . self::TABLE_FAVORITE . '` WHERE comment_id = 1001', 'ARRAY_A' );
|
|
||||||
$this->assertSame( '11', (string) $saved['user_id'] );
|
|
||||||
$this->assertSame( '222', (string) $saved['listing_id'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_favorite_unique_constraint_blocks_double_tap(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$adapter = new WPDO_HivePress_Favorites_Adapter();
|
|
||||||
|
|
||||||
// Two different comment_ids but same user+listing — UNIQUE should win.
|
|
||||||
$comment = new stdClass();
|
|
||||||
$comment->comment_type = 'hp_favorite';
|
|
||||||
$comment->user_id = 7;
|
|
||||||
$comment->comment_post_ID = 99;
|
|
||||||
$comment->comment_date = '2026-05-04 12:00:00';
|
|
||||||
|
|
||||||
$adapter->mirror_insert( 2001, $comment );
|
|
||||||
$adapter->mirror_insert( 2002, $comment ); // Race-style duplicate.
|
|
||||||
|
|
||||||
$rows = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_FAVORITE . '` WHERE user_id = 7 AND listing_id = 99' );
|
|
||||||
$this->assertSame( 1, $rows, 'UNIQUE(user_id, listing_id) must reject duplicate.' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_message_mirror_insert_promotes_recipient_id(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$adapter = new WPDO_HivePress_Messages_Adapter();
|
|
||||||
|
|
||||||
$comment = new stdClass();
|
|
||||||
$comment->comment_type = 'hp_message';
|
|
||||||
$comment->user_id = 5; // sender
|
|
||||||
$comment->comment_karma = 42; // recipient (HP hack)
|
|
||||||
$comment->comment_post_ID = 99;
|
|
||||||
$comment->comment_approved = 0;
|
|
||||||
$comment->comment_date = '2026-05-04 12:00:00';
|
|
||||||
|
|
||||||
$adapter->mirror_insert( 3001, $comment );
|
|
||||||
|
|
||||||
$saved = $wpdb->get_row( 'SELECT * FROM `' . self::TABLE_MESSAGE . '` WHERE comment_id = 3001', 'ARRAY_A' );
|
|
||||||
$this->assertNotNull( $saved );
|
|
||||||
$this->assertSame( '42', (string) $saved['recipient_id'] );
|
|
||||||
$this->assertSame( '5', (string) $saved['sender_id'] );
|
|
||||||
|
|
||||||
// recipient_unread index supports the dashboard query.
|
|
||||||
$unread_count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_MESSAGE . '` WHERE recipient_id = 42 AND is_read = 0' );
|
|
||||||
$this->assertSame( 1, $unread_count );
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── FSM lifecycle ───────────────────────────────────────────────────────
|
// ── FSM lifecycle ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
public function test_fsm_lifecycle_progression(): void {
|
public function test_fsm_lifecycle_progression(): void {
|
||||||
@@ -189,9 +59,3 @@ class HivepressIntegrationTest extends TestCase {
|
|||||||
$this->assertSame( 0, $count, 'Optimizer must short-circuit when module is idle.' );
|
$this->assertSame( 0, $count, 'Optimizer must short-circuit when module is idle.' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Production adapters use `$wpdb->prefix . self::TABLE` to compose the full
|
|
||||||
// table name. Integration test bootstrap sets prefix to `wp_itest_`, so the
|
|
||||||
// production mirror_insert() naturally writes to wp_itest_wpdo_comment_hp_*.
|
|
||||||
// No subclassing or test-double needed — the production code path is exercised
|
|
||||||
// directly against real MariaDB with isolated prefix.
|
|
||||||
|
|||||||
@@ -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' );
|
define( 'TMDO_HIVEPRESS_VERSION', '0.1.0' );
|
||||||
|
|
||||||
// ── AddOn classes ──────────────────────────────────────────────────────────
|
// ── 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(
|
$tmdo_hp_files = array(
|
||||||
'includes/class-tmdo-hivepress.php',
|
'includes/class-tmdo-hivepress.php',
|
||||||
@@ -39,7 +37,6 @@ $tmdo_hp_files = array(
|
|||||||
'includes/hivepress/class-tmdo-hivepress-detector.php',
|
'includes/hivepress/class-tmdo-hivepress-detector.php',
|
||||||
'includes/hivepress/class-tmdo-hivepress-conflict-guard.php',
|
'includes/hivepress/class-tmdo-hivepress-conflict-guard.php',
|
||||||
'includes/hivepress/class-tmdo-hivepress-bootstrap.php',
|
'includes/hivepress/class-tmdo-hivepress-bootstrap.php',
|
||||||
'includes/hivepress/class-tmdo-hivepress-comment-router.php',
|
|
||||||
'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php',
|
'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php',
|
||||||
'includes/hivepress/class-tmdo-hivepress-attribute-bridge.php',
|
'includes/hivepress/class-tmdo-hivepress-attribute-bridge.php',
|
||||||
'includes/hivepress/class-tmdo-hivepress-suitability-scorer.php',
|
'includes/hivepress/class-tmdo-hivepress-suitability-scorer.php',
|
||||||
@@ -47,7 +44,7 @@ $tmdo_hp_files = array(
|
|||||||
'includes/hivepress/class-tmdo-hivepress-rest.php',
|
'includes/hivepress/class-tmdo-hivepress-rest.php',
|
||||||
'includes/class-tmdo-hivepress-transient-filter.php',
|
'includes/class-tmdo-hivepress-transient-filter.php',
|
||||||
'includes/class-tmdo-hivepress-term-comment-fields.php',
|
'includes/class-tmdo-hivepress-term-comment-fields.php',
|
||||||
'includes/class-tmdo-hpct-import.php',
|
'includes/class-tmdo-hivepress-entity-groups.php',
|
||||||
'admin/class-tmdo-admin-hivepress.php',
|
'admin/class-tmdo-admin-hivepress.php',
|
||||||
'cli/class-tmdo-cli-hivepress.php',
|
'cli/class-tmdo-cli-hivepress.php',
|
||||||
);
|
);
|
||||||
@@ -55,12 +52,6 @@ $tmdo_hp_files = array(
|
|||||||
foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/hivepress/adapters/*.php' ) ?: array() as $tmdo_hp_glob ) {
|
foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/hivepress/adapters/*.php' ) ?: array() as $tmdo_hp_glob ) {
|
||||||
$tmdo_hp_files[] = 'includes/hivepress/adapters/' . basename( $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 ) {
|
foreach ( $tmdo_hp_files as $tmdo_hp_file ) {
|
||||||
$tmdo_hp_path = TMDO_HIVEPRESS_PATH . $tmdo_hp_file;
|
$tmdo_hp_path = TMDO_HIVEPRESS_PATH . $tmdo_hp_file;
|
||||||
|
|||||||
@@ -33,19 +33,6 @@ class HivePressBookingsAdapterTest extends TestCase {
|
|||||||
$this->assertSame( 'HivePress\\Bookings\\Plugin', $this->adapter->detection_class() );
|
$this->assertSame( 'HivePress\\Bookings\\Plugin', $this->adapter->detection_class() );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_on_register_fields_reserves_booking_hot_columns(): void {
|
|
||||||
$registry = WPDO_Schema_Registry::instance();
|
|
||||||
$this->adapter->on_register_fields( $registry );
|
|
||||||
|
|
||||||
$start = $registry->get_field( 'hp_booking', 'hp_start_time' );
|
|
||||||
$this->assertSame( 'hot', $start['zone'] );
|
|
||||||
$this->assertTrue( (bool) $start['indexed'] );
|
|
||||||
|
|
||||||
$end = $registry->get_field( 'hp_booking', 'hp_end_time' );
|
|
||||||
$this->assertSame( 'hot', $end['zone'] );
|
|
||||||
$this->assertTrue( (bool) $end['indexed'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_score_reflects_unverified_perf_dimension(): void {
|
public function test_score_reflects_unverified_perf_dimension(): void {
|
||||||
$score = $this->adapter->suitability_score();
|
$score = $this->adapter->suitability_score();
|
||||||
// D8 reduced to 0.9 because schema is best-effort until addon installs.
|
// D8 reduced to 0.9 because schema is best-effort until addon installs.
|
||||||
@@ -53,8 +40,4 @@ class HivePressBookingsAdapterTest extends TestCase {
|
|||||||
// Aggregate = (7 × 1.0 + 0.9) / 8 × 10 = 9.875 → 9.88.
|
// Aggregate = (7 × 1.0 + 0.9) / 8 × 10 = 9.875 → 9.88.
|
||||||
$this->assertSame( 9.88, $score['aggregate'] );
|
$this->assertSame( 9.88, $score['aggregate'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_migrations_lists_booking_module(): void {
|
|
||||||
$this->assertSame( array( 'hot_hp_booking' ), $this->adapter->migrations() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for WPDO_HivePress_Comment_Router.
|
|
||||||
*
|
|
||||||
* @covers WPDO_HivePress_Comment_Router
|
|
||||||
*/
|
|
||||||
class HivePressCommentRouterTest extends TestCase {
|
|
||||||
|
|
||||||
use WpdbMockTrait;
|
|
||||||
|
|
||||||
protected function setUp(): void {
|
|
||||||
$this->install_wpdb_mock();
|
|
||||||
WPDO_HivePress_Comment_Router::reset_for_tests();
|
|
||||||
// Default disabled.
|
|
||||||
$GLOBALS['_wp_options'][ WPDO_HivePress_Comment_Router::OPTION_ENABLED ] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_disabled_by_default(): void {
|
|
||||||
$this->assertFalse( WPDO_HivePress_Comment_Router::is_enabled() );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_enabled_when_option_set(): void {
|
|
||||||
$GLOBALS['_wp_options'][ WPDO_HivePress_Comment_Router::OPTION_ENABLED ] = 1;
|
|
||||||
$this->assertTrue( WPDO_HivePress_Comment_Router::is_enabled() );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_register_is_idempotent(): void {
|
|
||||||
$GLOBALS['_wp_options'][ WPDO_HivePress_Comment_Router::OPTION_ENABLED ] = 1;
|
|
||||||
WPDO_HivePress_Comment_Router::register();
|
|
||||||
WPDO_HivePress_Comment_Router::register(); // Second call must be no-op.
|
|
||||||
$this->assertTrue( true ); // No throw = pass
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_rewrite_skips_unknown_comment_type(): void {
|
|
||||||
$query = new stdClass();
|
|
||||||
$query->query_vars = array( 'type' => 'comment' ); // not in shadow map
|
|
||||||
|
|
||||||
$clauses = array( 'join' => '', 'where' => '1=1' );
|
|
||||||
$out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query );
|
|
||||||
|
|
||||||
$this->assertSame( $clauses, $out );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_rewrite_adds_left_join_for_hp_favorite(): void {
|
|
||||||
$query = new stdClass();
|
|
||||||
$query->query_vars = array( 'type' => 'hp_favorite' );
|
|
||||||
|
|
||||||
$clauses = array( 'join' => '', 'where' => '1=1' );
|
|
||||||
$out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query );
|
|
||||||
|
|
||||||
$this->assertStringContainsString( 'LEFT JOIN', $out['join'] );
|
|
||||||
$this->assertStringContainsString( 'wpdo_comment_hp_favorite', $out['join'] );
|
|
||||||
$this->assertStringContainsString( 'AS wpdo_shadow', $out['join'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_rewrite_promotes_recipient_for_hp_message(): void {
|
|
||||||
$query = new stdClass();
|
|
||||||
$query->query_vars = array( 'type' => 'hp_message' );
|
|
||||||
|
|
||||||
$clauses = array(
|
|
||||||
'join' => '',
|
|
||||||
'where' => 'wp_comments.comment_karma = 42 AND comment_approved = 0',
|
|
||||||
);
|
|
||||||
$out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query );
|
|
||||||
|
|
||||||
// recipient lookup should be rewritten to use shadow column.
|
|
||||||
$this->assertStringContainsString( 'wpdo_shadow.recipient_id = 42', $out['where'] );
|
|
||||||
// Other predicates preserved.
|
|
||||||
$this->assertStringContainsString( 'comment_approved = 0', $out['where'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_rewrite_handles_array_type(): void {
|
|
||||||
$query = new stdClass();
|
|
||||||
$query->query_vars = array( 'type' => array( 'hp_review', 'comment' ) );
|
|
||||||
|
|
||||||
$clauses = array( 'join' => '', 'where' => '' );
|
|
||||||
$out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query );
|
|
||||||
|
|
||||||
// First element of type array should match (hp_review).
|
|
||||||
$this->assertStringContainsString( 'wpdo_comment_hp_review', $out['join'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_rewrite_returns_unchanged_when_query_invalid(): void {
|
|
||||||
$clauses = array( 'join' => '', 'where' => '1=1' );
|
|
||||||
$out = WPDO_HivePress_Comment_Router::rewrite( $clauses, 'not an object' );
|
|
||||||
$this->assertSame( $clauses, $out );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -47,6 +47,16 @@ class HivePressConflictGuardTest extends TestCase {
|
|||||||
$this->assertSame( '', $out );
|
$this->assertSame( '', $out );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_guidance_does_not_point_to_a_missing_command(): void {
|
||||||
|
// There is no HPCT importer: `wp wpdo hivepress migrate-from-hpct` never
|
||||||
|
// existed and TMDO_HPCT_Import had no caller once core dropped HPCT.
|
||||||
|
$root = dirname( __DIR__, 2 );
|
||||||
|
foreach ( array( 'includes/hivepress/class-tmdo-hivepress-conflict-guard.php', 'admin/class-tmdo-admin-hivepress.php' ) as $file ) {
|
||||||
|
$this->assertStringNotContainsString( 'migrate-from-hpct', (string) file_get_contents( $root . '/' . $file ), $file );
|
||||||
|
}
|
||||||
|
$this->assertFileDoesNotExist( $root . '/includes/class-tmdo-hpct-import.php' );
|
||||||
|
}
|
||||||
|
|
||||||
public function test_memo_cache_avoids_repeat_probing(): void {
|
public function test_memo_cache_avoids_repeat_probing(): void {
|
||||||
WPDO_HivePress_Conflict_Guard::reset_for_tests();
|
WPDO_HivePress_Conflict_Guard::reset_for_tests();
|
||||||
$first = WPDO_HivePress_Conflict_Guard::detect_conflicts();
|
$first = WPDO_HivePress_Conflict_Guard::detect_conflicts();
|
||||||
|
|||||||
@@ -46,29 +46,10 @@ class HivePressCoreAdapterTest extends TestCase {
|
|||||||
$this->assertSame( '1.7.0', $this->adapter->minimum_addon_version() );
|
$this->assertSame( '1.7.0', $this->adapter->minimum_addon_version() );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_migrations_returns_listing_and_vendor_modules(): void {
|
|
||||||
$mods = $this->adapter->migrations();
|
|
||||||
$this->assertContains( 'hot_hp_listing', $mods );
|
|
||||||
$this->assertContains( 'hot_hp_vendor', $mods );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_on_register_fields_populates_schema_registry(): void {
|
public function test_on_register_fields_populates_schema_registry(): void {
|
||||||
$registry = WPDO_Schema_Registry::instance();
|
$registry = WPDO_Schema_Registry::instance();
|
||||||
$this->adapter->on_register_fields( $registry );
|
$this->adapter->on_register_fields( $registry );
|
||||||
|
|
||||||
// Listing hot fields.
|
|
||||||
$drafted = $registry->get_field( 'hp_listing', 'hp_drafted' );
|
|
||||||
$this->assertIsArray( $drafted );
|
|
||||||
$this->assertSame( 'hot', $drafted['zone'] );
|
|
||||||
$this->assertTrue( (bool) $drafted['indexed'] );
|
|
||||||
|
|
||||||
$expired = $registry->get_field( 'hp_listing', 'hp_expired_time' );
|
|
||||||
$this->assertSame( 'hot', $expired['zone'] );
|
|
||||||
$this->assertStringContainsString( 'bigint', $expired['data_type'] );
|
|
||||||
|
|
||||||
$featured_time = $registry->get_field( 'hp_listing', 'hp_featured_time' );
|
|
||||||
$this->assertSame( 'hot', $featured_time['zone'] );
|
|
||||||
|
|
||||||
// Vendor cold blob.
|
// Vendor cold blob.
|
||||||
$vendor_image = $registry->get_field( 'hp_vendor', 'hp_image' );
|
$vendor_image = $registry->get_field( 'hp_vendor', 'hp_image' );
|
||||||
$this->assertIsArray( $vendor_image );
|
$this->assertIsArray( $vendor_image );
|
||||||
@@ -87,16 +68,6 @@ class HivePressCoreAdapterTest extends TestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_doctor_check_reports_table_status(): void {
|
|
||||||
$result = $this->adapter->doctor_check();
|
|
||||||
$this->assertIsArray( $result );
|
|
||||||
$this->assertArrayHasKey( 'ok', $result );
|
|
||||||
$this->assertArrayHasKey( 'message', $result );
|
|
||||||
$this->assertArrayHasKey( 'details', $result );
|
|
||||||
// The stub $wpdb returns null for SHOW TABLES, so tables appear missing → ok=false.
|
|
||||||
$this->assertFalse( $result['ok'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_optimize_query_extracts_hot_clauses(): void {
|
public function test_optimize_query_extracts_hot_clauses(): void {
|
||||||
$registry = WPDO_Schema_Registry::instance();
|
$registry = WPDO_Schema_Registry::instance();
|
||||||
$this->adapter->on_register_fields( $registry );
|
$this->adapter->on_register_fields( $registry );
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ use PHPUnit\Framework\TestCase;
|
|||||||
/**
|
/**
|
||||||
* Tests for WPDO_HivePress_Favorites_Adapter.
|
* Tests for WPDO_HivePress_Favorites_Adapter.
|
||||||
*
|
*
|
||||||
|
* The adapter no longer mirrors favorites into a shadow table: that table was
|
||||||
|
* never created, so every mirror write failed.
|
||||||
|
*
|
||||||
* @covers WPDO_HivePress_Favorites_Adapter
|
* @covers WPDO_HivePress_Favorites_Adapter
|
||||||
*/
|
*/
|
||||||
class HivePressFavoritesAdapterTest extends TestCase {
|
class HivePressFavoritesAdapterTest extends TestCase {
|
||||||
@@ -32,72 +35,22 @@ class HivePressFavoritesAdapterTest extends TestCase {
|
|||||||
|
|
||||||
public function test_constants_exposed(): void {
|
public function test_constants_exposed(): void {
|
||||||
$this->assertSame( 'hp_favorite', WPDO_HivePress_Favorites_Adapter::COMMENT_TYPE );
|
$this->assertSame( 'hp_favorite', WPDO_HivePress_Favorites_Adapter::COMMENT_TYPE );
|
||||||
$this->assertSame( 'wpdo_comment_hp_favorite', WPDO_HivePress_Favorites_Adapter::TABLE );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_on_register_custom_tables_declares_shadow_table(): void {
|
public function test_registers_no_shadow_table(): void {
|
||||||
$registry = WPDO_Custom_Table_Registry::instance();
|
$registry = WPDO_Custom_Table_Registry::instance();
|
||||||
$this->adapter->on_register_custom_tables( $registry );
|
$this->adapter->on_register_custom_tables( $registry );
|
||||||
|
|
||||||
$tables = $registry->all();
|
$this->assertSame( array(), $registry->all() );
|
||||||
$this->assertCount( 1, $tables );
|
|
||||||
|
|
||||||
$cfg = reset( $tables );
|
|
||||||
$this->assertSame( 'hivepress-favorites', $cfg['provider'] );
|
|
||||||
$this->assertSame( 'wpdo_comment_hp_favorite', $cfg['table_name'] );
|
|
||||||
$this->assertSame( 'comment_id', $cfg['primary_key'] );
|
|
||||||
$this->assertArrayHasKey( 'unique_user_listing', $cfg['indexes'] );
|
|
||||||
$this->assertStringContainsString( 'UNIQUE', $cfg['indexes']['unique_user_listing'] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_mirror_insert_skips_non_favorite_comments(): void {
|
public function test_binds_no_comment_hooks(): void {
|
||||||
// Stub $wpdb tracks queries.
|
$GLOBALS['_wp_filter_callbacks'] = array();
|
||||||
global $wpdb;
|
|
||||||
$wpdb->queries = array();
|
|
||||||
|
|
||||||
$comment = new stdClass();
|
$this->adapter->on_register_event_hooks();
|
||||||
$comment->comment_type = 'comment'; // ← not hp_favorite
|
|
||||||
$comment->user_id = 1;
|
|
||||||
$comment->comment_post_ID = 99;
|
|
||||||
|
|
||||||
$this->adapter->mirror_insert( 123, $comment );
|
$this->assertArrayNotHasKey( 'wp_insert_comment', $GLOBALS['_wp_filter_callbacks'] );
|
||||||
$this->assertSame( array(), $wpdb->queries );
|
$this->assertArrayNotHasKey( 'deleted_comment', $GLOBALS['_wp_filter_callbacks'] );
|
||||||
}
|
|
||||||
|
|
||||||
public function test_mirror_insert_writes_for_hp_favorite(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$wpdb->queries = array();
|
|
||||||
|
|
||||||
$comment = new stdClass();
|
|
||||||
$comment->comment_type = 'hp_favorite';
|
|
||||||
$comment->user_id = 7;
|
|
||||||
$comment->comment_post_ID = 42;
|
|
||||||
$comment->comment_date = '2026-05-04 12:00:00';
|
|
||||||
|
|
||||||
$this->adapter->mirror_insert( 100, $comment );
|
|
||||||
|
|
||||||
$this->assertCount( 1, $wpdb->queries );
|
|
||||||
$this->assertStringContainsString( 'INSERT IGNORE', $wpdb->queries[0] );
|
|
||||||
$this->assertStringContainsString( 'wpdo_comment_hp_favorite', $wpdb->queries[0] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_mirror_insert_skips_when_user_or_listing_missing(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$wpdb->queries = array();
|
|
||||||
|
|
||||||
$comment = new stdClass();
|
|
||||||
$comment->comment_type = 'hp_favorite';
|
|
||||||
$comment->user_id = 0;
|
|
||||||
$comment->comment_post_ID = 99;
|
|
||||||
|
|
||||||
$this->adapter->mirror_insert( 100, $comment );
|
|
||||||
$this->assertSame( array(), $wpdb->queries );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_doctor_check_reports_missing_table_in_stub_env(): void {
|
|
||||||
$result = $this->adapter->doctor_check();
|
|
||||||
$this->assertFalse( $result['ok'] );
|
|
||||||
$this->assertStringContainsString( 'wpdo_comment_hp_favorite', $result['message'] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_score_aggregates_to_perfect_ten(): void {
|
public function test_score_aggregates_to_perfect_ten(): void {
|
||||||
@@ -105,7 +58,7 @@ class HivePressFavoritesAdapterTest extends TestCase {
|
|||||||
$this->assertSame( 10.0, $score['aggregate'] );
|
$this->assertSame( 10.0, $score['aggregate'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_migrations_lists_comment_module(): void {
|
public function test_migrations_is_empty(): void {
|
||||||
$this->assertSame( array( 'comment_hp_favorite' ), $this->adapter->migrations() );
|
$this->assertSame( array(), $this->adapter->migrations() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AddOn owns the HivePress entity groups core used to register.
|
||||||
|
*
|
||||||
|
* Their definitions are persisted (wpdo_registry_meta schema hash) and the
|
||||||
|
* flat tables on live sites were built from them, so they are pinned to the
|
||||||
|
* persisted hashes: any drift would ALTER flat tables that may hold
|
||||||
|
* aeav_only data. (Values recorded on dev20, 2026-09-25.)
|
||||||
|
*/
|
||||||
|
class HivePressEntityGroupsTest extends TestCase {
|
||||||
|
|
||||||
|
private const PERSISTED_HASHES = array(
|
||||||
|
'post/hp_listing_core' => 'fb5ab760',
|
||||||
|
'post/hp_request_core' => '27d6f4f5',
|
||||||
|
'post/hp_vendor_core' => '5366e5c8',
|
||||||
|
'user/hp_user' => '61ac526f',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookups that do not pass a post type (or run with scoped routing off,
|
||||||
|
* until a site's rows are reconciled) keep the legacy owner of keys shared
|
||||||
|
* by several post groups: the LAST group registered. Pinned so the
|
||||||
|
* registration order cannot change silently.
|
||||||
|
*/
|
||||||
|
private const SHARED_KEY_OWNERS = array(
|
||||||
|
'hp_status' => 'hp_request_core',
|
||||||
|
'hp_expired_time' => 'hp_request_core',
|
||||||
|
'hp_view_count' => 'hp_request_core',
|
||||||
|
'hp_verified' => 'hp_vendor_core',
|
||||||
|
'hp_rating' => 'hp_vendor_core',
|
||||||
|
'hp_rating_count' => 'hp_vendor_core',
|
||||||
|
'hp_hourly_rate' => 'hp_vendor_core',
|
||||||
|
'hp_user' => 'hp_vendor_core',
|
||||||
|
);
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
TMDO_Entity_Registry::init();
|
||||||
|
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||||
|
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||||
|
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void {
|
||||||
|
unset( $GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] );
|
||||||
|
unset( $GLOBALS['_wp_filter_callbacks']['tmdo_rest_default_post_type'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_definitions_match_persisted_schema_hashes(): void {
|
||||||
|
foreach ( self::PERSISTED_HASHES as $id => $hash ) {
|
||||||
|
list( $type, $group ) = explode( '/', $id );
|
||||||
|
$fields = TMDO_Entity_Registry::get_group_fields( $type, $group );
|
||||||
|
$this->assertNotEmpty( $fields, "{$id} not registered" );
|
||||||
|
$this->assertSame( $hash, substr( TMDO_Schema_Manager::calculate_schema_hash( $fields ), 0, 8 ), "{$id} drifted from the persisted definition" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_shared_keys_keep_their_current_owner(): void {
|
||||||
|
foreach ( self::SHARED_KEY_OWNERS as $key => $group ) {
|
||||||
|
$this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key )['group'], $key );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_shared_keys_follow_the_post_type_when_scoped_routing_is_on(): void {
|
||||||
|
$GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] = '1';
|
||||||
|
|
||||||
|
foreach ( array( 'hp_listing' => 'hp_listing_core', 'hp_request' => 'hp_request_core', 'hp_vendor' => 'hp_vendor_core' ) as $post_type => $group ) {
|
||||||
|
foreach ( TMDO_Entity_Registry::get_group_keys( 'post', $group ) as $key ) {
|
||||||
|
$this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key, $post_type )['group'], "{$post_type}:{$key}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertNull( TMDO_Entity_Registry::get_field( 'post', 'hp_rating', 'post' ), 'HivePress keys stay native on other post types' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields only the Hot zone used to cover (ADR-003), as their own groups.
|
||||||
|
*/
|
||||||
|
private const HOT_ONLY_GROUPS = array(
|
||||||
|
'hp_draft_flags' => array( array( 'hp_listing', 'hp_request' ), array( 'hp_drafted' ) ),
|
||||||
|
'hp_booking_core' => array( array( 'hp_booking' ), array( 'hp_start_time', 'hp_end_time', 'hp_quantity' ) ),
|
||||||
|
'hp_membership_plan_core' => array( array( 'hp_membership_plan' ), array( 'hp_expire_period', 'hp_primary' ) ),
|
||||||
|
'hp_membership_core' => array( array( 'hp_membership' ), array( 'hp_expired_time' ) ),
|
||||||
|
);
|
||||||
|
|
||||||
|
public function test_hot_only_fields_have_entity_groups_scoped_to_their_post_types(): void {
|
||||||
|
foreach ( self::HOT_ONLY_GROUPS as $group => list( $post_types, $keys ) ) {
|
||||||
|
$this->assertSame( $post_types, TMDO_Entity_Registry::get_group_post_types( 'post', $group ), $group );
|
||||||
|
$this->assertSame( $keys, TMDO_Entity_Registry::get_group_keys( 'post', $group ), $group );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_hot_only_fields_route_to_their_group_when_scoped_routing_is_on(): void {
|
||||||
|
$GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] = '1';
|
||||||
|
|
||||||
|
foreach ( self::HOT_ONLY_GROUPS as $group => list( $post_types, $keys ) ) {
|
||||||
|
foreach ( $post_types as $post_type ) {
|
||||||
|
foreach ( $keys as $key ) {
|
||||||
|
$this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key, $post_type )['group'], "{$post_type}:{$key}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertSame( 'hp_listing_core', TMDO_Entity_Registry::get_field( 'post', 'hp_expired_time', 'hp_listing' )['group'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_hot_only_groups_add_no_collisions(): void {
|
||||||
|
$this->assertSame( array(), TMDO_Entity_Registry::get_collisions() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reconciler_plans_the_misrouted_hivepress_keys(): void {
|
||||||
|
$moves = array();
|
||||||
|
foreach ( TMDO_Post_Group_Reconciler::plan() as $m ) {
|
||||||
|
$moves[] = "{$m['source']}>{$m['target']}:{$m['key']}";
|
||||||
|
}
|
||||||
|
sort( $moves );
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
array(
|
||||||
|
'hp_request_core>hp_listing_core:hp_expired_time',
|
||||||
|
'hp_request_core>hp_listing_core:hp_status',
|
||||||
|
'hp_request_core>hp_listing_core:hp_view_count',
|
||||||
|
'hp_request_core>hp_membership_core:hp_expired_time',
|
||||||
|
'hp_vendor_core>hp_listing_core:hp_hourly_rate',
|
||||||
|
'hp_vendor_core>hp_listing_core:hp_rating',
|
||||||
|
'hp_vendor_core>hp_listing_core:hp_rating_count',
|
||||||
|
'hp_vendor_core>hp_listing_core:hp_verified',
|
||||||
|
'hp_vendor_core>hp_request_core:hp_user',
|
||||||
|
),
|
||||||
|
$moves
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_rest_listings_default_to_hivepress_listings(): void {
|
||||||
|
TMDO_HivePress_Entity_Groups::register();
|
||||||
|
|
||||||
|
$this->assertSame( 'hp_listing', TMDO_REST_API::default_post_type() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_core_adapter_does_not_register_a_conflicting_group(): void {
|
||||||
|
( new TMDO_HivePress_Core_Adapter() )->on_register_entity_fields();
|
||||||
|
|
||||||
|
$this->assertSame( array(), TMDO_Entity_Registry::get_collisions() );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,19 +33,6 @@ class HivePressMarketplaceAdapterTest extends TestCase {
|
|||||||
$this->assertSame( 'HivePress\\Marketplace\\Plugin', $this->adapter->detection_class() );
|
$this->assertSame( 'HivePress\\Marketplace\\Plugin', $this->adapter->detection_class() );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_on_register_fields_registers_hot_price_and_warm_counter(): void {
|
|
||||||
$registry = WPDO_Schema_Registry::instance();
|
|
||||||
$this->adapter->on_register_fields( $registry );
|
|
||||||
|
|
||||||
$price = $registry->get_field( 'hp_listing', 'hp_purchase_price' );
|
|
||||||
$this->assertSame( 'hot', $price['zone'] );
|
|
||||||
$this->assertTrue( (bool) $price['indexed'] );
|
|
||||||
$this->assertStringContainsString( 'decimal', $price['data_type'] );
|
|
||||||
|
|
||||||
$count = $registry->get_field( 'hp_listing', 'hp_purchase_count' );
|
|
||||||
$this->assertSame( 'warm', $count['zone'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_score_aggregates_to_nine_eight_eight(): void {
|
public function test_score_aggregates_to_nine_eight_eight(): void {
|
||||||
// D8 = 0.9 → aggregate = (7×1.0 + 0.9)/8 × 10 = 9.875 → 9.88.
|
// D8 = 0.9 → aggregate = (7×1.0 + 0.9)/8 × 10 = 9.875 → 9.88.
|
||||||
$this->assertSame( 9.88, $this->adapter->suitability_score()['aggregate'] );
|
$this->assertSame( 9.88, $this->adapter->suitability_score()['aggregate'] );
|
||||||
|
|||||||
@@ -33,41 +33,8 @@ class HivePressMembershipsAdapterTest extends TestCase {
|
|||||||
$this->assertSame( '2.0.0', $this->adapter->minimum_addon_version() );
|
$this->assertSame( '2.0.0', $this->adapter->minimum_addon_version() );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_on_register_fields_registers_three_hot_fields(): void {
|
|
||||||
$registry = WPDO_Schema_Registry::instance();
|
|
||||||
$this->adapter->on_register_fields( $registry );
|
|
||||||
|
|
||||||
// Plan fields.
|
|
||||||
$expire_period = $registry->get_field( 'hp_membership_plan', 'hp_expire_period' );
|
|
||||||
$this->assertSame( 'hot', $expire_period['zone'] );
|
|
||||||
$this->assertStringContainsString( 'int', $expire_period['data_type'] );
|
|
||||||
|
|
||||||
$primary = $registry->get_field( 'hp_membership_plan', 'hp_primary' );
|
|
||||||
$this->assertSame( 'hot', $primary['zone'] );
|
|
||||||
$this->assertTrue( (bool) $primary['indexed'] );
|
|
||||||
|
|
||||||
// Membership field — the hottest path.
|
|
||||||
$expired = $registry->get_field( 'hp_membership', 'hp_expired_time' );
|
|
||||||
$this->assertSame( 'hot', $expired['zone'] );
|
|
||||||
$this->assertTrue( (bool) $expired['indexed'] );
|
|
||||||
$this->assertStringContainsString( 'bigint', $expired['data_type'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_doctor_check_reports_missing_tables_in_stub_env(): void {
|
|
||||||
$result = $this->adapter->doctor_check();
|
|
||||||
$this->assertFalse( $result['ok'] );
|
|
||||||
$this->assertArrayHasKey( 'hp_membership_plan', $result['details'] );
|
|
||||||
$this->assertArrayHasKey( 'hp_membership', $result['details'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_score_aggregates_to_perfect_ten(): void {
|
public function test_score_aggregates_to_perfect_ten(): void {
|
||||||
$score = $this->adapter->suitability_score();
|
$score = $this->adapter->suitability_score();
|
||||||
$this->assertSame( 10.0, $score['aggregate'] );
|
$this->assertSame( 10.0, $score['aggregate'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_migrations_lists_both_hot_modules(): void {
|
|
||||||
$mods = $this->adapter->migrations();
|
|
||||||
$this->assertContains( 'hot_hp_membership_plan', $mods );
|
|
||||||
$this->assertContains( 'hot_hp_membership', $mods );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ use PHPUnit\Framework\TestCase;
|
|||||||
/**
|
/**
|
||||||
* Tests for WPDO_HivePress_Messages_Adapter.
|
* Tests for WPDO_HivePress_Messages_Adapter.
|
||||||
*
|
*
|
||||||
|
* The adapter no longer mirrors messages into a shadow table: that table was
|
||||||
|
* never created, so every mirror write failed.
|
||||||
|
*
|
||||||
* @covers WPDO_HivePress_Messages_Adapter
|
* @covers WPDO_HivePress_Messages_Adapter
|
||||||
*/
|
*/
|
||||||
class HivePressMessagesAdapterTest extends TestCase {
|
class HivePressMessagesAdapterTest extends TestCase {
|
||||||
@@ -28,77 +31,26 @@ class HivePressMessagesAdapterTest extends TestCase {
|
|||||||
$this->assertSame( 'hivepress-messages', $this->adapter->plugin_slug() );
|
$this->assertSame( 'hivepress-messages', $this->adapter->plugin_slug() );
|
||||||
$this->assertSame( 'HivePress\\Messages\\Plugin', $this->adapter->detection_class() );
|
$this->assertSame( 'HivePress\\Messages\\Plugin', $this->adapter->detection_class() );
|
||||||
$this->assertSame( 'hp_message', WPDO_HivePress_Messages_Adapter::COMMENT_TYPE );
|
$this->assertSame( 'hp_message', WPDO_HivePress_Messages_Adapter::COMMENT_TYPE );
|
||||||
$this->assertSame( 'wpdo_comment_hp_message', WPDO_HivePress_Messages_Adapter::TABLE );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_on_register_custom_tables_declares_indexed_recipient(): void {
|
public function test_registers_no_shadow_table(): void {
|
||||||
$registry = WPDO_Custom_Table_Registry::instance();
|
$registry = WPDO_Custom_Table_Registry::instance();
|
||||||
$this->adapter->on_register_custom_tables( $registry );
|
$this->adapter->on_register_custom_tables( $registry );
|
||||||
|
|
||||||
$tables = $registry->all();
|
$this->assertSame( array(), $registry->all() );
|
||||||
$cfg = reset( $tables );
|
|
||||||
$this->assertSame( 'wpdo_comment_hp_message', $cfg['table_name'] );
|
|
||||||
$this->assertArrayHasKey( 'recipient_id', $cfg['expected_columns'] );
|
|
||||||
$this->assertArrayHasKey( 'idx_recipient_unread', $cfg['indexes'] );
|
|
||||||
$this->assertStringContainsString( 'recipient_id, is_read', $cfg['indexes']['idx_recipient_unread'] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_mirror_insert_skips_non_message_comments(): void {
|
public function test_binds_no_comment_hooks(): void {
|
||||||
global $wpdb;
|
$GLOBALS['_wp_filter_callbacks'] = array();
|
||||||
$wpdb->queries = array();
|
|
||||||
|
|
||||||
$comment = new stdClass();
|
$this->adapter->on_register_event_hooks();
|
||||||
$comment->comment_type = 'comment';
|
|
||||||
$comment->user_id = 1;
|
|
||||||
$comment->comment_karma = 5;
|
|
||||||
|
|
||||||
$this->adapter->mirror_insert( 1, $comment );
|
$this->assertArrayNotHasKey( 'wp_insert_comment', $GLOBALS['_wp_filter_callbacks'] );
|
||||||
$this->assertSame( array(), $wpdb->queries );
|
$this->assertArrayNotHasKey( 'wp_set_comment_status', $GLOBALS['_wp_filter_callbacks'] );
|
||||||
|
$this->assertArrayNotHasKey( 'deleted_comment', $GLOBALS['_wp_filter_callbacks'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_mirror_insert_promotes_recipient_from_comment_karma(): void {
|
public function test_migrations_is_empty(): void {
|
||||||
global $wpdb;
|
$this->assertSame( array(), $this->adapter->migrations() );
|
||||||
$wpdb->queries = array();
|
|
||||||
|
|
||||||
$comment = new stdClass();
|
|
||||||
$comment->comment_type = 'hp_message';
|
|
||||||
$comment->user_id = 7; // sender
|
|
||||||
$comment->comment_karma = 42; // ← recipient hack lives here
|
|
||||||
$comment->comment_post_ID = 99;
|
|
||||||
$comment->comment_approved = 0;
|
|
||||||
$comment->comment_date = '2026-05-04 12:00:00';
|
|
||||||
|
|
||||||
$this->adapter->mirror_insert( 100, $comment );
|
|
||||||
|
|
||||||
$this->assertCount( 1, $wpdb->queries );
|
|
||||||
$sql = $wpdb->queries[0];
|
|
||||||
$this->assertStringContainsString( 'INSERT INTO', $sql );
|
|
||||||
$this->assertStringContainsString( 'wpdo_comment_hp_message', $sql );
|
|
||||||
// The promoted recipient_id should appear in the bound SQL.
|
|
||||||
$this->assertStringContainsString( '42', $sql );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_mirror_insert_skips_when_sender_or_recipient_zero(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$wpdb->queries = array();
|
|
||||||
|
|
||||||
$comment = new stdClass();
|
|
||||||
$comment->comment_type = 'hp_message';
|
|
||||||
$comment->user_id = 0; // bad sender
|
|
||||||
$comment->comment_karma = 42;
|
|
||||||
$comment->comment_post_ID = 99;
|
|
||||||
|
|
||||||
$this->adapter->mirror_insert( 100, $comment );
|
|
||||||
$this->assertSame( array(), $wpdb->queries );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_doctor_check_reports_missing_table_in_stub_env(): void {
|
|
||||||
$result = $this->adapter->doctor_check();
|
|
||||||
$this->assertFalse( $result['ok'] );
|
|
||||||
$this->assertStringContainsString( 'wpdo_comment_hp_message', $result['message'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_score_aggregates_to_perfect_ten(): void {
|
|
||||||
$this->assertSame( 10.0, $this->adapter->suitability_score()['aggregate'] );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ADR-003: post fields converge on entity groups; the Hot zone is frozen.
|
||||||
|
*
|
||||||
|
* No HivePress adapter registers zone=hot fields or contributes hot_* FSM
|
||||||
|
* modules. The fields they used to cover live in TMDO_HivePress_Entity_Groups.
|
||||||
|
*/
|
||||||
|
class NoHotZoneRegistrationTest extends TestCase {
|
||||||
|
|
||||||
|
private const ADAPTERS = array(
|
||||||
|
'TMDO_HivePress_Core_Adapter',
|
||||||
|
'TMDO_HivePress_Requests_Adapter',
|
||||||
|
'TMDO_HivePress_Bookings_Adapter',
|
||||||
|
'TMDO_HivePress_Memberships_Adapter',
|
||||||
|
'TMDO_HivePress_Marketplace_Adapter',
|
||||||
|
'TMDO_HivePress_Favorites_Adapter',
|
||||||
|
'TMDO_HivePress_Reviews_Adapter',
|
||||||
|
'TMDO_HivePress_Social_Links_Adapter',
|
||||||
|
'TMDO_HivePress_Blocks_Adapter',
|
||||||
|
'TMDO_HivePress_Seo_Adapter',
|
||||||
|
'TMDO_HivePress_Messages_Adapter',
|
||||||
|
'TMDO_HivePress_Statistics_Adapter',
|
||||||
|
'TMDO_HivePress_Tags_Adapter',
|
||||||
|
);
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
$prop = ( new ReflectionClass( TMDO_Schema_Registry::class ) )->getProperty( 'instance' );
|
||||||
|
$prop->setAccessible( true );
|
||||||
|
$prop->setValue( null, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_no_adapter_registers_hot_fields(): void {
|
||||||
|
$registry = TMDO_Schema_Registry::instance();
|
||||||
|
foreach ( self::ADAPTERS as $class ) {
|
||||||
|
( new $class() )->on_register_fields( $registry );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertSame( array(), $registry->get_zone_fields( 'hot' ) );
|
||||||
|
$this->assertSame( 'cold', $registry->get_field_zone( 'hp_listing', 'hp_images' ), 'cold fields stay' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_no_adapter_contributes_hot_modules(): void {
|
||||||
|
foreach ( self::ADAPTERS as $class ) {
|
||||||
|
$this->assertSame( array(), ( new $class() )->migrations(), $class );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,67 +35,25 @@ class HivePressRequestsAdapterTest extends TestCase {
|
|||||||
$this->assertSame( 'hp_offer', WPDO_HivePress_Requests_Adapter::OFFER_COMMENT_TYPE );
|
$this->assertSame( 'hp_offer', WPDO_HivePress_Requests_Adapter::OFFER_COMMENT_TYPE );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_on_register_fields_registers_request_hot_columns(): void {
|
public function test_registers_no_offer_shadow_table(): void {
|
||||||
$registry = WPDO_Schema_Registry::instance();
|
// The offer shadow table was never created, so every mirror write failed.
|
||||||
$this->adapter->on_register_fields( $registry );
|
|
||||||
|
|
||||||
$drafted = $registry->get_field( 'hp_request', 'hp_drafted' );
|
|
||||||
$this->assertSame( 'hot', $drafted['zone'] );
|
|
||||||
$this->assertTrue( (bool) $drafted['indexed'] );
|
|
||||||
|
|
||||||
$expired = $registry->get_field( 'hp_request', 'hp_expired_time' );
|
|
||||||
$this->assertSame( 'hot', $expired['zone'] );
|
|
||||||
$this->assertTrue( (bool) $expired['indexed'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_on_register_custom_tables_declares_offer_shadow(): void {
|
|
||||||
$registry = WPDO_Custom_Table_Registry::instance();
|
$registry = WPDO_Custom_Table_Registry::instance();
|
||||||
$this->adapter->on_register_custom_tables( $registry );
|
$this->adapter->on_register_custom_tables( $registry );
|
||||||
|
|
||||||
$tables = $registry->all();
|
$this->assertSame( array(), $registry->all() );
|
||||||
$cfg = reset( $tables );
|
|
||||||
$this->assertSame( 'wpdo_comment_hp_offer', $cfg['table_name'] );
|
|
||||||
$this->assertSame( 'hp_request', $cfg['post_type_link'] );
|
|
||||||
$this->assertArrayHasKey( 'idx_request_approved', $cfg['indexes'] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_mirror_offer_insert_skips_other_comment_types(): void {
|
public function test_binds_no_comment_hooks(): void {
|
||||||
global $wpdb;
|
$GLOBALS['_wp_filter_callbacks'] = array();
|
||||||
$wpdb->queries = array();
|
|
||||||
|
|
||||||
$comment = new stdClass();
|
$this->adapter->on_register_event_hooks();
|
||||||
$comment->comment_type = 'comment'; // ← not hp_offer
|
|
||||||
$comment->comment_post_ID = 99;
|
|
||||||
$comment->user_id = 1;
|
|
||||||
|
|
||||||
$this->adapter->mirror_offer_insert( 1, $comment );
|
$this->assertArrayNotHasKey( 'wp_insert_comment', $GLOBALS['_wp_filter_callbacks'] );
|
||||||
$this->assertSame( array(), $wpdb->queries );
|
$this->assertArrayNotHasKey( 'edit_comment', $GLOBALS['_wp_filter_callbacks'] );
|
||||||
}
|
$this->assertArrayNotHasKey( 'deleted_comment', $GLOBALS['_wp_filter_callbacks'] );
|
||||||
|
|
||||||
public function test_mirror_offer_insert_writes_for_hp_offer(): void {
|
|
||||||
global $wpdb;
|
|
||||||
$wpdb->queries = array();
|
|
||||||
|
|
||||||
$comment = new stdClass();
|
|
||||||
$comment->comment_type = 'hp_offer';
|
|
||||||
$comment->comment_post_ID = 42;
|
|
||||||
$comment->user_id = 7;
|
|
||||||
$comment->comment_approved = 0;
|
|
||||||
$comment->comment_date = '2026-05-04 12:00:00';
|
|
||||||
|
|
||||||
$this->adapter->mirror_offer_insert( 100, $comment );
|
|
||||||
$this->assertCount( 1, $wpdb->queries );
|
|
||||||
$this->assertStringContainsString( 'INSERT INTO', $wpdb->queries[0] );
|
|
||||||
$this->assertStringContainsString( 'wpdo_comment_hp_offer', $wpdb->queries[0] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_score_aggregates_to_perfect_ten(): void {
|
public function test_score_aggregates_to_perfect_ten(): void {
|
||||||
$this->assertSame( 10.0, $this->adapter->suitability_score()['aggregate'] );
|
$this->assertSame( 10.0, $this->adapter->suitability_score()['aggregate'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_migrations_lists_request_and_offer_modules(): void {
|
|
||||||
$mods = $this->adapter->migrations();
|
|
||||||
$this->assertContains( 'hot_hp_request', $mods );
|
|
||||||
$this->assertContains( 'comment_hp_offer', $mods );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ class HivePressSocialLinksAdapterTest extends TestCase {
|
|||||||
$prop->setAccessible( true );
|
$prop->setAccessible( true );
|
||||||
$prop->setValue( null, null );
|
$prop->setValue( null, null );
|
||||||
|
|
||||||
|
unset( $GLOBALS['_wp_options']['hp_vendor_social_links'], $GLOBALS['_wp_options']['hp_listing_social_links'] );
|
||||||
|
|
||||||
$this->adapter = new WPDO_HivePress_Social_Links_Adapter();
|
$this->adapter = new WPDO_HivePress_Social_Links_Adapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,25 +32,39 @@ class HivePressSocialLinksAdapterTest extends TestCase {
|
|||||||
$this->assertSame( 'HivePress\\SocialLinks\\Plugin', $this->adapter->detection_class() );
|
$this->assertSame( 'HivePress\\SocialLinks\\Plugin', $this->adapter->detection_class() );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_on_register_fields_registers_seven_cold_fields(): void {
|
public function test_registers_enabled_links_per_model(): void {
|
||||||
|
// hivepress-social-links stores each enabled link as the attribute hp_{name},
|
||||||
|
// with separate admin selections for listings and vendors.
|
||||||
|
update_option( 'hp_vendor_social_links', array( 'facebook', 'website' ) );
|
||||||
|
update_option( 'hp_listing_social_links', array( 'instagram' ) );
|
||||||
|
|
||||||
$registry = WPDO_Schema_Registry::instance();
|
$registry = WPDO_Schema_Registry::instance();
|
||||||
$this->adapter->on_register_fields( $registry );
|
$this->adapter->on_register_fields( $registry );
|
||||||
|
|
||||||
// All seven social URL fields should land in cold zone on hp_vendor.
|
$this->assertSame( 'cold', $registry->get_field( 'hp_vendor', 'hp_facebook' )['zone'] );
|
||||||
$facebook = $registry->get_field( 'hp_vendor', 'hp_facebook_url' );
|
$this->assertSame( 'cold', $registry->get_field( 'hp_vendor', 'hp_website' )['zone'] );
|
||||||
$this->assertSame( 'cold', $facebook['zone'] );
|
$this->assertSame( 'cold', $registry->get_field( 'hp_listing', 'hp_instagram' )['zone'] );
|
||||||
|
|
||||||
$instagram = $registry->get_field( 'hp_vendor', 'hp_instagram_url' );
|
$this->assertNull( $registry->get_field( 'hp_vendor', 'hp_instagram' ) );
|
||||||
$this->assertSame( 'cold', $instagram['zone'] );
|
$this->assertNull( $registry->get_field( 'hp_vendor', 'hp_facebook_url' ) );
|
||||||
|
|
||||||
$whatsapp = $registry->get_field( 'hp_vendor', 'hp_whatsapp_url' );
|
|
||||||
$this->assertSame( 'cold', $whatsapp['zone'] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_doctor_check_reports_field_count(): void {
|
public function test_registers_nothing_when_no_links_enabled(): void {
|
||||||
|
$registry = WPDO_Schema_Registry::instance();
|
||||||
|
$this->adapter->on_register_fields( $registry );
|
||||||
|
|
||||||
|
$this->assertNull( $registry->get_field( 'hp_vendor', 'hp_facebook' ) );
|
||||||
|
$this->assertNull( $registry->get_field( 'hp_listing', 'hp_facebook' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_doctor_check_reports_registered_count(): void {
|
||||||
|
update_option( 'hp_vendor_social_links', array( 'facebook', 'website' ) );
|
||||||
|
update_option( 'hp_listing_social_links', array( 'instagram' ) );
|
||||||
|
|
||||||
$result = $this->adapter->doctor_check();
|
$result = $this->adapter->doctor_check();
|
||||||
|
|
||||||
$this->assertTrue( $result['ok'] );
|
$this->assertTrue( $result['ok'] );
|
||||||
$this->assertStringContainsString( '7 cold fields', $result['message'] );
|
$this->assertStringContainsString( '3 cold fields', $result['message'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_score_aggregates_to_perfect_ten(): void {
|
public function test_score_aggregates_to_perfect_ten(): void {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ declare(strict_types=1);
|
|||||||
* Several existing wpdo tests (ZoneHotTest, SyncBridgeTest, etc.) replace the
|
* Several existing wpdo tests (ZoneHotTest, SyncBridgeTest, etc.) replace the
|
||||||
* global $wpdb with their own anonymous-class mock and never restore it. When
|
* global $wpdb with their own anonymous-class mock and never restore it. When
|
||||||
* our adapter tests run after those, $wpdb lacks the query/get_col/delete
|
* our adapter tests run after those, $wpdb lacks the query/get_col/delete
|
||||||
* methods our mirror_insert / cron-optimizer code paths call, causing
|
* methods our cron-optimizer code paths call, causing
|
||||||
* cross-test pollution failures.
|
* cross-test pollution failures.
|
||||||
*
|
*
|
||||||
* Trait reinstalls a known $wpdb mock + clears the wpdo_features option so
|
* Trait reinstalls a known $wpdb mock + clears the wpdo_features option so
|
||||||
|
|||||||
Reference in New Issue
Block a user