Files
2meet-data-optimizer/includes/engine/class-tmdo-promotion-guard.php
T
wpdev 79a6c4d724 fix(2meet-data-optimizer): refuse aeav_only and cleanup on HivePress / WooCommerce sites
- Add TMDO_Promotion_Guard: HivePress models read every meta key with an
  empty-key get_*_meta() and WooCommerce does the same for products (and
  writes stock/sales with direct SQL), so on those sites native meta must
  stay authoritative. Promotion to aeav_only is refused in
  TMDO_Mode_Manager::set() (covers REST, CLI, phases, auto-promoter) for
  post/user/term/comment with HivePress and post/user with WooCommerce;
  TMDO_Post_Migration::cleanup() and the cleanup phase refuse too.
  Demotion stays open; filter tmdo_promotion_guard_allow overrides.
- Admin notice lists guarded entities already in aeav_only with the
  restore-native command (no forced demotion on read).
- Verify: PromotionGuardTest 8 OK; unit 668 OK, integration 480 OK,
  phpcs (phpcs.xml) 0, stan OK; dev36 live: 4 promotions + post-cleanup
  refused, wpdo_bridge_modes unchanged
- PR: PR-053
2026-09-26 18:29:04 +08:00

141 lines
4.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* TMDO_Promotion_Guard — keeps native meta authoritative on HivePress /
* WooCommerce sites (ADR-004 "search-index mode").
*
* Both plugins read native meta without going through a keyed
* get_*_meta() call: HivePress models load every key with get_*_meta( $id ),
* WooCommerce does the same for products and writes stock / sales with direct
* SQL. The Hook Bus cannot serve those reads from flat tables, so on these
* sites an entity must never reach aeav_only and native rows must never be
* cleaned up. Demotion always stays open.
*
* @package WP_Data_Optimizer
*/
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Refuses aeav_only promotion and native-meta cleanup where an integration
* reads native meta directly.
*/
final class TMDO_Promotion_Guard {
/**
* Entity types each integration reads natively.
*
* HivePress models (Post, User, Term, Comment) all load meta with an empty
* key; WooCommerce does so for products and customers.
*/
private const GUARDED = array(
'HivePress' => array( 'post', 'user', 'term', 'comment' ),
'WooCommerce' => array( 'post', 'user' ),
);
/**
* Check a mode change. Only promotion to aeav_only is guarded.
*
* @param string $entity_type Entity type.
* @param string $new_mode Target mode.
* @return WP_Error|null Error when refused.
*/
public static function check_promotion( string $entity_type, string $new_mode ): ?WP_Error {
if ( TMDO_Mode_Manager::MODE_AEAV_ONLY !== $new_mode ) {
return null;
}
return self::refuse( $entity_type, TMDO_Mode_Manager::MODE_AEAV_ONLY );
}
/**
* Check a cleanup of the entity's native managed meta rows.
*
* @param string $entity_type Entity type.
* @return WP_Error|null Error when refused.
*/
public static function check_cleanup( string $entity_type ): ?WP_Error {
return self::refuse( $entity_type, 'cleanup' );
}
/**
* Guarded entity types that are already in aeav_only (promoted before the
* guard existed). Native meta may be stale there.
*
* @return array<string, string[]> Entity type → integration names.
*/
public static function violations(): array {
$out = array();
foreach ( TMDO_Mode_Manager::all() as $type => $mode ) {
if ( TMDO_Mode_Manager::MODE_AEAV_ONLY !== $mode ) {
continue;
}
$integrations = self::integrations_for( $type );
if ( ! empty( $integrations ) ) {
$out[ $type ] = $integrations;
}
}
return $out;
}
/**
* Active integrations that read this entity type natively.
*
* @param string $entity_type Entity type.
* @return string[]
*/
private static function integrations_for( string $entity_type ): array {
$active = array(
'HivePress' => TMDO_Compatibility::is_hivepress_active(),
'WooCommerce' => TMDO_Compatibility::is_woocommerce_active(),
);
$names = array();
foreach ( self::GUARDED as $name => $types ) {
if ( $active[ $name ] && in_array( $entity_type, $types, true ) ) {
$names[] = $name;
}
}
return $names;
}
/**
* Build the refusal, unless no integration applies or a filter allows it.
*
* @param string $entity_type Entity type.
* @param string $action 'aeav_only' or 'cleanup'.
* @return WP_Error|null
*/
private static function refuse( string $entity_type, string $action ): ?WP_Error {
$integrations = self::integrations_for( $entity_type );
if ( empty( $integrations ) ) {
return null;
}
/**
* Allow a guarded aeav_only promotion or cleanup anyway.
*
* @param bool $allow Default false.
* @param string $entity_type Entity type.
* @param string $action 'aeav_only' or 'cleanup'.
* @param string[] $integrations Integrations that read this entity natively.
*/
if ( apply_filters( 'tmdo_promotion_guard_allow', false, $entity_type, $action, $integrations ) ) {
return null;
}
return new WP_Error(
'promotion_guarded',
sprintf(
/* translators: 1: integration names, 2: entity type, 3: aeav_only or cleanup */
__( '%1$s 直接讀取原生 meta,%2$s 不能執行 %3$s:這類站台以原生 meta 為唯一資料來源(搜尋索引模式)。', '2meet-data-optimizer' ),
implode( ' / ', $integrations ),
$entity_type,
$action
)
);
}
}