|
|
|
@@ -6,10 +6,9 @@
|
|
|
|
|
* `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. Add hot-zone fields to Schema_Registry for the legacy postmeta keys
|
|
|
|
|
* WC still uses on products / orders (when HPOS off)
|
|
|
|
|
* 3. Detect HPOS state and surface it to admin
|
|
|
|
|
* 4. NOT duplicate what WC already does (no shadow lookup tables)
|
|
|
|
|
* 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.
|
|
|
|
@@ -171,11 +170,9 @@ final class TMDO_WooCommerce {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ) );
|
|
|
|
|
add_action( 'wpdo_register_fields', array( __CLASS__, 'register_schema_fields' ) );
|
|
|
|
|
// v2.1.6: customer hot fields (_money_spent / _order_count / _last_order) move
|
|
|
|
|
// to Entity_Registry so Hook Bus can route them to wp_wpdo_user_hot once
|
|
|
|
|
// `entity_user` mode advances past `disabled`. See PLAN-entity-user-cutover.md.
|
|
|
|
|
add_action( 'wpdo_register_entity_fields', array( __CLASS__, 'register_user_entity_fields' ) );
|
|
|
|
|
// 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.
|
|
|
|
@@ -339,328 +336,6 @@ final class TMDO_WooCommerce {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Register hot-zone meta fields for products + orders + customers.
|
|
|
|
|
*
|
|
|
|
|
* Delegates to dedicated registrars to keep this method readable.
|
|
|
|
|
*
|
|
|
|
|
* @param mixed $schema TMDO_Schema_Registry (passed by action).
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public static function register_schema_fields( $schema = null ): void {
|
|
|
|
|
if ( ! class_exists( 'TMDO_Schema_Registry' ) ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$schema = $schema ?: TMDO_Schema_Registry::instance();
|
|
|
|
|
|
|
|
|
|
self::register_product_fields( $schema );
|
|
|
|
|
|
|
|
|
|
// Order fields only matter when HPOS is OFF (otherwise WC stores them in wc_orders_meta).
|
|
|
|
|
if ( ! self::is_hpos_enabled() ) {
|
|
|
|
|
self::register_order_fields( $schema );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// v2.1.6: Customer usermeta hot fields moved out of Schema_Registry — they
|
|
|
|
|
// are now registered via `register_user_entity_fields()` on the
|
|
|
|
|
// `wpdo_register_entity_fields` action so Hook Bus can intercept them.
|
|
|
|
|
// Schema_Registry never had a user-meta interceptor; the previous
|
|
|
|
|
// registration was inert. See PLAN-entity-user-cutover.md.
|
|
|
|
|
|
|
|
|
|
// Subscription fields if WC Subscriptions OR wc-linepay-subscription is active.
|
|
|
|
|
if ( self::has_subscriptions() ) {
|
|
|
|
|
self::register_subscription_fields( $schema );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Product hot fields (search/filter shop pages).
|
|
|
|
|
*
|
|
|
|
|
* @param TMDO_Schema_Registry $schema Registry.
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
private static function register_product_fields( $schema ): void {
|
|
|
|
|
$schema->register_many(
|
|
|
|
|
self::PROVIDER,
|
|
|
|
|
array(
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_price',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0',
|
|
|
|
|
'column' => '_price',
|
|
|
|
|
'indexed' => true,
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_regular_price',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0',
|
|
|
|
|
'column' => '_regular_price',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_sale_price',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => 'decimal(15,4) NULL',
|
|
|
|
|
'column' => '_sale_price',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_sku',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => "varchar(100) NOT NULL DEFAULT ''",
|
|
|
|
|
'column' => '_sku',
|
|
|
|
|
'indexed' => true,
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_stock',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => 'decimal(15,4) NULL',
|
|
|
|
|
'column' => '_stock',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_stock_status',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => "varchar(20) NOT NULL DEFAULT 'instock'",
|
|
|
|
|
'column' => '_stock_status',
|
|
|
|
|
'indexed' => true,
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_visibility',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => "varchar(20) NOT NULL DEFAULT 'visible'",
|
|
|
|
|
'column' => '_visibility',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_featured',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => "varchar(3) NOT NULL DEFAULT 'no'",
|
|
|
|
|
'column' => '_featured',
|
|
|
|
|
'indexed' => true,
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_tax_status',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => "varchar(20) NOT NULL DEFAULT 'taxable'",
|
|
|
|
|
'column' => '_tax_status',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_virtual',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => "varchar(3) NOT NULL DEFAULT 'no'",
|
|
|
|
|
'column' => '_virtual',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_downloadable',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => "varchar(3) NOT NULL DEFAULT 'no'",
|
|
|
|
|
'column' => '_downloadable',
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Cold zone — large display fields rarely searched but always shown.
|
|
|
|
|
$schema->register_many(
|
|
|
|
|
self::PROVIDER,
|
|
|
|
|
array(
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_product_attributes',
|
|
|
|
|
'zone' => 'cold',
|
|
|
|
|
'cache_group' => 'wpdo_cold_product',
|
|
|
|
|
'cache_ttl' => HOUR_IN_SECONDS,
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_purchase_note',
|
|
|
|
|
'zone' => 'cold',
|
|
|
|
|
'cache_group' => 'wpdo_cold_product',
|
|
|
|
|
'cache_ttl' => HOUR_IN_SECONDS,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Order hot fields (legacy postmeta path; only registered when HPOS off).
|
|
|
|
|
*
|
|
|
|
|
* @param TMDO_Schema_Registry $schema Registry.
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
private static function register_order_fields( $schema ): void {
|
|
|
|
|
$schema->register_many(
|
|
|
|
|
self::PROVIDER,
|
|
|
|
|
array(
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'shop_order',
|
|
|
|
|
'meta_key' => '_order_total',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0',
|
|
|
|
|
'column' => '_order_total',
|
|
|
|
|
'indexed' => true,
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'shop_order',
|
|
|
|
|
'meta_key' => '_order_currency',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => "varchar(8) NOT NULL DEFAULT ''",
|
|
|
|
|
'column' => '_order_currency',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'shop_order',
|
|
|
|
|
'meta_key' => '_billing_email',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => "varchar(254) NOT NULL DEFAULT ''",
|
|
|
|
|
'column' => '_billing_email',
|
|
|
|
|
'indexed' => true,
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'shop_order',
|
|
|
|
|
'meta_key' => '_payment_method',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => "varchar(60) NOT NULL DEFAULT ''",
|
|
|
|
|
'column' => '_payment_method',
|
|
|
|
|
'indexed' => true,
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'shop_order',
|
|
|
|
|
'meta_key' => '_customer_user',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => 'bigint(20) NOT NULL DEFAULT 0',
|
|
|
|
|
'column' => '_customer_user',
|
|
|
|
|
'indexed' => true,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Customer usermeta hot fields — registered to Entity_Registry so Hook Bus can
|
|
|
|
|
* route them to `wp_wpdo_user_hot` once `entity_user` mode advances past `disabled`.
|
|
|
|
|
*
|
|
|
|
|
* Replaces the v2.1.5 `register_customer_fields()` which targeted Schema_Registry
|
|
|
|
|
* (zone-based, no user interceptor) — that path was inert. See
|
|
|
|
|
* PLAN-entity-user-cutover.md for the migration playbook.
|
|
|
|
|
*
|
|
|
|
|
* Type mapping (Entity_Registry uses fixed-precision DECIMAL(18,6) / BIGINT(20),
|
|
|
|
|
* which is a superset of the prior `decimal(15,4)` / `bigint(20)` declarations —
|
|
|
|
|
* no precision loss):
|
|
|
|
|
* - _money_spent → decimal (DECIMAL(18,6))
|
|
|
|
|
* - _order_count → integer (BIGINT(20))
|
|
|
|
|
* - _last_order → integer (BIGINT(20))
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public static function register_user_entity_fields(): void {
|
|
|
|
|
if ( ! class_exists( 'TMDO_Entity_Registry' ) ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
TMDO_Entity_Registry::register_group(
|
|
|
|
|
'user',
|
|
|
|
|
'hot',
|
|
|
|
|
array(
|
|
|
|
|
array(
|
|
|
|
|
'key' => '_money_spent',
|
|
|
|
|
'type' => 'decimal',
|
|
|
|
|
'searchable' => true,
|
|
|
|
|
'label' => 'WC customer cumulative spend',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'key' => '_order_count',
|
|
|
|
|
'type' => 'integer',
|
|
|
|
|
'searchable' => true,
|
|
|
|
|
'label' => 'WC customer order count',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'key' => '_last_order',
|
|
|
|
|
'type' => 'integer',
|
|
|
|
|
'label' => 'WC customer last order ID',
|
|
|
|
|
),
|
|
|
|
|
// WC runtime stats — written on order/session events, high-frequency
|
|
|
|
|
// on busy stores. Unregistered they land back in wp_usermeta.
|
|
|
|
|
array(
|
|
|
|
|
'key' => 'wc_last_active',
|
|
|
|
|
'type' => 'integer',
|
|
|
|
|
'label' => 'WC last customer activity timestamp (Unix)',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
// WooCommerce stores this per-site via Users::update_site_user_meta(),
|
|
|
|
|
// which appends the de-suffixed blog prefix ('wc_order_count' + '_wp'
|
|
|
|
|
// on the main site, '_wp_2' on a subsite). Compute it the same way
|
|
|
|
|
// rather than hardcoding the main-site form.
|
|
|
|
|
'key' => 'wc_order_count_' . rtrim( $GLOBALS['wpdb']->get_blog_prefix(), '_' ),
|
|
|
|
|
'type' => 'integer',
|
|
|
|
|
'label' => 'WC order count for the current site (blog-aware key)',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'key' => 'last_update',
|
|
|
|
|
'type' => 'integer',
|
|
|
|
|
'label' => 'WC customer last profile update timestamp (Unix)',
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Subscription product fields (wc-subscriptions or wc-linepay-subscription).
|
|
|
|
|
*
|
|
|
|
|
* @param TMDO_Schema_Registry $schema Registry.
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
private static function register_subscription_fields( $schema ): void {
|
|
|
|
|
$schema->register_many(
|
|
|
|
|
self::PROVIDER,
|
|
|
|
|
array(
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_subscription_price',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => 'decimal(15,4) NULL',
|
|
|
|
|
'column' => '_subscription_price',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_subscription_period',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => 'varchar(20) NULL',
|
|
|
|
|
'column' => '_subscription_period',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_subscription_period_interval',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => 'int(11) NULL',
|
|
|
|
|
'column' => '_subscription_period_interval',
|
|
|
|
|
),
|
|
|
|
|
array(
|
|
|
|
|
'post_type' => 'product',
|
|
|
|
|
'meta_key' => '_subscription_length',
|
|
|
|
|
'zone' => 'hot',
|
|
|
|
|
'data_type' => 'int(11) NULL',
|
|
|
|
|
'column' => '_subscription_length',
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Subscription extension detection — WC native or wc-linepay-subscription.
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
private static function has_subscriptions(): bool {
|
|
|
|
|
return class_exists( 'WC_Subscriptions' )
|
|
|
|
|
|| defined( 'WCS_INIT_TIMESTAMP' )
|
|
|
|
|
|| file_exists( WP_PLUGIN_DIR . '/wc-linepay-subscription/wc-linepay-subscription.php' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* WPDO doctor probe for any wc_* table — verifies existence + reports row count.
|
|
|
|
|
*
|
|
|
|
|