Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 763f705ac1 | |||
| 5953a5ed3b | |||
| 9803f8379d | |||
| f7f9bc5cc4 | |||
| bfe8912600 | |||
| 15bbf335f4 | |||
| 0cd2a9db5b | |||
| d802bf0145 | |||
| df62edd0f5 | |||
| 69cd4b6f8b | |||
| c139ebca89 | |||
| 0efac618de | |||
| e57dd5bd15 | |||
| b2585459e3 | |||
| d129139c7d | |||
| f06a80eae5 | |||
| 130678a8e6 |
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: 2meet Data Optimizer — Hivepress AddOn
|
||||
* Plugin URI: https://2meet.io/2meet-data-optimizer/hivepress
|
||||
* Description: HivePress + 12 擴充整合:13 adapter (core/reviews/bookings/messages/memberships/requests/favorites/statistics/tags/seo/social-links/blocks/marketplace) + 7 HPCT interceptor + 5 query interceptor + admin tab + CLI namespace
|
||||
* Version: 0.3.0
|
||||
* Version: 0.5.3
|
||||
* Requires at least: 6.0
|
||||
* Tested up to: 6.9.4
|
||||
* Requires PHP: 8.1
|
||||
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'TMDO_HIVEPRESS_VERSION', '0.3.0' );
|
||||
define( 'TMDO_HIVEPRESS_VERSION', '0.5.3' );
|
||||
define( 'TMDO_HIVEPRESS_PATH', plugin_dir_path( __FILE__ ) );
|
||||
define( 'TMDO_HIVEPRESS_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'TMDO_HIVEPRESS_FILE', __FILE__ );
|
||||
|
||||
+105
@@ -11,6 +11,111 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
---
|
||||
|
||||
## [0.5.3] — 2026-10-07 — 瀏覽數 flush 不再造成漂移
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`TMDO_Listing_Stats::flush_views_to_postmeta()` 寫完後同步扁平表與快取**:每小時把 warm zone 的瀏覽數
|
||||
用原子 `UPDATE … meta_value + N` 加回 `hp_view_count`,繞過了 meta API:Hook Bus 看不到這次寫入,
|
||||
dual_write/shadow_read 下 `hp_listing_core.hp_view_count` 每批落後一次(夜間 drift-check 每晚 diff),
|
||||
物件快取(Redis 下跨請求)也一直是舊值。現在 UPDATE 之後清掉該篇的 `post_meta` 快取,
|
||||
並在原生仍是真相時(同時寫原生與扁平)用 `TMDO_Entity_Migration_Engine::migrate_entity()` 重建那一列。
|
||||
`hp_view_count` 同時在 `hp_listing_core` 與 `hp_request_core`:只重建含這個 key、而且適用該篇 post type 的群組
|
||||
(不帶 post type 的 `get_field()` 只回最後登記的那組)。aeav_only 不動(扁平才是真相)。
|
||||
|
||||
---
|
||||
|
||||
## [0.5.2] — 2026-10-05 — attribute bridge 的 meta key
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`TMDO_HivePress_Attribute_Bridge` 組出正確的 meta key**:HivePress 把每個模型的屬性存成
|
||||
`hp_{slug}`(例如 `hp_warranty`),不是 `hp_{model}_{slug}`。舊的 key 在資料庫裡不存在,
|
||||
照它建議的欄位登記後永遠是空的。
|
||||
|
||||
---
|
||||
|
||||
## [0.5.1] — 2026-10-02 — 重新打包
|
||||
|
||||
### Changed
|
||||
- 重新打包(無功能變更):與 WP-MVB v8 樞紐站(dev39)一同驗收後發版;程式碼與前一版相同。
|
||||
|
||||
---
|
||||
|
||||
## [0.5.0] — 2026-09-28
|
||||
|
||||
Released with 2meet-data-optimizer 1.4.0 (hardening 2.4).
|
||||
|
||||
### Changed
|
||||
|
||||
- `hp_listing_core` declares `(hp_price, post_id)`, `(hp_rating, post_id)` and
|
||||
`(hp_hourly_rate, post_id)` indexes for the listing sorts HivePress offers:
|
||||
with core 1.4.0 reading the ID tie-break from the flat `post_id`, a sort walks
|
||||
the index and stops at the page instead of sorting every match (dev36, 1.3M
|
||||
listings: price sort 14 s native → 0.37 s routed, hourly rate 11.5 s → 0.35 s,
|
||||
rating 43 s → 0.51 s). Core 1.3.0 ignores indexes ending with `post_id`, so
|
||||
nothing changes there. The pinned schema hash is unchanged. (PR-119)
|
||||
- Transient entries in a persistent object cache live a day (`MAX_CACHE_TTL`,
|
||||
HivePress's own default) instead of a week; a timeout still outlives its value
|
||||
by a minute. At a week dev36 kept 4.7M entries (1 GB) in the shared Redis and
|
||||
pushed other sites' keys out. Entries already cached keep their week. (PR-120)
|
||||
|
||||
---
|
||||
|
||||
## [0.4.0] — 2026-09-27
|
||||
|
||||
Released with 2meet-data-optimizer 1.3.0 (hardening 2.3).
|
||||
|
||||
### Changed
|
||||
|
||||
- `hp_listing_core` declares indexes on `hp_hourly_rate` and `hp_rating_count`
|
||||
(HivePress filters and sorts on both). They are declared through the group's
|
||||
`indexes`, not `searchable`, so the pinned schema hash is unchanged and the
|
||||
table is not rebuilt; core 1.3.0 adds them in place from WP-CLI / WP-Cron.
|
||||
dev36 (902k listings): two indexes in about 4.7 s; routed `rating_count IN`
|
||||
2.1 s → 0.36 s, `rating_count !=` 2.1 s → 0.76 s, hourly rate + price sort
|
||||
2.1 s → 0.65 s. (PR-090)
|
||||
- With a persistent object cache, transient entries now expire: a value after
|
||||
a week (`MAX_CACHE_TTL`; HivePress writes it before its timeout, and a miss
|
||||
only makes it rebuild), its timeout a minute after the later of that and its
|
||||
own time — HivePress reads a missing timeout as "never expires", so the
|
||||
timeout must go last. Entries of deleted posts no longer pile up. (PR-099,
|
||||
PR-115)
|
||||
|
||||
### Added
|
||||
|
||||
- Opt-in flattening of a site's own listing attributes: the
|
||||
`tmdo_hp_listing_attribute_fields` filter (`'hp_warranty' => 'integer'`)
|
||||
registers them in an `hp_listing_attrs` group — a new table, each column
|
||||
indexed at creation — leaving `hp_listing_core` unchanged. Types: text,
|
||||
textarea, integer, decimal, boolean, date, datetime, timestamp, enum. The
|
||||
group is registered once themes load, so the filter may live in a theme;
|
||||
core 1.3.0 keeps it native until a drift fix has filled it. (PR-109, PR-115)
|
||||
|
||||
### Upgrading
|
||||
|
||||
- Deploy with core 1.3.0: on an older core the AddOn declares no index at all
|
||||
(core 1.2.0 would run the ALTER inside a web request). (PR-090b)
|
||||
- Redis: entries written by 0.3.0 have no TTL (dev36 had 3.0 M). Give them one
|
||||
with non-blocking commands — never `wp cache flush-group`, which the Redis
|
||||
Object Cache drop-in runs as one server-side script and which blocked Redis
|
||||
on a large group. `NX` leaves entries written by 0.4.0 alone:
|
||||
|
||||
```
|
||||
redis-cli --scan --pattern '<WP_CACHE_KEY_SALT>wp:tmdo_hp_transient:*' \
|
||||
| awk '{printf "EXPIRE %s 604800 NX\r\n", $0}' | redis-cli --pipe
|
||||
redis-cli --scan --pattern '<WP_CACHE_KEY_SALT>wp:tmdo_hp_transient:_transient_timeout_*' \
|
||||
| awk '{printf "EXPIRE %s 608400 GT\r\n", $0}' | redis-cli --pipe
|
||||
```
|
||||
|
||||
The second pass makes timeouts outlive their values. dev36: 3,013,352 keys
|
||||
in 45 s and 1,867,569 timeouts in 32 s, 0 errors, Redis latency max 2 ms. The pattern
|
||||
follows the drop-in's key layout (`<salt><blog prefix>:<group>:<key>`);
|
||||
check it with `redis-cli --scan --pattern '<salt>*tmdo_hp_transient*' | head`
|
||||
first. redis-cli 7 has no `--count` with `--scan`.
|
||||
|
||||
---
|
||||
|
||||
## [0.3.0] — 2026-09-27
|
||||
|
||||
Released with 2meet-data-optimizer 1.2.0 (hardening 2.2).
|
||||
|
||||
@@ -76,6 +76,7 @@ final class TMDO_HivePress_Entity_Groups {
|
||||
self::register_hp_request_core_group();
|
||||
self::register_hp_vendor_core_group();
|
||||
self::register_hp_user_group();
|
||||
self::register_hp_listing_attributes_group();
|
||||
}
|
||||
|
||||
// ── Group definitions ────────────────────────────────────────────────────
|
||||
@@ -257,9 +258,66 @@ final class TMDO_HivePress_Entity_Groups {
|
||||
'label' => 'Hourly rate',
|
||||
),
|
||||
),
|
||||
array( 'post_types' => array( 'hp_listing' ) )
|
||||
array(
|
||||
'post_types' => array( 'hp_listing' ),
|
||||
// Filtered and sorted on by HivePress search. Declared as indexes
|
||||
// (added in place) rather than `searchable`, which would change
|
||||
// the schema hash and rebuild a table that may hold aeav_only data.
|
||||
// Only with core 1.3.0+ (ddl_allowed()): older cores run the ALTER
|
||||
// inside whatever request comes first, a visitor's included.
|
||||
'indexes' => method_exists( 'TMDO_Schema_Manager', 'ddl_allowed' ) ? array(
|
||||
'hourly_rate' => array( 'hp_hourly_rate' ),
|
||||
'rating_count' => array( 'hp_rating_count' ),
|
||||
// HivePress sorts: the value then the ID tie-break, which core 1.4.0
|
||||
// reads in order (flat-first) instead of sorting every match.
|
||||
'price_id' => array( 'hp_price', 'post_id' ),
|
||||
'rating_id' => array( 'hp_rating', 'post_id' ),
|
||||
'hourly_rate_id' => array( 'hp_hourly_rate', 'post_id' ),
|
||||
) : array(),
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* A site's own HivePress listing attributes (created under Listings →
|
||||
* Attributes, e.g. a "Warranty" number → hp_warranty), opt-in through the
|
||||
* `tmdo_hp_listing_attribute_fields` filter: `array( 'hp_warranty' => 'integer' )`.
|
||||
*
|
||||
* They get a group of their own — a new table, each column indexed at
|
||||
* creation — so hp_listing_core and its pinned definition stay unchanged.
|
||||
* Keys another group already manages, keys without the hp_ prefix and
|
||||
* types that cannot take a plain index (json, binary) are ignored. Core
|
||||
* fills the new table from native meta (drift fix) before using it.
|
||||
*
|
||||
* Registered once themes are loaded (the init:1 run): a filter added in a
|
||||
* theme is seen, and the group gets one definition per request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function register_hp_listing_attributes_group(): void {
|
||||
if ( ! did_action( 'after_setup_theme' ) ) {
|
||||
return;
|
||||
}
|
||||
$fields = array();
|
||||
foreach ( (array) apply_filters( 'tmdo_hp_listing_attribute_fields', array() ) as $key => $type ) {
|
||||
$key = (string) $key;
|
||||
$owner = TMDO_Entity_Registry::get_field( 'post', $key, 'hp_listing' );
|
||||
if ( 1 !== preg_match( '/^hp_[a-z0-9_]+$/', $key )
|
||||
|| ! in_array( $type, array( 'text', 'textarea', 'integer', 'decimal', 'boolean', 'date', 'datetime', 'timestamp', 'enum' ), true )
|
||||
|| ( null !== $owner && 'hp_listing_attrs' !== $owner['group'] ) ) {
|
||||
continue;
|
||||
}
|
||||
$fields[] = array(
|
||||
'key' => $key,
|
||||
'type' => $type,
|
||||
'searchable' => true,
|
||||
'label' => 'Site listing attribute',
|
||||
);
|
||||
}
|
||||
if ( $fields ) {
|
||||
TMDO_Entity_Registry::register_group( 'post', 'hp_listing_attrs', $fields, array( 'post_types' => array( 'hp_listing' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HivePress request core meta keys (post_type=hp_request).
|
||||
*
|
||||
|
||||
@@ -80,6 +80,9 @@ final class TMDO_Hivepress_Transient_Filter {
|
||||
/** Object cache group used instead of wp_options when a persistent cache is present. */
|
||||
public const CACHE_GROUP = 'tmdo_hp_transient';
|
||||
|
||||
/** Longest an object-cache entry lives: a day, HivePress's own default (2.4 PR-120). */
|
||||
public const MAX_CACHE_TTL = DAY_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Register the four metadata filters on `init`. Idempotent — safe to call
|
||||
* multiple times.
|
||||
@@ -267,8 +270,11 @@ final class TMDO_Hivepress_Transient_Filter {
|
||||
* In wp_options both halves of the transient pair are written with
|
||||
* `autoload=no` (they are read on-demand only — never via
|
||||
* wp_load_alloptions() — so eagerly loading them on every page would defeat
|
||||
* the point of the move). HivePress checks the timeout itself, so the
|
||||
* cache entries get no TTL of their own.
|
||||
* the point of the move). In the object cache the value, written before its
|
||||
* timeout, expires after MAX_CACHE_TTL (a miss only makes HivePress rebuild
|
||||
* it) and the timeout a minute after the later of that and its own time,
|
||||
* so HivePress always sees it expire first; entries of deleted posts do
|
||||
* not pile up (2.3 PR-099).
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Original `_transient_*` meta_key.
|
||||
@@ -281,7 +287,10 @@ final class TMDO_Hivepress_Transient_Filter {
|
||||
$option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated;
|
||||
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
wp_cache_set( $option_name, $meta_value, self::CACHE_GROUP );
|
||||
// A timeout outlives its value: HivePress takes a missing timeout for
|
||||
// "never expires" and would keep serving the value.
|
||||
$ttl = $is_timeout ? max( self::MAX_CACHE_TTL, (int) $meta_value - time() ) + MINUTE_IN_SECONDS : self::MAX_CACHE_TTL;
|
||||
wp_cache_set( $option_name, $meta_value, self::CACHE_GROUP, $ttl );
|
||||
return;
|
||||
}
|
||||
update_option( $option_name, $meta_value, false );
|
||||
|
||||
@@ -154,6 +154,8 @@ class TMDO_Listing_Stats {
|
||||
// Seed the key when it does not exist yet.
|
||||
if ( ! $updated ) {
|
||||
add_post_meta( $post_id, 'hp_view_count', $new_views, true );
|
||||
} else {
|
||||
self::resync_view_count( $post_id );
|
||||
}
|
||||
|
||||
// Remove the warm entry after flush.
|
||||
@@ -164,6 +166,40 @@ class TMDO_Listing_Stats {
|
||||
return $flushed;
|
||||
}
|
||||
|
||||
/**
|
||||
* The atomic UPDATE above bypasses the meta API: the object cache (persistent
|
||||
* under Redis) keeps the old count, and the Hook Bus never sees the write, so in
|
||||
* dual_write / shadow_read the flat column drifts by every flushed batch
|
||||
* (drift-check diff on hp_view_count each night). Drop the cached meta and
|
||||
* rebuild the entity's flat row from native — only while native is the source
|
||||
* of truth (aeav_only keeps flat authoritative).
|
||||
*
|
||||
* @param int $post_id Listing post ID.
|
||||
*/
|
||||
private static function resync_view_count( int $post_id ): void {
|
||||
wp_cache_delete( $post_id, 'post_meta' );
|
||||
if ( ! class_exists( 'TMDO_Mode_Manager' ) || ! class_exists( 'TMDO_Entity_Registry' ) || ! class_exists( 'TMDO_Entity_Migration_Engine' ) ) {
|
||||
return;
|
||||
}
|
||||
if ( ! TMDO_Mode_Manager::writes_to_flat( 'post' ) || ! TMDO_Mode_Manager::writes_to_eav( 'post' ) ) {
|
||||
return;
|
||||
}
|
||||
// hp_view_count lives in more than one group (hp_listing_core, hp_request_core):
|
||||
// get_field() without a post type returns whichever registered last. Rebuild only
|
||||
// the groups that hold the key and apply to this post's type.
|
||||
$type = (string) get_post_type( $post_id );
|
||||
foreach ( TMDO_Entity_Registry::get_groups_for_type( 'post' ) as $group ) {
|
||||
$types = TMDO_Entity_Registry::get_group_post_types( 'post', $group );
|
||||
if ( ! in_array( 'hp_view_count', TMDO_Entity_Registry::get_group_keys( 'post', $group ), true ) || ( $types && ! in_array( $type, $types, true ) ) ) {
|
||||
continue;
|
||||
}
|
||||
TMDO_Entity_Migration_Engine::migrate_entity( 'post', $group, $post_id );
|
||||
if ( class_exists( 'TMDO_Cache_Orchestrator' ) ) {
|
||||
TMDO_Cache_Orchestrator::invalidate( 'post', $post_id, $group );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Zone D: Expired Listing Archival ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* 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'], ...]
|
||||
* // returns: [['slug' => 'beds', 'type' => 'integer', 'filterable' => true, 'searchable' => true, 'meta_key' => 'hp_beds'], ...]
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
* @since 3.0.0
|
||||
@@ -123,7 +123,8 @@ if ( ! class_exists( 'TMDO_HivePress_Attribute_Bridge' ) ) {
|
||||
return array(
|
||||
'model' => $model,
|
||||
'slug' => $slug,
|
||||
'meta_key' => sprintf( 'hp_%s_%s', $model, $slug ),
|
||||
// HivePress stores every model's attribute as hp_{slug} (e.g. hp_warranty), not hp_{model}_{slug}.
|
||||
'meta_key' => 'hp_' . $slug,
|
||||
'type' => (string) ( $cfg['edit_field']['type'] ?? $cfg['search_field']['type'] ?? 'text' ),
|
||||
'filterable' => (bool) ( $cfg['filterable'] ?? false ),
|
||||
'sortable' => (bool) ( $cfg['sortable'] ?? false ),
|
||||
|
||||
@@ -24,6 +24,12 @@ if ( ! file_exists( $tmdo_core_bootstrap ) ) {
|
||||
}
|
||||
require_once $tmdo_core_bootstrap;
|
||||
|
||||
if ( ! function_exists( 'did_action' ) ) {
|
||||
function did_action( string $hook ): int {
|
||||
return (int) ( $GLOBALS['_wp_did_action'][ $hook ] ?? 0 );
|
||||
}
|
||||
}
|
||||
|
||||
define( 'TMDO_HIVEPRESS_PATH', dirname( __DIR__ ) . '/' );
|
||||
define( 'TMDO_HIVEPRESS_URL', 'http://localhost/wp-content/plugins/2meet-data-optimizer-hivepress-addon/' );
|
||||
define( 'TMDO_HIVEPRESS_FILE', dirname( __DIR__ ) . '/2meet-data-optimizer-hivepress-addon.php' );
|
||||
|
||||
@@ -213,6 +213,34 @@ class HivepressTransientFilterTest extends TestCase {
|
||||
$this->assertSame( array( '99999999' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, '_transient_timeout_hp_models/cat/v1', true ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Entries expire in the object cache too (2.3 PR-099): the value (written
|
||||
* first, before its timeout is known) after a day, the timeout after the
|
||||
* later of that and its own time, plus a minute. Orphans of deleted posts
|
||||
* no longer pile up.
|
||||
*/
|
||||
public function test_with_an_object_cache_entries_get_a_ttl(): void {
|
||||
// A day, HivePress's own default: at a week, 1.3M listings kept 4.7M
|
||||
// entries (1 GB) in a shared Redis and pushed other sites' keys out (2.4 PR-120).
|
||||
$this->assertSame( 86400, WPDO_Hivepress_Transient_Filter::MAX_CACHE_TTL );
|
||||
$GLOBALS['_wp_using_ext_object_cache'] = true;
|
||||
$key = '_transient_hp_models/cat/v1';
|
||||
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
|
||||
|
||||
WPDO_Hivepress_Transient_Filter::on_update( null, 100, $key, 'val', '' );
|
||||
WPDO_Hivepress_Transient_Filter::on_update( null, 100, '_transient_timeout_hp_models/cat/v1', (string) ( time() + 3600 ), '' );
|
||||
WPDO_Hivepress_Transient_Filter::on_update( null, 101, '_transient_timeout_hp_models/cat/v1', (string) ( time() - 5 ), '' );
|
||||
|
||||
$ttl = $GLOBALS['_wp_cache_ttl'][ WPDO_Hivepress_Transient_Filter::CACHE_GROUP ];
|
||||
$this->assertSame( WPDO_Hivepress_Transient_Filter::MAX_CACHE_TTL, $ttl[ '_transient_' . $translated ] );
|
||||
// A timeout must outlive its value: HivePress takes a missing timeout for
|
||||
// "never expires" and would serve the stale value (review F5).
|
||||
$this->assertSame( WPDO_Hivepress_Transient_Filter::MAX_CACHE_TTL + 60, $ttl[ '_transient_timeout_' . $translated ] );
|
||||
$this->assertSame( WPDO_Hivepress_Transient_Filter::MAX_CACHE_TTL + 60, $ttl[ '_transient_timeout_' . WPDO_Hivepress_Transient_Filter::translate_key( 101, $key ) ], 'already expired' );
|
||||
WPDO_Hivepress_Transient_Filter::on_update( null, 102, '_transient_timeout_hp_models/cat/v1', (string) ( time() + 2 * WPDO_Hivepress_Transient_Filter::MAX_CACHE_TTL ), '' );
|
||||
$this->assertEqualsWithDelta( 2 * WPDO_Hivepress_Transient_Filter::MAX_CACHE_TTL + 60, $GLOBALS['_wp_cache_ttl'][ WPDO_Hivepress_Transient_Filter::CACHE_GROUP ][ '_transient_timeout_' . WPDO_Hivepress_Transient_Filter::translate_key( 102, $key ) ], 2 );
|
||||
}
|
||||
|
||||
public function test_with_an_object_cache_a_value_left_in_wp_options_is_not_read(): void {
|
||||
// A miss must not fall back to get_option(): the option is absent after
|
||||
// cleanup and WordPress would add its name to `notoptions` for good.
|
||||
|
||||
@@ -21,6 +21,14 @@ if ( ! file_exists( $tmdo_core_bootstrap ) ) {
|
||||
. dirname( __DIR__, 3 ) . '/2meet-data-optimizer'
|
||||
);
|
||||
}
|
||||
// Defined before the core bootstrap (which only adds it when missing) so tests can read the TTL.
|
||||
if ( ! function_exists( 'wp_cache_set' ) ) {
|
||||
function wp_cache_set( $key, $value, $group = '', $ttl = 0 ): bool {
|
||||
$GLOBALS['_wp_cache'][ $group ][ $key ] = $value;
|
||||
$GLOBALS['_wp_cache_ttl'][ $group ][ $key ] = (int) $ttl;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
require_once $tmdo_core_bootstrap;
|
||||
|
||||
// External object cache switch for the transient filter tests (Redis on real sites).
|
||||
|
||||
@@ -62,7 +62,7 @@ class HivePressAttributeBridgeTest extends TestCase {
|
||||
|
||||
$this->assertSame( 'listing', $record['model'] );
|
||||
$this->assertSame( 'beds', $record['slug'] );
|
||||
$this->assertSame( 'hp_listing_beds', $record['meta_key'] );
|
||||
$this->assertSame( 'hp_beds', $record['meta_key'] );
|
||||
$this->assertSame( 'integer', $record['type'] );
|
||||
$this->assertTrue( $record['filterable'] );
|
||||
$this->assertFalse( $record['sortable'] );
|
||||
|
||||
@@ -37,9 +37,15 @@ class HivePressEntityGroupsTest extends TestCase {
|
||||
'hp_user' => 'hp_vendor_core',
|
||||
);
|
||||
|
||||
private function post_adapter(): TMDO_Entity_Adapter_Interface {
|
||||
$adapter = $this->createMock( TMDO_Entity_Adapter_Interface::class );
|
||||
$adapter->method( 'get_entity_id_column' )->willReturn( 'post_id' );
|
||||
return $adapter;
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
TMDO_Entity_Registry::init();
|
||||
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||
TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() );
|
||||
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
||||
}
|
||||
@@ -58,6 +64,86 @@ class HivePressEntityGroupsTest extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HivePress filters and sorts listings by these; they are declared as
|
||||
* group indexes (added in place by core) rather than `searchable`, which
|
||||
* would change the pinned hash and rebuild the table.
|
||||
*/
|
||||
public function test_listing_columns_filtered_and_sorted_on_are_indexed(): void {
|
||||
$this->assertSame(
|
||||
array(
|
||||
'hourly_rate' => array( 'hp_hourly_rate' ),
|
||||
'rating_count' => array( 'hp_rating_count' ),
|
||||
// Sorts: the value then the ID tie-break, read in order (2.4 PR-119).
|
||||
'price_id' => array( 'hp_price', 'post_id' ),
|
||||
'rating_id' => array( 'hp_rating', 'post_id' ),
|
||||
'hourly_rate_id' => array( 'hp_hourly_rate', 'post_id' ),
|
||||
),
|
||||
TMDO_Entity_Registry::get_group_indexes( 'post', 'hp_listing_core' )
|
||||
);
|
||||
}
|
||||
|
||||
public function test_site_listing_attributes_are_opt_in(): void {
|
||||
$this->assertSame( array(), TMDO_Entity_Registry::get_group_fields( 'post', 'hp_listing_attrs' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* A site flattens its own HivePress listing attributes (e.g. a "Warranty"
|
||||
* number attribute → hp_warranty) in a group of their own: a new table,
|
||||
* indexed at creation; hp_listing_core and its pinned hash stay as they are.
|
||||
*/
|
||||
public function test_site_listing_attributes_get_their_own_indexed_group(): void {
|
||||
$fields = static fn() => array(
|
||||
'hp_warranty' => 'integer',
|
||||
'hp_price' => 'decimal', // Already in hp_listing_core.
|
||||
'warranty' => 'integer', // Not a HivePress key.
|
||||
'hp_color' => 'colour', // Unknown type.
|
||||
'hp_features' => 'json', // No plain index on LONGTEXT.
|
||||
);
|
||||
add_filter( 'tmdo_hp_listing_attribute_fields', $fields );
|
||||
$GLOBALS['_wp_did_action']['after_setup_theme'] = 1;
|
||||
try {
|
||||
TMDO_Entity_Registry::init();
|
||||
TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() );
|
||||
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
||||
} finally {
|
||||
$GLOBALS['_wp_filter_callbacks']['tmdo_hp_listing_attribute_fields'] = array();
|
||||
unset( $GLOBALS['_wp_did_action'] );
|
||||
}
|
||||
|
||||
$this->assertSame( array( 'hp_warranty' ), TMDO_Entity_Registry::get_group_keys( 'post', 'hp_listing_attrs' ) );
|
||||
$this->assertTrue( (bool) TMDO_Entity_Registry::get_group_fields( 'post', 'hp_listing_attrs' )[0]['searchable'] );
|
||||
$this->assertSame( array( 'hp_listing' ), TMDO_Entity_Registry::get_group_post_types( 'post', 'hp_listing_attrs' ) );
|
||||
$this->assertSame( 'hp_listing_core', TMDO_Entity_Registry::get_field( 'post', 'hp_price' )['group'] );
|
||||
$this->test_definitions_match_persisted_schema_hashes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration runs twice (plugins_loaded, then init:1): the group is
|
||||
* registered on the run after themes load, so a theme's filter counts and
|
||||
* a filter that changed between runs cannot collide (review F8).
|
||||
*/
|
||||
public function test_site_listing_attributes_are_registered_once_themes_are_loaded(): void {
|
||||
add_filter( 'tmdo_hp_listing_attribute_fields', static fn() => array( 'hp_warranty' => 'integer' ) );
|
||||
try {
|
||||
TMDO_Entity_Registry::init();
|
||||
TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() );
|
||||
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
||||
$this->assertSame( array(), TMDO_Entity_Registry::get_group_keys( 'post', 'hp_listing_attrs' ), 'plugins_loaded run: not yet' );
|
||||
|
||||
$GLOBALS['_wp_did_action']['after_setup_theme'] = 1;
|
||||
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
||||
} finally {
|
||||
$GLOBALS['_wp_filter_callbacks']['tmdo_hp_listing_attribute_fields'] = array();
|
||||
unset( $GLOBALS['_wp_did_action'] );
|
||||
}
|
||||
|
||||
$this->assertSame( array( 'hp_warranty' ), TMDO_Entity_Registry::get_group_keys( 'post', 'hp_listing_attrs' ) );
|
||||
$this->assertSame( array(), TMDO_Entity_Registry::get_collisions() );
|
||||
}
|
||||
|
||||
public function test_shared_keys_keep_their_current_owner(): void {
|
||||
foreach ( self::SHARED_KEY_OWNERS as $key => $group ) {
|
||||
$this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key )['group'], $key );
|
||||
|
||||
Reference in New Issue
Block a user