Files
2meet-data-optimizer-hivepr…/includes/hivepress/class-tmdo-hivepress-attribute-bridge.php
T
wpdev b4400a68e5 chore: initial snapshot of 2meet-data-optimizer-hivepress-addon v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:06:36 +08:00

175 lines
6.1 KiB
PHP

<?php
/**
* HivePress dynamic attribute bridge.
*
* HivePress lets users define custom listing/vendor attributes through the
* admin UI ("Listings → Attributes"). Each attribute creates a postmeta key
* shaped like `hp_listing_{slug}` with type info attached. When the
* attribute is marked `filterable=true` AND `searchable=true` it appears in
* archive search forms — and produces meta_query slowness identical to the
* pre-Sprint-1 hp_featured / hp_drafted patterns.
*
* This bridge introspects HivePress's attribute registry and reports which
* dynamic attributes are PROMOTION CANDIDATES — i.e. would benefit from
* being moved to a hot column. It does NOT alter database schema; that's
* destructive and belongs in a CLI tool (Sprint 4 will add
* `wp wpdo hivepress promote-attribute <slug>`).
*
* 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<int,string>
*/
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<int,array{model:string, slug:string, meta_key:string, type:string, filterable:bool, sortable:bool, searchable:bool}>
*/
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<int,array<string,mixed>> 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<string,mixed> 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<string,mixed> $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 )