Files
2meet-data-optimizer-woocom…/includes/class-tmdo-woocommerce.php
T
wpdev c3c4e1de44 refactor(2meet-data-optimizer-woocommerce-addon): drop the product/order hot fields and the user hot group
- Stop registering WC product/order/subscription zone fields and the user hot group; WC reads its meta directly and writes stock by SQL, so a flat copy drifts
- Keep the custom tables registration, the HPOS notice and the commissions
- Verify: composer test / test:integration; dev36 customer save, order stock reduce/restore with no DB errors
- PR: PR-085
2026-09-27 09:48:33 +08:00

369 lines
12 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_WooCommerce — integration with WooCommerce + WooCommerce Subscriptions.
*
* Strategy: WC core has its OWN anti-EAV story (HPOS for orders;
* `wp_wc_product_meta_lookup` for product search). WPDO's job is to:
* 1. Register the 20 `wp_wc_*` tables to Custom_Table_Registry
* (so doctor / conflict-scan know about them)
* 2. Detect HPOS state and surface it to admin
* 3. NOT duplicate what WC already does (no shadow lookup tables, and since
* 0.2.0 no flat copy of product / order / customer meta)
*
* This is "Tier 5" in ENTITY_ADAPTER_COOKBOOK.md: integrating with a plugin
* that has its own anti-EAV solution.
*
* @package TMDO
* @since 2.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WooCommerce integration. No-op when WC not active.
*/
final class TMDO_WooCommerce {
/**
* Provider key for registry.
*/
public const PROVIDER = 'woocommerce';
/**
* All 20 wp_wc_* tables WC core creates. Keyed by suffix (no prefix).
*
* @var array<string, array{primary_key: string, post_type_link: ?string, description: string, since: string}>
*/
private const WC_CORE_TABLES = array(
'wc_admin_note_actions' => array(
'primary_key' => 'action_id',
'post_type_link' => null,
'description' => 'WC admin notes — action buttons',
'since' => 'WC 4.0',
),
'wc_admin_notes' => array(
'primary_key' => 'note_id',
'post_type_link' => null,
'description' => 'WC admin notes',
'since' => 'WC 4.0',
),
'wc_category_lookup' => array(
'primary_key' => 'category_tree_id',
'post_type_link' => null,
'description' => 'Product category hierarchy lookup',
'since' => 'WC 4.6',
),
'wc_customer_lookup' => array(
'primary_key' => 'customer_id',
'post_type_link' => null,
'description' => 'Customer aggregate (anti-EAV by WC core)',
'since' => 'WC 4.0',
),
'wc_download_log' => array(
'primary_key' => 'download_log_id',
'post_type_link' => null,
'description' => 'Downloadable product access log',
'since' => 'WC 3.3',
),
'wc_order_addresses' => array(
'primary_key' => 'id',
'post_type_link' => 'shop_order',
'description' => 'Order billing/shipping addresses (HPOS)',
'since' => 'WC 8.2',
),
'wc_order_coupon_lookup' => array(
'primary_key' => 'order_id',
'post_type_link' => 'shop_order',
'description' => 'Order coupon analytics lookup',
'since' => 'WC 4.0',
),
'wc_order_operational_data' => array(
'primary_key' => 'id',
'post_type_link' => 'shop_order',
'description' => 'Order operational data (HPOS)',
'since' => 'WC 8.2',
),
'wc_order_product_lookup' => array(
'primary_key' => 'order_item_id',
'post_type_link' => 'shop_order',
'description' => 'Order product analytics lookup',
'since' => 'WC 4.0',
),
'wc_order_stats' => array(
'primary_key' => 'order_id',
'post_type_link' => 'shop_order',
'description' => 'Order statistics (anti-EAV by WC core)',
'since' => 'WC 4.0',
),
'wc_order_tax_lookup' => array(
'primary_key' => null,
'post_type_link' => 'shop_order',
'description' => 'Order tax analytics lookup',
'since' => 'WC 4.0',
),
'wc_orders' => array(
'primary_key' => 'id',
'post_type_link' => 'shop_order',
'description' => 'Orders main table (HPOS)',
'since' => 'WC 8.2',
),
'wc_orders_meta' => array(
'primary_key' => 'id',
'post_type_link' => 'shop_order',
'description' => 'Order meta (HPOS replacement for postmeta)',
'since' => 'WC 8.2',
),
'wc_product_attributes_lookup' => array(
'primary_key' => null,
'post_type_link' => 'product',
'description' => 'Product attributes lookup (faceted search)',
'since' => 'WC 4.9',
),
'wc_product_download_directories' => array(
'primary_key' => 'url_id',
'post_type_link' => null,
'description' => 'Approved download directories',
'since' => 'WC 6.5',
),
'wc_product_meta_lookup' => array(
'primary_key' => 'product_id',
'post_type_link' => 'product',
'description' => 'Product meta lookup (anti-EAV by WC core)',
'since' => 'WC 3.6',
),
'wc_rate_limits' => array(
'primary_key' => 'rate_limit_id',
'post_type_link' => null,
'description' => 'API rate limits',
'since' => 'WC 5.6',
),
'wc_reserved_stock' => array(
'primary_key' => null,
'post_type_link' => 'product',
'description' => 'Reserved stock (cart hold)',
'since' => 'WC 4.5',
),
'wc_tax_rate_classes' => array(
'primary_key' => 'tax_rate_class_id',
'post_type_link' => null,
'description' => 'Tax rate classes',
'since' => 'WC 3.7',
),
'wc_webhooks' => array(
'primary_key' => 'webhook_id',
'post_type_link' => null,
'description' => 'Webhooks subscriptions',
'since' => 'WC 2.6',
),
);
/**
* Wire integration. No-op when WC not detected.
*
* @return void
*/
public static function register(): void {
if ( ! self::is_active() ) {
return;
}
add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ) );
// 0.2.0: no product / order / subscription zone fields and no user hot group.
// WooCommerce reads its meta directly and writes stock by SQL, so a flat
// copy drifts; its meta stays native only (core 1.2.0 dropped wc_product too).
// HPOS recommendation notice — only on WC admin pages, only when:
// (a) HPOS is OFF, (b) the threshold of legacy orders has been crossed.
if ( is_admin() ) {
add_action( 'admin_notices', array( __CLASS__, 'maybe_render_hpos_notice' ) );
}
}
/**
* Threshold of legacy `shop_order` posts above which we recommend HPOS.
* Below this, the HPOS migration overhead isn't justified.
*/
private const HPOS_RECOMMEND_THRESHOLD = 100;
/**
* Option name used to permanently dismiss the notice.
*/
private const HPOS_NOTICE_DISMISSED_OPT = 'wpdo_hpos_notice_dismissed';
/**
* Conditionally render the HPOS recommendation admin notice.
*
* @return void
*/
public static function maybe_render_hpos_notice(): void {
// Already enabled or already dismissed.
if ( self::is_hpos_enabled() ) {
return;
}
if ( '1' === (string) get_option( self::HPOS_NOTICE_DISMISSED_OPT, '' ) ) {
return;
}
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return;
}
// Handle dismiss action (lightweight, no separate AJAX endpoint).
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only nonce check below.
if ( ! empty( $_GET['wpdo_hpos_dismiss'] ) ) {
$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ) : '';
if ( wp_verify_nonce( $nonce, 'wpdo_hpos_dismiss' ) ) {
update_option( self::HPOS_NOTICE_DISMISSED_OPT, '1', false );
return;
}
}
$legacy_count = self::count_legacy_orders();
if ( $legacy_count < self::HPOS_RECOMMEND_THRESHOLD ) {
return;
}
$dismiss_url = wp_nonce_url(
add_query_arg( 'wpdo_hpos_dismiss', '1' ),
'wpdo_hpos_dismiss'
);
$settings_url = admin_url( 'admin.php?page=wc-settings&tab=advanced&section=features' );
?>
<div class="notice notice-warning is-dismissible">
<p>
<strong>WP Data Optimizer:</strong>
<?php
printf(
/* translators: 1: legacy order count, 2: HPOS settings URL, 3: dismiss URL */
esc_html__( '偵測到 %1$d 筆 legacy shop_order(postmeta-based)。建議啟用 WooCommerce High Performance Order Storage (HPOS) 加速 order 查詢。前往 %2$s 啟用,或 %3$s 永久隱藏此通知。', '2meet-data-optimizer' ),
(int) $legacy_count,
'<a href="' . esc_url( $settings_url ) . '">WooCommerce → 進階 → 功能</a>',
'<a href="' . esc_url( $dismiss_url ) . '">關閉</a>'
);
?>
</p>
</div>
<?php
}
/**
* Count legacy `shop_order` posts (excluding HPOS table).
*
* @return int
*/
private static function count_legacy_orders(): int {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'shop_order'" );
}
/**
* Active when WC class loaded OR WC plugin file exists (loose detect for early bootstrap).
*
* @return bool
*/
public static function is_active(): bool {
return class_exists( 'WooCommerce' )
|| ( defined( 'WC_VERSION' ) && WC_VERSION )
|| file_exists( WP_PLUGIN_DIR . '/woocommerce/woocommerce.php' );
}
/**
* HPOS (custom orders table) detection.
*
* @return bool True when HPOS is enabled.
*/
public static function is_hpos_enabled(): bool {
// WC OrderUtil class — modern WC API.
if ( class_exists( 'Automattic\\WooCommerce\\Utilities\\OrderUtil' )
&& method_exists( 'Automattic\\WooCommerce\\Utilities\\OrderUtil', 'custom_orders_table_usage_is_enabled' )
) {
return (bool) Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
}
// Fallback: option check.
return 'yes' === get_option( 'woocommerce_custom_orders_table_enabled' );
}
/**
* Detected WC version (or 'unknown').
*
* @return string
*/
public static function wc_version(): string {
if ( defined( 'WC_VERSION' ) ) {
return WC_VERSION;
}
return 'unknown';
}
/**
* Register all 20 wp_wc_* tables.
*
* @param mixed $registry TMDO_Custom_Table_Registry (passed by action).
* @return void
*/
public static function register_custom_tables( $registry = null ): void {
if ( ! class_exists( 'TMDO_Custom_Table_Registry' ) ) {
return;
}
$registry = $registry ?: TMDO_Custom_Table_Registry::instance();
foreach ( self::WC_CORE_TABLES as $name => $meta ) {
$registry->register(
self::PROVIDER,
array(
'table_name' => $name,
'primary_key' => $meta['primary_key'] ?: 'id',
'post_type_link' => $meta['post_type_link'],
'doctor_callback' => array( __CLASS__, 'doctor_check' ),
'description' => $meta['description'] . ' (' . $meta['since'] . ')',
)
);
}
// Our own commission tracking table (v2.1.0).
$registry->register(
self::PROVIDER,
array(
'table_name' => 'wpdo_wc_commissions',
'primary_key' => 'id',
'post_type_link' => null,
'doctor_callback' => array( __CLASS__, 'doctor_check' ),
'description' => 'Vendor commission tracking (replaces hpct_wc_orders, HPOS-aware) (WPDO 2.1.0)',
)
);
}
/**
* WPDO doctor probe for any wc_* table — verifies existence + reports row count.
*
* @param string $table_suffix Suffix without prefix.
* @return array{ok:bool, message:string}
*/
public static function doctor_check( string $table_suffix ): array {
global $wpdb;
$full = $wpdb->prefix . $table_suffix;
$exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->prepare( 'SHOW TABLES LIKE %s', $full )
);
if ( ! $exists ) {
$is_hpos_table = in_array( $table_suffix, array( 'wc_orders', 'wc_orders_meta', 'wc_order_addresses', 'wc_order_operational_data' ), true );
$msg = $is_hpos_table
? "{$full} 不存在 — HPOS 尚未啟用或 WC 版本 < 8.2。"
: "{$full} 不存在 — 請確認 WooCommerce 已啟用。";
return array(
'ok' => false,
'message' => $msg,
);
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$full}`" );
return array(
'ok' => true,
'message' => "{$full}: {$count} rows",
);
}
}