hardening 2.0: 2meet-data-optimizer (staged rollout, dev22 first) #3
@@ -286,6 +286,8 @@ if ( is_admin() ) {
|
|||||||
TMDO_Setup_Wizard::register();
|
TMDO_Setup_Wizard::register();
|
||||||
require_once TMDO_PATH . 'admin/class-tmdo-dashboard-widget.php';
|
require_once TMDO_PATH . 'admin/class-tmdo-dashboard-widget.php';
|
||||||
TMDO_Dashboard_Widget::register();
|
TMDO_Dashboard_Widget::register();
|
||||||
|
require_once TMDO_PATH . 'admin/class-tmdo-deprecated-addons-notice.php';
|
||||||
|
TMDO_Deprecated_Addons_Notice::register();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── WP-CLI ─────────────────────────────────────────────────────────────────
|
// ── WP-CLI ─────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin notice for the deprecated in-house partner AddOns.
|
||||||
|
*
|
||||||
|
* The AddOns for 2meet's own plugins (bookings, collab, hub, infocards,
|
||||||
|
* mobile-bridge, playlist, quotation, spoke) only registered tables and fields
|
||||||
|
* on the partner's behalf. Partner plugins now register themselves with the
|
||||||
|
* TMDO_Anti_EAV_Aware trait (docs/PARTNER-REGISTRATION.md), so these AddOns
|
||||||
|
* are deprecated and will be removed.
|
||||||
|
*
|
||||||
|
* @package WP_Data_Optimizer
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warns admins who still run one of the deprecated partner AddOns.
|
||||||
|
*/
|
||||||
|
final class TMDO_Deprecated_Addons_Notice {
|
||||||
|
|
||||||
|
/** In-house plugins whose AddOn is replaced by partner self-registration. */
|
||||||
|
private const SLUGS = array( 'bookings', 'collab', 'hub', 'infocards', 'mobile-bridge', 'playlist', 'quotation', 'spoke' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook the notice.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register(): void {
|
||||||
|
add_action( 'admin_notices', array( __CLASS__, 'render' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slugs of the deprecated AddOns active on this site (or network-wide).
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function active(): array {
|
||||||
|
$active = (array) get_option( 'active_plugins', array() );
|
||||||
|
if ( is_multisite() ) {
|
||||||
|
$active = array_merge( $active, array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) ) );
|
||||||
|
}
|
||||||
|
return array_values(
|
||||||
|
array_filter(
|
||||||
|
self::SLUGS,
|
||||||
|
static fn( string $slug ): bool => in_array( "2meet-data-optimizer-{$slug}-addon/2meet-data-optimizer-{$slug}-addon.php", $active, true )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the notice for administrators when any deprecated AddOn is active.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render(): void {
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$active = self::active();
|
||||||
|
if ( empty( $active ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$names = implode( ', ', array_map( static fn( string $slug ): string => "2meet-data-optimizer-{$slug}-addon", $active ) );
|
||||||
|
printf(
|
||||||
|
'<div class="notice notice-warning"><p><strong>%s</strong> %s</p><p>%s</p></div>',
|
||||||
|
esc_html__( '2meet Data Optimizer:', '2meet-data-optimizer' ),
|
||||||
|
esc_html(
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: comma-separated AddOn plugin slugs */
|
||||||
|
__( '以下 AddOn 已淘汰:%s。對應的 2meet 外掛改為自行向核心註冊資料表與欄位,AddOn 之後會移除。', '2meet-data-optimizer' ),
|
||||||
|
$names
|
||||||
|
)
|
||||||
|
),
|
||||||
|
esc_html__( '改寫方式見 2meet-data-optimizer 外掛內的 docs/PARTNER-REGISTRATION.md;partner 外掛完成自行註冊後即可停用對應 AddOn。', '2meet-data-optimizer' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# Integration Pattern Decision — 2026-04-25
|
# Integration Pattern Decision — 2026-04-25
|
||||||
|
|
||||||
|
> **2026-09 更新:** 2meet 自家外掛回到「partner 自行註冊」,做法見 `PARTNER-REGISTRATION.md`;對應的 8 個 AddOn 已淘汰。
|
||||||
|
|
||||||
> **v1.0.0 後記(2026-07-31):本文結論已被架構拆分取代。**
|
> **v1.0.0 後記(2026-07-31):本文結論已被架構拆分取代。**
|
||||||
> 當時的兩個選項是「集中在核心」vs「各外掛自帶 bridge」。v1.0.0 走的是第三條路:
|
> 當時的兩個選項是「集中在核心」vs「各外掛自帶 bridge」。v1.0.0 走的是第三條路:
|
||||||
> 每個夥伴外掛對應**一個獨立 AddOn 外掛**(`2meet-data-optimizer-<partner>-addon`,共 11 個),
|
> 每個夥伴外掛對應**一個獨立 AddOn 外掛**(`2meet-data-optimizer-<partner>-addon`,共 11 個),
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Partner 外掛自行註冊指南
|
||||||
|
|
||||||
|
> 適用:2meet 自家外掛(bookings、collab、hub、infocards、mobile-bridge、playlist、quotation、spoke)。
|
||||||
|
> 這 8 個外掛原本各有一個 `2meet-data-optimizer-<slug>-addon`,替它們向核心註冊資料表和欄位。
|
||||||
|
> 這些 AddOn 已淘汰:資料結構的定義應該和資料的擁有者放在一起,所以改由 partner 外掛自己註冊。
|
||||||
|
> 啟用中的 AddOn 會在後台顯示淘汰通知(`TMDO_Deprecated_Addons_Notice`)。
|
||||||
|
>
|
||||||
|
> HivePress、WooCommerce、LatePoint 是第三方外掛,仍然保留各自的 AddOn。
|
||||||
|
|
||||||
|
## 1. 掛上 trait
|
||||||
|
|
||||||
|
核心提供 `TMDO_Anti_EAV_Aware` trait(`includes/trait-tmdo-anti-eav-aware.php`)。trait 由核心載入,所以只有在核心存在時才宣告 bridge 類別,把它放在獨立檔案:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 2meet-bookings.php(partner 主檔)
|
||||||
|
add_action(
|
||||||
|
'plugins_loaded',
|
||||||
|
static function () {
|
||||||
|
if ( ! trait_exists( 'TMDO_Anti_EAV_Aware' ) ) {
|
||||||
|
return; // 核心未啟用:什麼都不做。
|
||||||
|
}
|
||||||
|
require_once __DIR__ . '/includes/class-tmb-tmdo-bridge.php';
|
||||||
|
( new TMB_TMDO_Bridge() )->boot();
|
||||||
|
},
|
||||||
|
3
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
// includes/class-tmb-tmdo-bridge.php
|
||||||
|
final class TMB_TMDO_Bridge {
|
||||||
|
use TMDO_Anti_EAV_Aware;
|
||||||
|
|
||||||
|
protected function plugin_slug(): string {
|
||||||
|
return '2meet-bookings';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void {
|
||||||
|
$this->bind_anti_eav_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆寫需要的 on_register_*(),見下面兩節。
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**時機**:核心在 `plugins_loaded:4` 觸發 `wpdo_register_*`,並在 `init:1` 再觸發一次。
|
||||||
|
建議掛在 `plugins_loaded:3`,這樣第一次觸發就能收到你的註冊,`init` 之前的讀寫也會被正確路由。
|
||||||
|
掛在 `:5` 之後也能運作(會在 `init:1` 補上),只是 `init` 之前的請求看不到你的欄位。
|
||||||
|
|
||||||
|
**名稱**:請用 `TMDO_Anti_EAV_Aware`。`WPDO_Anti_EAV_Aware` 是舊的 wp-data-optimizer 3.x 的 trait,由 mu-plugin `wpdo-policy-enforcer.php` 從已退役的路徑載入,沒有後面提到的 `post_types` 參數。
|
||||||
|
|
||||||
|
## 2. 註冊自有資料表
|
||||||
|
|
||||||
|
讓 `wp wpdo doctor` / benchmark 認得你的資料表,並做 schema 漂移檢查。以 bookings 為例:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function on_register_custom_tables( TMDO_Custom_Table_Registry $r ): void {
|
||||||
|
$this->register_custom_table(
|
||||||
|
$r,
|
||||||
|
array(
|
||||||
|
'table_name' => '2mb_services', // 不含 $wpdb->prefix
|
||||||
|
'primary_key' => 'id',
|
||||||
|
'post_type_link' => null, // 若每列對應一個 post,填 post_type(playlist 用 'hp_vendor')
|
||||||
|
'expected_columns' => array(
|
||||||
|
'id' => 'BIGINT',
|
||||||
|
'vendor_id' => 'BIGINT',
|
||||||
|
'title' => 'VARCHAR',
|
||||||
|
// …照 CREATE TABLE 逐欄列出
|
||||||
|
),
|
||||||
|
'indexes' => array(
|
||||||
|
'vendor_id' => array( 'vendor_id' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
collab、playlist、quotation、mobile-bridge 的 AddOn 只做這件事:把各自 `includes/class-tmdo-<slug>.php` 裡 `register_custom_tables()` 的 `$tables` 陣列**原樣**搬進 partner 的 bridge 即可。
|
||||||
|
|
||||||
|
## 3. 註冊 entity 欄位群組(選用)
|
||||||
|
|
||||||
|
只有要把 meta 從 `wp_postmeta` / `wp_usermeta` 等 EAV 表移到扁平表時才需要:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function on_register_entity_fields(): void {
|
||||||
|
$this->register_entity_fields(
|
||||||
|
'post',
|
||||||
|
'tmb_service_core',
|
||||||
|
array(
|
||||||
|
array( 'key' => 'tmb_price', 'type' => 'decimal', 'searchable' => true ),
|
||||||
|
array( 'key' => 'tmb_status', 'type' => 'enum', 'options' => array( 'open', 'closed' ) ),
|
||||||
|
),
|
||||||
|
array( 'post_types' => array( 'tmb_service' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`post_types`(post 群組建議必填)**:群組只套用在這些 post type。多個群組共用同一個 meta key 時,會依 post 本身的類型決定寫到哪張表;不屬於這些 post type 的 post,這個 key 不受管理(照舊寫原生 meta)。沒有宣告時,共用 key 會被「最後註冊的群組」整個接走。
|
||||||
|
- **定義一旦上線就不要改**:欄位定義的 hash 存在 `wpdo_registry_meta`,改了會對已有資料的扁平表執行 ALTER。要新增欄位時,開新群組。
|
||||||
|
- **群組名稱不可和別人重複**:同名但定義不同時,後註冊的會被忽略,並在 Site Health、後台 Doctor 分頁和 `wp wpdo doctor` 顯示 `[COLLISION]`。
|
||||||
|
|
||||||
|
## 4. 從 AddOn 遷移
|
||||||
|
|
||||||
|
1. 在 partner 的 bridge 裡註冊和 AddOn **完全相同**的資料表與群組(直接複製 AddOn 的定義)。
|
||||||
|
2. 部署 partner 新版,AddOn 先保持啟用。`wp wpdo doctor` 不應出現 `[COLLISION]`;兩邊定義相同時,重複註冊會被靜默忽略。
|
||||||
|
3. 停用 AddOn。
|
||||||
|
4. 再跑一次 `wp wpdo doctor`:資料表仍在清單中,而且不應出現 `[ORPHAN]`。如果出現,表示有群組沒有被 partner 接手;該實體是 aeav_only 時,核心會依 `wpdo_registry_meta` 暫時還原它,讓資料可以讀取,但仍應補上註冊。
|
||||||
|
5. 確認沒有問題後,刪除 AddOn 外掛。
|
||||||
|
|
||||||
|
hub、infocards、spoke 的 partner 外掛本身已經會自己註冊,AddOn 只是重複註冊;停用前照第 2 步確認沒有 `[COLLISION]` 即可。
|
||||||
|
|
||||||
|
## 相關文件
|
||||||
|
|
||||||
|
- `ENTITY_ADAPTER_COOKBOOK.md`:整合深度 Tier 1–3(`TMDO_API` facade、Schema Registry、自訂 adapter)。
|
||||||
|
- `INTEGRATION_PATTERN_DECISION.md`:2026-04 的「partner 自帶 bridge」決策紀錄;本指南回到這個做法。
|
||||||
@@ -122,12 +122,14 @@ if ( ! trait_exists( 'TMDO_Anti_EAV_Aware' ) ) {
|
|||||||
* @param string $entity_type 'post' | 'user' | 'term' | 'comment'.
|
* @param string $entity_type 'post' | 'user' | 'term' | 'comment'.
|
||||||
* @param string $group_name Logical group name (e.g. 'tmos_vendor', 'tmos_audience').
|
* @param string $group_name Logical group name (e.g. 'tmos_vendor', 'tmos_audience').
|
||||||
* @param array<int,array<string,mixed>> $fields Field definitions.
|
* @param array<int,array<string,mixed>> $fields Field definitions.
|
||||||
|
* @param array<string,mixed> $args Optional: post_types (string[]) scopes a post group
|
||||||
|
* to those post types (see TMDO_Entity_Registry::register_group()).
|
||||||
*/
|
*/
|
||||||
protected function register_entity_fields( string $entity_type, string $group_name, array $fields ): bool {
|
protected function register_entity_fields( string $entity_type, string $group_name, array $fields, array $args = array() ): bool {
|
||||||
if ( ! class_exists( 'TMDO_Entity_Registry' ) ) {
|
if ( ! class_exists( 'TMDO_Entity_Registry' ) ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return TMDO_Entity_Registry::register_group( $entity_type, $group_name, $fields );
|
return TMDO_Entity_Registry::register_group( $entity_type, $group_name, $fields, $args );
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Read/write convenience wrappers ──────────────────────────────────────
|
// ── Read/write convenience wrappers ──────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 8 AddOns for in-house 2meet plugins are deprecated: each partner plugin
|
||||||
|
* registers its own tables and fields with the TMDO_Anti_EAV_Aware trait
|
||||||
|
* (docs/PARTNER-REGISTRATION.md). Admins running one get told so.
|
||||||
|
*/
|
||||||
|
class DeprecatedAddonsNoticeTest extends TestCase {
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
if ( ! class_exists( 'TMDO_Deprecated_Addons_Notice' ) ) {
|
||||||
|
require_once TMDO_PATH . 'admin/class-tmdo-deprecated-addons-notice.php';
|
||||||
|
}
|
||||||
|
$GLOBALS['_wp_options'] = array();
|
||||||
|
$GLOBALS['_wp_current_user_can'] = array( 'manage_options' => true );
|
||||||
|
$GLOBALS['_wp_is_multisite'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function activate( string ...$slugs ): void {
|
||||||
|
$GLOBALS['_wp_options']['active_plugins'] = array_map(
|
||||||
|
static fn( $s ) => "2meet-data-optimizer-{$s}-addon/2meet-data-optimizer-{$s}-addon.php",
|
||||||
|
$slugs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_lists_only_active_deprecated_addons(): void {
|
||||||
|
$this->activate( 'spoke', 'hivepress', 'bookings' );
|
||||||
|
|
||||||
|
$this->assertSame( array( 'bookings', 'spoke' ), TMDO_Deprecated_Addons_Notice::active() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_notice_names_the_addons_and_points_to_the_guide(): void {
|
||||||
|
$this->activate( 'spoke' );
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
TMDO_Deprecated_Addons_Notice::render();
|
||||||
|
$html = (string) ob_get_clean();
|
||||||
|
|
||||||
|
$this->assertStringContainsString( '2meet-data-optimizer-spoke-addon', $html );
|
||||||
|
$this->assertStringContainsString( 'PARTNER-REGISTRATION.md', $html );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_no_notice_when_none_active_or_user_cannot_manage(): void {
|
||||||
|
ob_start();
|
||||||
|
TMDO_Deprecated_Addons_Notice::render();
|
||||||
|
$this->assertSame( '', ob_get_clean() );
|
||||||
|
|
||||||
|
$this->activate( 'spoke' );
|
||||||
|
$GLOBALS['_wp_current_user_can'] = array();
|
||||||
|
ob_start();
|
||||||
|
TMDO_Deprecated_Addons_Notice::render();
|
||||||
|
$this->assertSame( '', ob_get_clean() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_trait_passes_post_type_scope_to_the_registry(): void {
|
||||||
|
TMDO_Entity_Registry::init();
|
||||||
|
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||||
|
$partner = new class() {
|
||||||
|
use TMDO_Anti_EAV_Aware;
|
||||||
|
|
||||||
|
public function run(): bool {
|
||||||
|
return $this->register_entity_fields( 'post', 'x_item', array( array( 'key' => 'x_price', 'type' => 'decimal' ) ), array( 'post_types' => array( 'x_item' ) ) );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->assertTrue( $partner->run() );
|
||||||
|
$this->assertSame( array( 'x_item' ), TMDO_Entity_Registry::get_group_post_types( 'post', 'x_item' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user