b4400a68e5
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
175 lines
6.1 KiB
PHP
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 )
|