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
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
<?php
|
||||
/**
|
||||
* Core HivePress adapter — `hivepress` (the parent plugin).
|
||||
*
|
||||
* Owns three 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`.
|
||||
*
|
||||
* This adapter SUPERSEDES the legacy `class-tmdo-hivepress.php` (242 lines).
|
||||
* Behavioural parity:
|
||||
*
|
||||
* - `optimize_query()` / `optimize_search()` — preserved as instance
|
||||
* methods bound on `hivepress/v1/models/{listing,vendor}/{query,search}`
|
||||
* at priority 20 (after HP own setup).
|
||||
* - `register_extended_fields()` — preserved as `on_register_fields()`
|
||||
* handler. Geolocation / Bookings / Marketplace addon detection is
|
||||
* hoisted to the dedicated adapter files in Sprint 2/3; this core
|
||||
* adapter only registers `hivepress` core fields.
|
||||
* - Vendor cold fields (`hp_image`, `hp_images`) — preserved.
|
||||
*
|
||||
* NEW additions vs legacy:
|
||||
*
|
||||
* - `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).
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
* @since 3.0.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'TMDO_HivePress_Core_Adapter' ) ) {
|
||||
|
||||
/**
|
||||
* Core HivePress adapter (Listing / Vendor / User).
|
||||
*/
|
||||
final class TMDO_HivePress_Core_Adapter implements TMDO_HivePress_Adapter {
|
||||
|
||||
use TMDO_HivePress_Adapter_Trait;
|
||||
|
||||
/**
|
||||
* HP models that have Zone A hot tables. Used by query optimizer.
|
||||
*/
|
||||
private const HOT_MODELS = array(
|
||||
'listing' => '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<string,mixed>}
|
||||
*/
|
||||
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<int,string>
|
||||
*/
|
||||
public function migrations(): array {
|
||||
return array(
|
||||
'hot_hp_listing',
|
||||
'hot_hp_vendor',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} // end if ( ! class_exists )
|
||||
Reference in New Issue
Block a user