hardening 2.0: 2meet-data-optimizer (staged rollout, dev22 first) #3
@@ -268,6 +268,48 @@ class TMDO_Installer {
|
|||||||
self::install();
|
self::install();
|
||||||
}
|
}
|
||||||
self::maybe_preserve_misc_bucket_default();
|
self::maybe_preserve_misc_bucket_default();
|
||||||
|
self::maybe_init_post_scoped_routing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-time default for post_type-scoped entity routing.
|
||||||
|
*
|
||||||
|
* Autoloaded because TMDO_Entity_Registry::get_field() reads it per lookup.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function maybe_init_post_scoped_routing(): void {
|
||||||
|
global $wpdb;
|
||||||
|
if ( false !== get_option( TMDO_Entity_Registry::SCOPED_ROUTING_OPTION, false ) || ! class_exists( 'TMDO_Mode_Manager' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$has_rows = false;
|
||||||
|
$tables = (array) $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $wpdb->prefix . 'wpdo_post_' ) . '%' ) );
|
||||||
|
foreach ( $tables as $table ) {
|
||||||
|
if ( $wpdb->get_var( "SELECT 1 FROM `{$table}` LIMIT 1" ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from SHOW TABLES.
|
||||||
|
$has_rows = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add_option( TMDO_Entity_Registry::SCOPED_ROUTING_OPTION, self::post_scoped_routing_default( TMDO_Mode_Manager::get( 'post' ), $has_rows ), '', true );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether post_type-scoped routing can start on right away.
|
||||||
|
*
|
||||||
|
* Existing post flat rows were written with legacy routing (a key shared by
|
||||||
|
* several groups went to the last one registered), so they may sit in
|
||||||
|
* another post type's table. Scoped lookups stay off there until the rows
|
||||||
|
* are reconciled; with the post bridge disabled nothing reads them.
|
||||||
|
*
|
||||||
|
* @param string $post_mode Current post entity bridge mode.
|
||||||
|
* @param bool $has_post_flat_rows Whether any wpdo_post_* table has rows.
|
||||||
|
* @return string '1' to enable, '0' to wait for reconcile.
|
||||||
|
*/
|
||||||
|
public static function post_scoped_routing_default( string $post_mode, bool $has_post_flat_rows ): string {
|
||||||
|
return ( 'disabled' === $post_mode || ! $has_post_flat_rows ) ? '1' : '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ final class TMDO_Entity_Registry {
|
|||||||
/** @var array<int, array{type:string, group:string, fields:array}> 待建表的 Schema 清單 */
|
/** @var array<int, array{type:string, group:string, fields:array}> 待建表的 Schema 清單 */
|
||||||
private static array $pending_schemas = array();
|
private static array $pending_schemas = array();
|
||||||
|
|
||||||
|
/** @var array<string, array<string, array>> 實體類型 → meta_key → 欄位定義(僅未限定 post_type 的群組)*/
|
||||||
|
private static array $unscoped_index = array();
|
||||||
|
|
||||||
|
/** @var array<string, array<string, array<string, array>>> 實體類型 → post_type → meta_key → 欄位定義 */
|
||||||
|
private static array $scoped_index = array();
|
||||||
|
|
||||||
/** @var array<string, array> "entity/group" → 同名群組以不同定義重複註冊的紀錄 */
|
/** @var array<string, array> "entity/group" → 同名群組以不同定義重複註冊的紀錄 */
|
||||||
private static array $collisions = array();
|
private static array $collisions = array();
|
||||||
|
|
||||||
@@ -55,10 +61,15 @@ final class TMDO_Entity_Registry {
|
|||||||
/** 合法的實體類型 */
|
/** 合法的實體類型 */
|
||||||
public const VALID_ENTITY_TYPES = array( 'post', 'user', 'term', 'comment' );
|
public const VALID_ENTITY_TYPES = array( 'post', 'user', 'term', 'comment' );
|
||||||
|
|
||||||
|
/** Option:'1' 時 get_field() 依傳入的 post_type 解析(見 get_field())。 */
|
||||||
|
public const SCOPED_ROUTING_OPTION = 'tmdo_post_scoped_routing';
|
||||||
|
|
||||||
public static function init(): void {
|
public static function init(): void {
|
||||||
self::$adapters = array();
|
self::$adapters = array();
|
||||||
self::$groups = array();
|
self::$groups = array();
|
||||||
self::$field_index = array();
|
self::$field_index = array();
|
||||||
|
self::$unscoped_index = array();
|
||||||
|
self::$scoped_index = array();
|
||||||
self::$pending_schemas = array();
|
self::$pending_schemas = array();
|
||||||
self::$collisions = array();
|
self::$collisions = array();
|
||||||
self::$restored = array();
|
self::$restored = array();
|
||||||
@@ -100,10 +111,14 @@ final class TMDO_Entity_Registry {
|
|||||||
* - unique (bool) 唯一索引
|
* - unique (bool) 唯一索引
|
||||||
* - options (array) enum 選項
|
* - options (array) enum 選項
|
||||||
* - label (string) 顯示名稱
|
* - label (string) 顯示名稱
|
||||||
|
* @param array $args 選填:post_types (string[]) — 群組只套用於這些 post_type。
|
||||||
|
* 多個群組共用 meta_key 時,依 post 本身的類型決定歸屬;
|
||||||
|
* 不屬於任何宣告 post_type 的 post 不受管理(走原生 meta)。
|
||||||
|
* 不列入欄位定義,所以不影響 schema hash 與衝突比對。
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function register_group( string $entity_type, string $group_name, array $fields ): bool {
|
public static function register_group( string $entity_type, string $group_name, array $fields, array $args = array() ): bool {
|
||||||
|
|
||||||
if ( ! in_array( $entity_type, self::VALID_ENTITY_TYPES, true ) ) {
|
if ( ! in_array( $entity_type, self::VALID_ENTITY_TYPES, true ) ) {
|
||||||
return false;
|
return false;
|
||||||
@@ -128,8 +143,9 @@ final class TMDO_Entity_Registry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 建立索引以供快速查詢
|
// 建立索引以供快速查詢
|
||||||
|
$post_types = array_values( array_filter( array_map( 'strval', (array) ( $args['post_types'] ?? array() ) ) ) );
|
||||||
foreach ( $normalized as $normalized_field ) {
|
foreach ( $normalized as $normalized_field ) {
|
||||||
self::$field_index[ $entity_type ][ $normalized_field['key'] ] = $normalized_field;
|
self::index_field( $entity_type, $normalized_field, $post_types );
|
||||||
}
|
}
|
||||||
|
|
||||||
self::$groups[ $entity_type ][ $group_name ] = $normalized;
|
self::$groups[ $entity_type ][ $group_name ] = $normalized;
|
||||||
@@ -229,7 +245,7 @@ final class TMDO_Entity_Registry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ( $kept as $field ) {
|
foreach ( $kept as $field ) {
|
||||||
self::$field_index[ $entity_type ][ $field['key'] ] = $field;
|
self::index_field( $entity_type, $field, array() );
|
||||||
}
|
}
|
||||||
self::$groups[ $entity_type ][ $group_name ] = $kept;
|
self::$groups[ $entity_type ][ $group_name ] = $kept;
|
||||||
|
|
||||||
@@ -244,6 +260,24 @@ final class TMDO_Entity_Registry {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把欄位加入索引。field_index 保留舊語意(整個實體共用、後註冊者覆蓋),
|
||||||
|
* 供不帶 post_type 的查詢使用;另外依是否限定 post_type 分別建索引。
|
||||||
|
*
|
||||||
|
* @param string[] $post_types 群組限定的 post_type;空陣列表示不限定。
|
||||||
|
*/
|
||||||
|
private static function index_field( string $entity_type, array $field, array $post_types ): void {
|
||||||
|
self::$field_index[ $entity_type ][ $field['key'] ] = $field;
|
||||||
|
|
||||||
|
if ( empty( $post_types ) ) {
|
||||||
|
self::$unscoped_index[ $entity_type ][ $field['key'] ] = $field;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ( $post_types as $post_type ) {
|
||||||
|
self::$scoped_index[ $entity_type ][ $post_type ][ $field['key'] ] = $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 本請求從 wpdo_registry_meta 還原的孤兒群組。
|
* 本請求從 wpdo_registry_meta 還原的孤兒群組。
|
||||||
*
|
*
|
||||||
@@ -305,11 +339,22 @@ final class TMDO_Entity_Registry {
|
|||||||
/**
|
/**
|
||||||
* 依 meta_key 查詢欄位定義
|
* 依 meta_key 查詢欄位定義
|
||||||
*
|
*
|
||||||
|
* 傳入 $post_type 且 SCOPED_ROUTING_OPTION 開啟時:先找限定該 post_type 的群組,
|
||||||
|
* 再找不限定的群組,都沒有則為 null。否則沿用舊語意(整個實體共用索引,
|
||||||
|
* 共用 key 由最後註冊的群組擁有)。舊資料可能已依舊語意散落在別的群組表,
|
||||||
|
* 所以既有站要等資料對齊後才開啟 option。
|
||||||
|
*
|
||||||
|
* @param string $post_type 選填:post 的 post_type。
|
||||||
* @return array|null 欄位定義,或 null(非 UAE 管理欄位)
|
* @return array|null 欄位定義,或 null(非 UAE 管理欄位)
|
||||||
*/
|
*/
|
||||||
public static function get_field( string $entity_type, string $meta_key ): ?array {
|
public static function get_field( string $entity_type, string $meta_key, string $post_type = '' ): ?array {
|
||||||
|
if ( '' === $post_type || '1' !== (string) get_option( self::SCOPED_ROUTING_OPTION, '0' ) ) {
|
||||||
return self::$field_index[ $entity_type ][ $meta_key ] ?? null;
|
return self::$field_index[ $entity_type ][ $meta_key ] ?? null;
|
||||||
}
|
}
|
||||||
|
return self::$scoped_index[ $entity_type ][ $post_type ][ $meta_key ]
|
||||||
|
?? self::$unscoped_index[ $entity_type ][ $meta_key ]
|
||||||
|
?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取得實體類型下所有群組名稱
|
* 取得實體類型下所有群組名稱
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ final class TMDO_Hook_Bus {
|
|||||||
return $check;
|
return $check;
|
||||||
}
|
}
|
||||||
|
|
||||||
$field_def = TMDO_Entity_Registry::get_field( $type, $meta_key );
|
$field_def = self::field_for( $type, (int) $object_id, $meta_key );
|
||||||
if ( ! $field_def ) {
|
if ( ! $field_def ) {
|
||||||
return $check; // 非管理欄位,放行
|
return $check; // 非管理欄位,放行
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@ final class TMDO_Hook_Bus {
|
|||||||
return $check;
|
return $check;
|
||||||
}
|
}
|
||||||
|
|
||||||
$field_def = TMDO_Entity_Registry::get_field( $type, $meta_key );
|
$field_def = self::field_for( $type, (int) $object_id, $meta_key );
|
||||||
if ( ! $field_def ) {
|
if ( ! $field_def ) {
|
||||||
return $check;
|
return $check;
|
||||||
}
|
}
|
||||||
@@ -374,7 +374,7 @@ final class TMDO_Hook_Bus {
|
|||||||
return $check;
|
return $check;
|
||||||
}
|
}
|
||||||
|
|
||||||
$field_def = TMDO_Entity_Registry::get_field( $type, $meta_key );
|
$field_def = self::field_for( $type, (int) $object_id, $meta_key );
|
||||||
if ( ! $field_def ) {
|
if ( ! $field_def ) {
|
||||||
return $check;
|
return $check;
|
||||||
}
|
}
|
||||||
@@ -471,7 +471,7 @@ final class TMDO_Hook_Bus {
|
|||||||
return $check;
|
return $check;
|
||||||
}
|
}
|
||||||
|
|
||||||
$field_def = TMDO_Entity_Registry::get_field( $type, $meta_key );
|
$field_def = self::field_for( $type, (int) $object_id, $meta_key );
|
||||||
if ( ! $field_def ) {
|
if ( ! $field_def ) {
|
||||||
return $check;
|
return $check;
|
||||||
}
|
}
|
||||||
@@ -622,6 +622,19 @@ final class TMDO_Hook_Bus {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 欄位定義查詢;post meta 依該 post 本身的 post_type 解析(見 TMDO_Entity_Registry::get_field())。
|
||||||
|
*
|
||||||
|
* 先做不帶 post_type 的查詢:大多數 key 不受管理,可省下 get_post_type()。
|
||||||
|
*/
|
||||||
|
private static function field_for( string $type, int $object_id, string $meta_key ): ?array {
|
||||||
|
$field_def = TMDO_Entity_Registry::get_field( $type, $meta_key );
|
||||||
|
if ( null === $field_def || 'post' !== $type ) {
|
||||||
|
return $field_def;
|
||||||
|
}
|
||||||
|
return TMDO_Entity_Registry::get_field( $type, $meta_key, (string) get_post_type( $object_id ) );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 直接讀取(繞過 WP filter 系統)
|
* 直接讀取(繞過 WP filter 系統)
|
||||||
* 供 wpdo_get_meta() 便利函式使用,性能更好
|
* 供 wpdo_get_meta() 便利函式使用,性能更好
|
||||||
@@ -646,7 +659,7 @@ final class TMDO_Hook_Bus {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
$field_def = TMDO_Entity_Registry::get_field( $type, $key );
|
$field_def = self::field_for( $type, $entity_id, $key );
|
||||||
if ( ! $field_def ) {
|
if ( ! $field_def ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,7 +340,8 @@ final class TMDO_Post_Fields {
|
|||||||
'type' => 'decimal',
|
'type' => 'decimal',
|
||||||
'label' => 'Hourly rate',
|
'label' => 'Hourly rate',
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
array( 'post_types' => array( 'hp_listing' ) )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +385,8 @@ final class TMDO_Post_Fields {
|
|||||||
'type' => 'integer',
|
'type' => 'integer',
|
||||||
'label' => 'View counter',
|
'label' => 'View counter',
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
array( 'post_types' => array( 'hp_request' ) )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +429,8 @@ final class TMDO_Post_Fields {
|
|||||||
'searchable' => true,
|
'searchable' => true,
|
||||||
'label' => 'Average rating',
|
'label' => 'Average rating',
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
array( 'post_types' => array( 'hp_vendor' ) )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class TMDO_Post_Query_Router {
|
|||||||
|
|
||||||
$matched = false;
|
$matched = false;
|
||||||
foreach ( $post_types as $pt ) {
|
foreach ( $post_types as $pt ) {
|
||||||
$field = TMDO_Entity_Registry::get_field( 'post', $clause['key'] );
|
$field = TMDO_Entity_Registry::get_field( 'post', $clause['key'], (string) $pt );
|
||||||
if ( ! $field ) {
|
if ( ! $field ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook Bus routes post meta to the group of the post's own type.
|
||||||
|
*
|
||||||
|
* Two groups share `shared_key`; the listing group is scoped to hp_listing,
|
||||||
|
* the vendor group to hp_vendor. Before scoping, both posts' values landed
|
||||||
|
* in the vendor table (last registered wins).
|
||||||
|
*/
|
||||||
|
class PostScopedRoutingTest extends TestCase {
|
||||||
|
|
||||||
|
private const LISTING_TABLE = 'wp_itest_wpdo_post_scope_listing';
|
||||||
|
private const VENDOR_TABLE = 'wp_itest_wpdo_post_scope_vendor';
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$base = WPDO_PLUGIN_DIR;
|
||||||
|
foreach ( array(
|
||||||
|
'includes/adapters/interface-entity-adapter.php',
|
||||||
|
'includes/engine/class-tmdo-type-caster.php',
|
||||||
|
'includes/engine/class-tmdo-schema-manager.php',
|
||||||
|
'includes/engine/class-tmdo-entity-registry.php',
|
||||||
|
'includes/engine/class-tmdo-mode-manager.php',
|
||||||
|
'includes/engine/class-tmdo-cache-orchestrator.php',
|
||||||
|
'includes/adapters/class-tmdo-adapter-post.php',
|
||||||
|
'includes/engine/class-tmdo-hook-bus.php',
|
||||||
|
) as $file ) {
|
||||||
|
require_once $base . $file;
|
||||||
|
}
|
||||||
|
foreach ( array( self::LISTING_TABLE, self::VENDOR_TABLE ) as $table ) {
|
||||||
|
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||||
|
$wpdb->query(
|
||||||
|
"CREATE TABLE `{$table}` (
|
||||||
|
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
`post_id` BIGINT(20) NOT NULL,
|
||||||
|
`shared_key` VARCHAR(255) DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_entity` (`post_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tearDownAfterClass(): void {
|
||||||
|
global $wpdb;
|
||||||
|
foreach ( array( self::LISTING_TABLE, self::VENDOR_TABLE ) as $table ) {
|
||||||
|
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||||
|
}
|
||||||
|
WPDO_Entity_Registry::init();
|
||||||
|
$ref = new ReflectionProperty( WPDO_Mode_Manager::class, 'cache' );
|
||||||
|
$ref->setAccessible( true );
|
||||||
|
$ref->setValue( null, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
global $wpdb;
|
||||||
|
foreach ( array( self::LISTING_TABLE, self::VENDOR_TABLE ) as $table ) {
|
||||||
|
$wpdb->query( "TRUNCATE TABLE `{$table}`" );
|
||||||
|
}
|
||||||
|
$GLOBALS['_wp_options'] = array( WPDO_Entity_Registry::SCOPED_ROUTING_OPTION => '1' );
|
||||||
|
$GLOBALS['_wp_cache'] = array();
|
||||||
|
$GLOBALS['_wp_post_types'] = array( 101 => 'hp_listing', 202 => 'hp_vendor', 303 => 'post' );
|
||||||
|
$GLOBALS['_wp_current_filter'] = 'update_post_metadata';
|
||||||
|
|
||||||
|
$tec = new ReflectionProperty( WPDO_Schema_Manager::class, 'table_exists_cache' );
|
||||||
|
$tec->setAccessible( true );
|
||||||
|
$tec->setValue( null, array( self::LISTING_TABLE => true, self::VENDOR_TABLE => true ) );
|
||||||
|
$l1 = new ReflectionProperty( WPDO_Cache_Orchestrator::class, 'l1_cache' );
|
||||||
|
$l1->setAccessible( true );
|
||||||
|
$l1->setValue( null, array() );
|
||||||
|
$mode = new ReflectionProperty( WPDO_Mode_Manager::class, 'cache' );
|
||||||
|
$mode->setAccessible( true );
|
||||||
|
$mode->setValue( null, array( 'post' => 'aeav_only', 'user' => 'disabled', 'term' => 'disabled', 'comment' => 'disabled' ) );
|
||||||
|
|
||||||
|
WPDO_Entity_Registry::init();
|
||||||
|
WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() );
|
||||||
|
WPDO_Entity_Registry::register_group( 'post', 'scope_listing', array( array( 'key' => 'shared_key', 'type' => 'text' ) ), array( 'post_types' => array( 'hp_listing' ) ) );
|
||||||
|
WPDO_Entity_Registry::register_group( 'post', 'scope_vendor', array( array( 'key' => 'shared_key', 'type' => 'text' ) ), array( 'post_types' => array( 'hp_vendor' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function value_in( string $table, int $post_id ): ?string {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->get_var( $wpdb->prepare( "SELECT shared_key FROM `{$table}` WHERE post_id = %d", $post_id ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_each_post_type_writes_to_its_own_group_table(): void {
|
||||||
|
WPDO_Hook_Bus::intercept_update( null, 101, 'shared_key', 'listing value', '' );
|
||||||
|
WPDO_Hook_Bus::intercept_update( null, 202, 'shared_key', 'vendor value', '' );
|
||||||
|
|
||||||
|
$this->assertSame( 'listing value', $this->value_in( self::LISTING_TABLE, 101 ) );
|
||||||
|
$this->assertNull( $this->value_in( self::VENDOR_TABLE, 101 ) );
|
||||||
|
$this->assertSame( 'vendor value', $this->value_in( self::VENDOR_TABLE, 202 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reads_come_from_the_post_types_group_table(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->insert( self::LISTING_TABLE, array( 'post_id' => 101, 'shared_key' => 'from listing table' ) );
|
||||||
|
$wpdb->insert( self::VENDOR_TABLE, array( 'post_id' => 101, 'shared_key' => 'stray copy' ) );
|
||||||
|
|
||||||
|
$this->assertSame( array( 'from listing table' ), WPDO_Hook_Bus::intercept_get( null, 101, 'shared_key', true, 'post' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_other_post_types_fall_through_to_native_meta(): void {
|
||||||
|
$this->assertNull( WPDO_Hook_Bus::intercept_update( null, 303, 'shared_key', 'x', '' ) );
|
||||||
|
$this->assertNull( WPDO_Hook_Bus::intercept_get( null, 303, 'shared_key', true, 'post' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_without_the_option_routing_stays_legacy(): void {
|
||||||
|
$GLOBALS['_wp_options'][ WPDO_Entity_Registry::SCOPED_ROUTING_OPTION ] = '0';
|
||||||
|
|
||||||
|
WPDO_Hook_Bus::intercept_update( null, 101, 'shared_key', 'legacy', '' );
|
||||||
|
|
||||||
|
$this->assertSame( 'legacy', $this->value_in( self::VENDOR_TABLE, 101 ) );
|
||||||
|
$this->assertNull( $this->value_in( self::LISTING_TABLE, 101 ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post entity groups scoped to post types.
|
||||||
|
*
|
||||||
|
* The post entity used to keep one meta_key index for every post type, so a
|
||||||
|
* key shared by several groups (hp_rating in hp_listing_core and
|
||||||
|
* hp_vendor_core) was routed to whichever group registered last — a
|
||||||
|
* listing's hp_rating landed in the vendor table. Groups can now declare the
|
||||||
|
* post types they belong to, and lookups that pass a post type use it.
|
||||||
|
*/
|
||||||
|
class EntityRegistryScopeTest extends TestCase {
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
TMDO_Entity_Registry::init();
|
||||||
|
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||||
|
$GLOBALS['_wp_options'] = array( TMDO_Entity_Registry::SCOPED_ROUTING_OPTION => '1' );
|
||||||
|
|
||||||
|
TMDO_Entity_Registry::register_group( 'post', 'site_core', array( array( 'key' => '_edit_lock', 'type' => 'text' ) ) );
|
||||||
|
TMDO_Entity_Registry::register_group(
|
||||||
|
'post',
|
||||||
|
'listing_core',
|
||||||
|
array(
|
||||||
|
array( 'key' => 'rating', 'type' => 'decimal' ),
|
||||||
|
array( 'key' => 'price', 'type' => 'decimal' ),
|
||||||
|
),
|
||||||
|
array( 'post_types' => array( 'hp_listing' ) )
|
||||||
|
);
|
||||||
|
TMDO_Entity_Registry::register_group(
|
||||||
|
'post',
|
||||||
|
'vendor_core',
|
||||||
|
array( array( 'key' => 'rating', 'type' => 'decimal' ) ),
|
||||||
|
array( 'post_types' => array( 'hp_vendor' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_shared_key_resolves_to_the_group_of_the_post_type(): void {
|
||||||
|
$this->assertSame( 'listing_core', TMDO_Entity_Registry::get_field( 'post', 'rating', 'hp_listing' )['group'] );
|
||||||
|
$this->assertSame( 'vendor_core', TMDO_Entity_Registry::get_field( 'post', 'rating', 'hp_vendor' )['group'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scoped_key_is_unmanaged_for_other_post_types(): void {
|
||||||
|
$this->assertNull( TMDO_Entity_Registry::get_field( 'post', 'rating', 'post' ) );
|
||||||
|
$this->assertNull( TMDO_Entity_Registry::get_field( 'post', 'price', 'hp_vendor' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unscoped_group_applies_to_every_post_type(): void {
|
||||||
|
$this->assertSame( 'site_core', TMDO_Entity_Registry::get_field( 'post', '_edit_lock', 'hp_listing' )['group'] );
|
||||||
|
$this->assertSame( 'site_core', TMDO_Entity_Registry::get_field( 'post', '_edit_lock', 'page' )['group'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_lookup_without_post_type_keeps_legacy_last_wins(): void {
|
||||||
|
$this->assertSame( 'vendor_core', TMDO_Entity_Registry::get_field( 'post', 'rating' )['group'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scoping_is_off_until_the_option_enables_it(): void {
|
||||||
|
$GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] = '0';
|
||||||
|
|
||||||
|
$this->assertSame( 'vendor_core', TMDO_Entity_Registry::get_field( 'post', 'rating', 'hp_listing' )['group'] );
|
||||||
|
$this->assertSame( 'listing_core', TMDO_Entity_Registry::get_field( 'post', 'price', 'hp_vendor' )['group'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scope_is_not_part_of_the_field_definition(): void {
|
||||||
|
// Keeps schema hashes and PR-017 collision checks unchanged.
|
||||||
|
$fields = TMDO_Entity_Registry::get_group_fields( 'post', 'vendor_core' );
|
||||||
|
$this->assertArrayNotHasKey( 'post_types', $fields[0] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scoped_routing_default_waits_for_reconcile_on_sites_with_post_flat_data(): void {
|
||||||
|
// Existing flat rows may sit in the wrong group table (legacy routing);
|
||||||
|
// switching lookups before they are reconciled would hide them.
|
||||||
|
$this->assertSame( '0', TMDO_Installer::post_scoped_routing_default( 'aeav_only', true ) );
|
||||||
|
$this->assertSame( '0', TMDO_Installer::post_scoped_routing_default( 'dual_write', true ) );
|
||||||
|
$this->assertSame( '1', TMDO_Installer::post_scoped_routing_default( 'aeav_only', false ) );
|
||||||
|
$this->assertSame( '1', TMDO_Installer::post_scoped_routing_default( 'disabled', true ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_core_hivepress_groups_are_scoped_without_changing_their_hash(): void {
|
||||||
|
TMDO_Entity_Registry::init();
|
||||||
|
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||||
|
TMDO_Post_Fields::register_entity_fields();
|
||||||
|
|
||||||
|
$persisted = array(
|
||||||
|
'hp_listing_core' => 'fb5ab760',
|
||||||
|
'hp_request_core' => '27d6f4f5',
|
||||||
|
'hp_vendor_core' => '5366e5c8',
|
||||||
|
);
|
||||||
|
foreach ( $persisted as $group => $hash ) {
|
||||||
|
$fields = TMDO_Entity_Registry::get_group_fields( 'post', $group );
|
||||||
|
$this->assertSame( $hash, substr( TMDO_Schema_Manager::calculate_schema_hash( $fields ), 0, 8 ), $group );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertSame( 'hp_listing_core', TMDO_Entity_Registry::get_field( 'post', 'hp_rating', 'hp_listing' )['group'] );
|
||||||
|
$this->assertSame( 'hp_listing_core', TMDO_Entity_Registry::get_field( 'post', 'hp_status', 'hp_listing' )['group'] );
|
||||||
|
$this->assertSame( 'hp_request_core', TMDO_Entity_Registry::get_field( 'post', 'hp_status', 'hp_request' )['group'] );
|
||||||
|
$this->assertSame( 'hp_vendor_core', TMDO_Entity_Registry::get_field( 'post', 'hp_user', 'hp_vendor' )['group'] );
|
||||||
|
$this->assertSame( 'hp_request_core', TMDO_Entity_Registry::get_field( 'post', 'hp_user', 'hp_request' )['group'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user