'hp_listing', 'vendor' => 'hp_vendor', ); /** * Constructor — bind WPDO core hooks via the inherited trait sugar. * * The bootstrap calls `on_register_query_hooks()` / `on_register_event_hooks()` * directly after instantiation, but field/table registration goes * through WordPress's action dispatch via this trait helper. */ public function __construct() { $this->bind_anti_eav_hooks(); } // ── Adapter identity ─────────────────────────────────────────────── /** * Stable adapter slug (matches the hivepress core wp.org slug). */ public function plugin_slug(): string { return 'hivepress'; } /** * Class probed for HivePress core presence. */ public function detection_class(): string { return 'HivePress\\Core'; } /** * Version constant probed for HivePress core presence. */ public function detection_const(): string { return 'HIVEPRESS_VERSION'; } /** * Minimum HivePress core version this adapter supports. * * Version 1.7.0 introduced the `_alias` / `_external` field convention * this adapter relies on. Older versions store fields differently and * would require fallback paths not implemented here. */ public function minimum_addon_version(): string { return '1.7.0'; } // ── Field registration ───────────────────────────────────────────── /** * Register Listing/Vendor zone field mappings. * * Fired on the `wpdo_register_fields` action via the trait's * `bind_anti_eav_hooks()`. Schema_Registry has internal dedup so * re-firing on init:1 is safe. * * @param TMDO_Schema_Registry $registry Schema registry singleton. */ public function on_register_fields( TMDO_Schema_Registry $registry ): void { // ── Listing hot fields (search/filter accelerators) ── $registry->register_many( $this->plugin_slug(), 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). array( 'post_type' => 'hp_vendor', 'meta_key' => 'hp_image', 'zone' => 'cold', 'cache_group' => 'wpdo_cold_hp_vendor', 'cache_ttl' => HOUR_IN_SECONDS, ), // Listing cold blob — gallery (display-only). array( 'post_type' => 'hp_listing', 'meta_key' => 'hp_images', 'zone' => 'cold', 'cache_group' => 'wpdo_cold_hp_listing', 'cache_ttl' => HOUR_IN_SECONDS, ), ) ); } /** * 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) ── /** * Bind HivePress search query hooks at priority 20 (after HP setup). */ public function on_register_query_hooks(): void { foreach ( self::HOT_MODELS as $model => $post_type ) { unset( $post_type ); // Bound below by string-interpolated $model only. add_action( "hivepress/v1/models/{$model}/query", array( $this, 'optimize_query' ), 20, 1 ); add_action( "hivepress/v1/models/{$model}/search", array( $this, 'optimize_search' ), 20, 2 ); } } /** * Optimize a HivePress model query by extracting hot-zone meta_query clauses. * * Extracted clauses are stashed on the WP_Query as `wpdo_hot_clauses`; * `TMDO_Query_Router` rewrites them into flat-column JOINs at * `posts_join` time. * * @param \WP_Query $query The WP_Query object. */ public function optimize_query( \WP_Query $query ): void { $post_types = (array) $query->get( 'post_type' ); if ( empty( $post_types ) ) { return; } $existing_hot = $query->get( 'wpdo_hot_clauses' ); if ( ! empty( $existing_hot ) ) { return; } $meta_query = (array) $query->get( 'meta_query' ); if ( empty( $meta_query ) ) { return; } $registry = TMDO_Schema_Registry::instance(); $hot_clauses = array(); $remaining = array(); foreach ( $meta_query as $k => $clause ) { if ( 'relation' === $k || ! is_array( $clause ) || ! isset( $clause['key'] ) ) { $remaining[ $k ] = $clause; continue; } $matched = false; foreach ( $post_types as $pt ) { $field = $registry->get_field( $pt, $clause['key'] ); if ( $field && 'hot' === $field['zone'] ) { $module = 'hot_' . sanitize_key( $pt ); if ( class_exists( 'TMDO_Feature_Flags' ) && ! TMDO_Feature_Flags::is_query_active( $module ) ) { continue; } $hot_clauses[ $pt ][] = array( 'column' => $field['column'], 'value' => $clause['value'] ?? '', 'compare' => strtoupper( trim( $clause['compare'] ?? '=' ) ), 'type' => strtoupper( trim( $clause['type'] ?? 'CHAR' ) ), ); $matched = true; break; } } if ( ! $matched ) { $remaining[ $k ] = $clause; } } if ( empty( $hot_clauses ) ) { return; } if ( isset( $meta_query['relation'] ) && ! isset( $remaining['relation'] ) ) { $remaining['relation'] = $meta_query['relation']; } $query->set( 'meta_query', $remaining ); $query->set( 'wpdo_hot_clauses', $hot_clauses ); } /** * Optimize a HivePress search query (delegates to optimize_query). * * Bound to `hivepress/v1/models/{model}/search` which passes attribute * fields as second arg; we ignore them (extracted via meta_query). * * @param \WP_Query $query The WP_Query object. * @param array $attribute_fields HP attribute field objects (unused). */ public function optimize_search( \WP_Query $query, array $attribute_fields ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter unset( $attribute_fields ); $this->optimize_query( $query ); } // ── Doctor / score / migrations ──────────────────────────────────── /** * Health probe: verify hot tables exist + sample row count. * * @return array{ok:bool, message:string, details?:array} */ 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. * * Default 1.0 across the board because: * - D1: only TMDO_API + Schema_Registry calls (no direct *_meta()) * - D2: no hardcoded wp_postmeta literals * - D3: hot tables for both Listing + Vendor exist * - D4: every field registered with explicit data_type + indexed flag * - D5: no cross-adapter writes needed (single-adapter scope) * - D6: zero options written; no transients (relies on WPDO core caching) * - D7: no queries against other adapters' tables * - D8: hot columns indexed on every search-relevant field */ public function suitability_score(): array { return $this->compose_score( array( // D5 is N/A for a single-adapter; report 1.0 (no cross-write needed). 'd5_hook_bus' => 1.0, ) ); } /** * FSM modules this adapter contributes. * * @return array */ public function migrations(): array { return array( 'hot_hp_listing', 'hot_hp_vendor', ); } } } // end if ( ! class_exists )