Files
2meet-data-optimizer-hivepr…/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php
T
wpdev a01314cbb1
Tests / PHP Lint (pull_request) Successful in 1m5s
Tests / Unit Tests (pull_request) Successful in 1m9s
Tests / Integration Tests (pull_request) Failing after 1m46s
refactor(2meet-data-optimizer-hivepress-addon): leave HivePress queries to the post router
- Do not bind optimize_query / optimize_search on hivepress/v1/models/{listing,vendor}/{query,search} while core's TMDO_Post_Query_Router is active: core now routes HivePress searches (filters and _sort) to the entity flat tables, and two routers would claim the same clauses
- Version 0.2.0 (released with core 1.1.0); CHANGELOG
- Verify: composer test (144, +2 binding tests); integration 49; dev36 fe.sh 9 URLs ids == native (except known flat drift)
- PR: PR-063
2026-09-26 20:19:27 +08:00

278 lines
8.7 KiB
PHP

<?php
/**
* Core HivePress adapter — `hivepress` (the parent plugin).
*
* Covers the primary HivePress models:
*
* - `Listing` (post_type=hp_listing) — cold blob for the gallery.
* - `Vendor` (post_type=hp_vendor) — cold blob for the image.
* - `User` (entity=user) — no zone fields; its `hp_user`
* entity group is registered by TMDO_HivePress_Entity_Groups.
*
* Search/filter keys are flattened by TMDO_HivePress_Entity_Groups
* (ADR-003: no Hot zone fields).
*
* 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.
*
* (This adapter used to register a second `hp_user` definition —
* hp_verified / hp_avatar_id, keys HivePress never writes — which the
* registry silently dropped; TMDO_HivePress_Entity_Groups owns it now.)
*
* @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 {
// ── Cold blobs (display-only) ──
$registry->register_many(
$this->plugin_slug(),
array(
// 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,
),
)
);
}
// ── Query optimization (preserved from legacy class-tmdo-hivepress.php) ──
/**
* Bind HivePress search query hooks at priority 20 (after HP setup).
*
* Not while core's post router serves HivePress searches from the
* entity flat tables: two routers would claim the same clauses.
*/
public function on_register_query_hooks(): void {
if ( class_exists( 'TMDO_Post_Query_Router' ) && TMDO_Post_Query_Router::is_router_active() ) {
return;
}
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 ────────────────────────────────────
/**
* 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,
)
);
}
}
} // end if ( ! class_exists )