refactor(2meet-data-optimizer-hivepress-addon): take ownership of hp_* entity groups

- Add TMDO_HivePress_Entity_Groups registering hp_listing_core / hp_request_core / hp_vendor_core / hp_user, copied verbatim from core (same order; schema hashes match the persisted wpdo_registry_meta rows)
- Drop the core adapter's conflicting hp_user definition (hp_verified / hp_avatar_id — keys HivePress never writes)
- Verify: HivePressEntityGroupsTest (identical to core, same shared-key resolution, no collisions); HP unit 144 / integration 49 OK; hashes = dev20 registry_meta
- PR: PR-019
This commit is contained in:
2026-09-25 23:43:40 +08:00
parent ff6d6b10e4
commit 19add5fc16
7 changed files with 346 additions and 48 deletions
+1
View File
@@ -30,6 +30,7 @@ add_action(
'TMDO_HivePress_Bootstrap' => 'WPDO_HivePress_Bootstrap',
'TMDO_HivePress_Conflict_Guard' => 'WPDO_HivePress_Conflict_Guard',
'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_Detector' => 'WPDO_HivePress_Detector',
'TMDO_HivePress_Favorites_Adapter' => 'WPDO_HivePress_Favorites_Adapter',
@@ -68,6 +68,7 @@ final class TMDO_HP_Bootstrap {
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-entity-groups.php';
require_once $base . 'includes/class-tmdo-listing-stats.php';
}
@@ -97,6 +98,9 @@ final class TMDO_HP_Bootstrap {
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
if ( is_admin() ) {
$admin_file = TMDO_HIVEPRESS_PATH . 'admin/class-tmdo-admin-hivepress.php';
@@ -0,0 +1,251 @@
<?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
*
* 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 also share keys (hp_status, hp_user, …) and the
* entity field index is last-registered-wins, so 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.
*
* @return void
*/
public static function register(): void {
add_action( 'wpdo_register_entity_fields', array( __CLASS__, 'register_entity_fields' ) );
}
/**
* 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_listing_core_group();
self::register_hp_request_core_group();
self::register_hp_vendor_core_group();
self::register_hp_user_group();
}
// ── Group definitions ────────────────────────────────────────────────────
/**
* 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',
),
)
);
}
/**
* 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',
),
)
);
}
/**
* 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',
),
)
);
}
/**
* 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',
),
)
);
}
}
@@ -2,15 +2,14 @@
/**
* 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
* (drafted, featured, verified, expired_time, featured_time).
* - `Vendor` (post_type=hp_vendor) — Hot column `verified`; cold blob
* for image / description (display-only).
* - `User` (entity=user) — Entity group `hp_user_core` for
* `hp_verified` and the WP-stock `first_name` / `last_name` /
* `description`.
* - `User` (entity=user) — no zone fields; its `hp_user`
* entity group is registered by TMDO_HivePress_Entity_Groups.
*
* This adapter SUPERSEDES the legacy `class-tmdo-hivepress.php` (242 lines).
* Behavioural parity:
@@ -29,9 +28,9 @@
* - `hp_drafted` (Listing) — Hot tinyint(1) for user-dashboard filtering.
* - `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).
* (This adapter used to register a second `hp_user` definition —
* hp_verified / hp_avatar_id, keys HivePress never writes — which the
* registry silently dropped; TMDO_HivePress_Entity_Groups owns it now.)
*
* @package WP_Data_Optimizer
* @since 3.0.0
@@ -166,47 +165,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) ──
/**
+1
View File
@@ -45,6 +45,7 @@ $tmdo_hp_files = array(
'includes/hivepress/class-tmdo-hivepress-rest.php',
'includes/class-tmdo-hivepress-transient-filter.php',
'includes/class-tmdo-hivepress-term-comment-fields.php',
'includes/class-tmdo-hivepress-entity-groups.php',
'includes/class-tmdo-hpct-import.php',
'includes/class-tmdo-listing-stats.php',
'admin/class-tmdo-admin-hivepress.php',
+1
View File
@@ -44,6 +44,7 @@ $tmdo_hp_files = array(
'includes/hivepress/class-tmdo-hivepress-rest.php',
'includes/class-tmdo-hivepress-transient-filter.php',
'includes/class-tmdo-hivepress-term-comment-fields.php',
'includes/class-tmdo-hivepress-entity-groups.php',
'includes/class-tmdo-hpct-import.php',
'admin/class-tmdo-admin-hivepress.php',
'cli/class-tmdo-cli-hivepress.php',
+82
View File
@@ -0,0 +1,82 @@
<?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 core's version, so the AddOn must
* register byte-identical definitions: any drift changes the schema hash and
* would ALTER tables that hold aeav_only data once core stops registering
* them (PR-020).
*/
class HivePressEntityGroupsTest extends TestCase {
private const GROUPS = array(
array( 'post', 'hp_listing_core' ),
array( 'post', 'hp_request_core' ),
array( 'post', 'hp_vendor_core' ),
array( 'user', 'hp_user' ),
);
private function fresh_registry(): 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 ) );
}
private function register_core(): void {
TMDO_Post_Fields::register_entity_fields();
TMDO_Member_Fields::register_entity_fields();
}
public function test_definitions_match_core_exactly(): void {
$this->fresh_registry();
$this->register_core();
$core = array();
foreach ( self::GROUPS as $g ) {
$core[ $g[1] ] = TMDO_Entity_Registry::get_group_fields( $g[0], $g[1] );
}
$this->fresh_registry();
TMDO_HivePress_Entity_Groups::register_entity_fields();
foreach ( self::GROUPS as $g ) {
$this->assertNotEmpty( $core[ $g[1] ], "core no longer registers {$g[1]}" );
$this->assertSame( $core[ $g[1] ], TMDO_Entity_Registry::get_group_fields( $g[0], $g[1] ), "{$g[0]}/{$g[1]} drifted from core" );
}
}
public function test_shared_keys_resolve_to_the_same_group_as_core(): void {
// hp_status / hp_user / hp_view_count … appear in several post groups and
// field_index is last-registered-wins, so registration order matters.
$this->fresh_registry();
$this->register_core();
$core = array();
foreach ( self::GROUPS as $g ) {
foreach ( TMDO_Entity_Registry::get_group_keys( $g[0], $g[1] ) as $key ) {
$core[ $g[0] . ':' . $key ] = TMDO_Entity_Registry::get_field( $g[0], $key )['group'];
}
}
$this->fresh_registry();
TMDO_HivePress_Entity_Groups::register_entity_fields();
foreach ( $core as $id => $group ) {
list( $type, $key ) = explode( ':', $id, 2 );
$this->assertSame( $group, TMDO_Entity_Registry::get_field( $type, $key )['group'], $id );
}
}
public function test_registering_alongside_core_raises_no_collision(): void {
$this->fresh_registry();
$this->register_core();
TMDO_HivePress_Entity_Groups::register_entity_fields();
( new TMDO_HivePress_Core_Adapter() )->on_register_entity_fields();
$this->assertSame( array(), TMDO_Entity_Registry::get_collisions() );
}
}