`). * * Per Karpathy guideline: surface tradeoffs, don't auto-promote. * * Usage: * $bridge = new TMDO_HivePress_Attribute_Bridge(); * $candidates = $bridge->discover_attributes(); * // returns: [['slug' => 'beds', 'type' => 'integer', 'filterable' => true, 'searchable' => true, 'meta_key' => 'hp_listing_beds'], ...] * * @package WP_Data_Optimizer * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'TMDO_HivePress_Attribute_Bridge' ) ) { /** * Discovers HivePress dynamic attributes that would benefit from hot promotion. */ final class TMDO_HivePress_Attribute_Bridge { /** * Models whose attributes we inspect. * * @var array */ private const SUPPORTED_MODELS = array( 'listing', 'vendor' ); /** * Discover promotion candidates across all supported HP models. * * Returns one record per attribute that meets the promotion bar: * `filterable=true` (would appear in URL filters) OR `sortable=true` * (would feed orderby). * * @return array */ public function discover_attributes(): array { $out = array(); foreach ( self::SUPPORTED_MODELS as $model ) { $attributes = $this->fetch_attributes_for( $model ); foreach ( $attributes as $slug => $config ) { $record = $this->build_record( $model, (string) $slug, (array) $config ); if ( $this->is_promotion_candidate( $record ) ) { $out[] = $record; } } } return $out; } /** * Map of attribute records to suggested zone field configs (does NOT register). * * Use this output as input to a CLI promotion tool — it will pass each * config to `TMDO_Schema_Registry::register()` and trigger schema migration. * * @return array> Field config records. */ public function suggest_zone_configs(): array { $configs = array(); foreach ( $this->discover_attributes() as $record ) { $configs[] = array( 'post_type' => 'hp_' . $record['model'], 'meta_key' => $record['meta_key'], 'zone' => 'hot', 'data_type' => $this->sql_type_for( $record['type'] ), 'column' => sanitize_key( $record['meta_key'] ), 'indexed' => $record['filterable'] || $record['sortable'], ); } return $configs; } // ── Internal helpers ──────────────────────────────────────────────── /** * Pull the attribute definitions registered for a given HP model. * * Goes through HivePress' own filter so any third-party attribute * registrations are included. Returns empty when HivePress is not * loaded (defensive — bridge can run during introspection tests). * * @param string $model 'listing' or 'vendor'. * @return array Slug => attribute config. */ private function fetch_attributes_for( string $model ): array { if ( ! function_exists( 'apply_filters' ) ) { return array(); } $filter = sprintf( 'hivepress/v1/models/%s/attributes', $model ); $attrs = apply_filters( $filter, array() ); return is_array( $attrs ) ? $attrs : array(); } /** * Normalise one attribute config into our record shape. * * @param string $model HP model name. * @param string $slug Attribute slug. * @param array $cfg Raw attribute config from HivePress. * @return array{model:string, slug:string, meta_key:string, type:string, filterable:bool, sortable:bool, searchable:bool} */ private function build_record( string $model, string $slug, array $cfg ): array { return array( 'model' => $model, 'slug' => $slug, 'meta_key' => sprintf( 'hp_%s_%s', $model, $slug ), 'type' => (string) ( $cfg['edit_field']['type'] ?? $cfg['search_field']['type'] ?? 'text' ), 'filterable' => (bool) ( $cfg['filterable'] ?? false ), 'sortable' => (bool) ( $cfg['sortable'] ?? false ), 'searchable' => (bool) ( $cfg['searchable'] ?? false ), ); } /** * Whether an attribute is worth promoting to a hot column. * * @param array{model:string, slug:string, meta_key:string, type:string, filterable:bool, sortable:bool, searchable:bool} $record Attribute record. */ private function is_promotion_candidate( array $record ): bool { // Filterable attributes always go through meta_query in archives. // Sortable attributes go through `meta_key` in orderby. // Either case = postmeta hot path → worth promoting. return $record['filterable'] || $record['sortable']; } /** * Map HivePress field type → SQL column type for hot zone. * * Conservative defaults; over-sizing is safer than under-sizing. * * @param string $type HivePress field type. */ private function sql_type_for( string $type ): string { switch ( $type ) { case 'number': case 'integer': return 'int(11) NOT NULL DEFAULT 0'; case 'decimal': case 'price': return 'decimal(10,2) DEFAULT NULL'; case 'checkbox': case 'boolean': return 'tinyint(1) NOT NULL DEFAULT 0'; case 'date': return 'date DEFAULT NULL'; case 'text': case 'select': default: return "varchar(191) NOT NULL DEFAULT ''"; } } } } // end if ( ! class_exists )