diff --git a/includes/back-compat-aliases.php b/includes/back-compat-aliases.php index 27c5adf..1a729c0 100644 --- a/includes/back-compat-aliases.php +++ b/includes/back-compat-aliases.php @@ -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', diff --git a/includes/class-tmdo-hivepress-bootstrap.php b/includes/class-tmdo-hivepress-bootstrap.php index 831d33d..c146afe 100644 --- a/includes/class-tmdo-hivepress-bootstrap.php +++ b/includes/class-tmdo-hivepress-bootstrap.php @@ -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'; diff --git a/includes/class-tmdo-hivepress-entity-groups.php b/includes/class-tmdo-hivepress-entity-groups.php new file mode 100644 index 0000000..8e94bf5 --- /dev/null +++ b/includes/class-tmdo-hivepress-entity-groups.php @@ -0,0 +1,251 @@ + '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', + ), + ) + ); + } +} diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php index 25e9664..1580f78 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php @@ -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) ── /** diff --git a/tests/bootstrap.php b/tests/bootstrap.php index e358c4e..cbaaa44 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -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', diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index ec73697..1b21448 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -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', diff --git a/tests/unit/HivePressEntityGroupsTest.php b/tests/unit/HivePressEntityGroupsTest.php new file mode 100644 index 0000000..fa917d2 --- /dev/null +++ b/tests/unit/HivePressEntityGroupsTest.php @@ -0,0 +1,82 @@ +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() ); + } +}