chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
@@ -0,0 +1,498 @@
|
||||
<?php
|
||||
/**
|
||||
* Member entity field registration for WP Data Optimizer.
|
||||
*
|
||||
* Registers four user entity groups designed for 千萬 (10M) member scale.
|
||||
* All groups use TMDO_Entity_Registry → flat tables instead of wp_usermeta EAV.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers four user entity groups for membership, activity, profile, and SSO.
|
||||
*
|
||||
* Groups → flat tables:
|
||||
* membership → wp_wpdo_user_membership (tier, points, expiry — high-freq search)
|
||||
* activity → wp_wpdo_user_activity (login counters, last-active — high-freq write)
|
||||
* profile → wp_wpdo_user_profile (display fields, specialties — low-freq write)
|
||||
* sso → wp_wpdo_user_sso (Hub token cache, replaces _tmso_* usermeta)
|
||||
*
|
||||
* Called from TMDO_Core::run() before wpdo_register_entity_fields fires.
|
||||
* Pattern mirrors TMDO_WooCommerce::register() / register_user_entity_fields().
|
||||
*/
|
||||
final class TMDO_Member_Fields {
|
||||
|
||||
/**
|
||||
* Hook into WPDO entity field registration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register(): void {
|
||||
add_action( 'wpdo_register_entity_fields', array( __CLASS__, 'register_entity_fields' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all user entity groups.
|
||||
*
|
||||
* V2.7.0: Adds four legacy-key groups (core_profile, social, commerce, hp_user)
|
||||
* to absorb wp_usermeta rows that previously bypassed the entity bridge —
|
||||
* driving the wp_users:wp_usermeta ratio from 1:5.5 toward 1:2.5.
|
||||
*
|
||||
* V2.8.4: Adds admin_prefs group — the 7 default keys WP core writes for
|
||||
* EVERY new user via wp_insert_user (rich_editing, syntax_highlighting,
|
||||
* comment_shortcuts, admin_color, use_ssl, show_admin_bar_front,
|
||||
* dismissed_wp_pointers). Without this group, fresh users always show
|
||||
* ratio 1:9 regardless of any other optimization.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_entity_fields(): void {
|
||||
if ( ! class_exists( 'TMDO_Entity_Registry' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::register_membership_group();
|
||||
self::register_activity_group();
|
||||
self::register_profile_group();
|
||||
self::register_sso_group();
|
||||
self::register_core_profile_group();
|
||||
self::register_social_group();
|
||||
self::register_commerce_group();
|
||||
self::register_hp_user_group();
|
||||
self::register_admin_prefs_group();
|
||||
}
|
||||
|
||||
// ── Group definitions ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Tier level, points balance, expiry — primary search target.
|
||||
*
|
||||
* Extra composite indexes (idx_level_expires, idx_expires_level, idx_points_bal)
|
||||
* are applied by TMDO_Installer::install_member_indexes() after table creation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function register_membership_group(): void {
|
||||
TMDO_Entity_Registry::register_group(
|
||||
'user',
|
||||
'membership',
|
||||
array(
|
||||
array(
|
||||
'key' => 'membership_level',
|
||||
'type' => 'enum',
|
||||
'searchable' => true,
|
||||
'options' => array( 'bronze', 'silver', 'gold', 'platinum', 'custom' ),
|
||||
'label' => 'Membership tier level',
|
||||
),
|
||||
array(
|
||||
'key' => 'points_balance',
|
||||
'type' => 'integer',
|
||||
'searchable' => true,
|
||||
'default' => 0,
|
||||
'label' => 'Current points balance',
|
||||
),
|
||||
array(
|
||||
'key' => 'membership_expires_at',
|
||||
'type' => 'datetime',
|
||||
'searchable' => true,
|
||||
'label' => 'Membership expiry datetime',
|
||||
),
|
||||
array(
|
||||
'key' => 'membership_activated_at',
|
||||
'type' => 'datetime',
|
||||
'label' => 'Membership activation datetime',
|
||||
),
|
||||
array(
|
||||
'key' => 'tier_source',
|
||||
'type' => 'text',
|
||||
'label' => 'Tier source: manual / wc_subscription / admin_set',
|
||||
),
|
||||
array(
|
||||
'key' => 'custom_tier_label',
|
||||
'type' => 'text',
|
||||
'label' => 'Custom tier display label (when level=custom)',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login counters and last-active timestamps — separated to avoid lock
|
||||
* contention with membership reads during high-traffic periods.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function register_activity_group(): void {
|
||||
TMDO_Entity_Registry::register_group(
|
||||
'user',
|
||||
'activity',
|
||||
array(
|
||||
array(
|
||||
'key' => 'login_count',
|
||||
'type' => 'integer',
|
||||
'searchable' => true,
|
||||
'default' => 0,
|
||||
'label' => 'Cumulative login count',
|
||||
),
|
||||
array(
|
||||
'key' => 'last_active_at',
|
||||
'type' => 'datetime',
|
||||
'searchable' => true,
|
||||
'label' => 'Last activity datetime',
|
||||
),
|
||||
array(
|
||||
'key' => 'last_login_at',
|
||||
'type' => 'datetime',
|
||||
'label' => 'Last login datetime',
|
||||
),
|
||||
array(
|
||||
'key' => 'last_order_at',
|
||||
'type' => 'datetime',
|
||||
'label' => 'Last order datetime',
|
||||
),
|
||||
array(
|
||||
'key' => 'session_count',
|
||||
'type' => 'integer',
|
||||
'default' => 0,
|
||||
'label' => 'Total session count',
|
||||
),
|
||||
array(
|
||||
'key' => 'account_flags',
|
||||
'type' => 'integer',
|
||||
'default' => 0,
|
||||
'label' => 'Bitmask: 1=email_verified 2=phone_verified 4=kyc 8=social_signup',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display fields, specialties, avatar — written infrequently.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function register_profile_group(): void {
|
||||
TMDO_Entity_Registry::register_group(
|
||||
'user',
|
||||
'profile',
|
||||
array(
|
||||
array(
|
||||
'key' => 'specialties',
|
||||
'type' => 'json',
|
||||
'label' => 'Professional specialties (JSON array)',
|
||||
),
|
||||
array(
|
||||
'key' => 'bio_url',
|
||||
'type' => 'text',
|
||||
'label' => 'Bio or portfolio URL',
|
||||
),
|
||||
array(
|
||||
'key' => 'avatar_url',
|
||||
'type' => 'text',
|
||||
'label' => 'Avatar image URL',
|
||||
),
|
||||
array(
|
||||
'key' => 'display_name_custom',
|
||||
'type' => 'text',
|
||||
'searchable' => true,
|
||||
'fulltext' => true,
|
||||
'label' => 'Custom display name (fulltext searchable)',
|
||||
),
|
||||
array(
|
||||
'key' => 'locale',
|
||||
'type' => 'text',
|
||||
'label' => 'User locale (e.g. zh_TW)',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hub/Spoke SSO token cache — replaces _tmso_* usermeta.
|
||||
*
|
||||
* Silent refresh fires every 15 min per user; at 10M users this is a
|
||||
* high-frequency EAV hot-spot. A flat table + object cache hit cuts DB
|
||||
* load 10–50× versus a wp_usermeta EAV scan per refresh.
|
||||
*
|
||||
* last_id_token is NOT stored in plaintext (privacy + volume). Only the
|
||||
* SHA-256 hash is kept for SLO token comparison.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function register_sso_group(): void {
|
||||
TMDO_Entity_Registry::register_group(
|
||||
'user',
|
||||
'sso',
|
||||
array(
|
||||
array(
|
||||
'key' => 'hub_global_user_id',
|
||||
'type' => 'text',
|
||||
'searchable' => true,
|
||||
'label' => 'Hub global user UUID (2mso_user_mapping.global_user_id bridge key)',
|
||||
),
|
||||
array(
|
||||
'key' => 'picture_url',
|
||||
'type' => 'text',
|
||||
'label' => 'Social / SSO profile picture URL',
|
||||
),
|
||||
array(
|
||||
'key' => 'last_id_token_hash',
|
||||
'type' => 'text',
|
||||
'label' => 'SHA-256(last_id_token) for SLO comparison — no plaintext stored',
|
||||
),
|
||||
array(
|
||||
'key' => 'refresh_token_enc',
|
||||
'type' => 'textarea',
|
||||
'label' => 'Encrypted refresh token (TMSO_Crypto — key-versioned enc_vN:ciphertext)',
|
||||
),
|
||||
array(
|
||||
'key' => 'token_expires_at',
|
||||
'type' => 'datetime',
|
||||
'searchable' => true,
|
||||
'label' => 'SSO token expiry (set to past to force re-auth on next request)',
|
||||
),
|
||||
array(
|
||||
'key' => 'sso_last_login_at',
|
||||
'type' => 'datetime',
|
||||
'label' => 'Last SSO-initiated login datetime',
|
||||
),
|
||||
array(
|
||||
'key' => 'sso_login_count',
|
||||
'type' => 'integer',
|
||||
'default' => 0,
|
||||
'label' => 'SSO login count',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* WP core user fields stored as multi-row EAV in wp_usermeta.
|
||||
*
|
||||
* Absorbing these here lets the Hook Bus short-circuit get_user_meta() /
|
||||
* update_user_meta() for the keys WP itself uses for display_name resolution
|
||||
* and the WP profile UI.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function register_core_profile_group(): void {
|
||||
TMDO_Entity_Registry::register_group(
|
||||
'user',
|
||||
'core_profile',
|
||||
array(
|
||||
array(
|
||||
'key' => 'nickname',
|
||||
'type' => 'text',
|
||||
'searchable' => true,
|
||||
'label' => 'WP nickname',
|
||||
),
|
||||
array(
|
||||
'key' => 'first_name',
|
||||
'type' => 'text',
|
||||
'searchable' => true,
|
||||
'label' => 'WP first name',
|
||||
),
|
||||
array(
|
||||
'key' => 'last_name',
|
||||
'type' => 'text',
|
||||
'searchable' => true,
|
||||
'label' => 'WP last name',
|
||||
),
|
||||
array(
|
||||
'key' => 'description',
|
||||
'type' => 'textarea',
|
||||
'label' => 'WP user bio',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Social profile URLs (HivePress vendor-profile + WP user-contact-methods).
|
||||
*
|
||||
* 15 keys × 8 vendor users ≈ 120 EAV rows in this dataset.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function register_social_group(): void {
|
||||
$social_keys = array(
|
||||
'facebook',
|
||||
'twitter',
|
||||
'instagram',
|
||||
'youtube',
|
||||
'tiktok',
|
||||
'linkedin',
|
||||
'vimeo',
|
||||
'vkontakte',
|
||||
'mastodon',
|
||||
'medium',
|
||||
'wordpress',
|
||||
'odnoklassniki',
|
||||
'pinterest',
|
||||
'dribbble',
|
||||
'github',
|
||||
);
|
||||
|
||||
// Social URLs typed as `textarea` (TEXT) — VARCHAR(255) silently truncates
|
||||
// long share URLs (utm params, deep paths) under WP's default non-strict
|
||||
// SQL mode. TEXT (64KB) covers all realistic URL lengths.
|
||||
$fields = array();
|
||||
foreach ( $social_keys as $key ) {
|
||||
$fields[] = array(
|
||||
'key' => $key,
|
||||
'type' => 'textarea',
|
||||
'label' => ucfirst( $key ) . ' profile URL',
|
||||
);
|
||||
}
|
||||
|
||||
TMDO_Entity_Registry::register_group( 'user', 'social', $fields );
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce billing & shipping address fields.
|
||||
*
|
||||
* Billing_email is searchable for guest-checkout customer lookups.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function register_commerce_group(): void {
|
||||
$address_keys = array(
|
||||
'first_name',
|
||||
'last_name',
|
||||
'company',
|
||||
'address_1',
|
||||
'address_2',
|
||||
'city',
|
||||
'state',
|
||||
'postcode',
|
||||
'country',
|
||||
);
|
||||
|
||||
$fields = array();
|
||||
foreach ( $address_keys as $key ) {
|
||||
$fields[] = array(
|
||||
'key' => 'billing_' . $key,
|
||||
'type' => 'text',
|
||||
'label' => 'WC billing ' . str_replace( '_', ' ', $key ),
|
||||
);
|
||||
$fields[] = array(
|
||||
'key' => 'shipping_' . $key,
|
||||
'type' => 'text',
|
||||
'label' => 'WC shipping ' . str_replace( '_', ' ', $key ),
|
||||
);
|
||||
}
|
||||
|
||||
// Email + phone are billing-only.
|
||||
$fields[] = array(
|
||||
'key' => 'billing_email',
|
||||
'type' => 'text',
|
||||
'searchable' => true,
|
||||
'label' => 'WC billing email',
|
||||
);
|
||||
$fields[] = array(
|
||||
'key' => 'billing_phone',
|
||||
'type' => 'text',
|
||||
'label' => 'WC billing phone',
|
||||
);
|
||||
$fields[] = array(
|
||||
'key' => 'shipping_phone',
|
||||
'type' => 'text',
|
||||
'label' => 'WC shipping phone',
|
||||
);
|
||||
|
||||
TMDO_Entity_Registry::register_group( 'user', 'commerce', $fields );
|
||||
}
|
||||
|
||||
/**
|
||||
* HivePress per-user fields (favorites + avatar attachment).
|
||||
*
|
||||
* Hp_favorited_listings is a serialized array of post IDs in legacy storage;
|
||||
* the migration engine safe_unserialize()s it (allowed_classes=false to block
|
||||
* PHP-object injection) then the json type encodes back to a JSON array column.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function register_hp_user_group(): void {
|
||||
TMDO_Entity_Registry::register_group(
|
||||
'user',
|
||||
'hp_user',
|
||||
array(
|
||||
array(
|
||||
'key' => 'hp_favorited_listings',
|
||||
'type' => 'json',
|
||||
'label' => 'HivePress favorited listing IDs (array)',
|
||||
),
|
||||
array(
|
||||
'key' => 'hp_image',
|
||||
'type' => 'text',
|
||||
'label' => 'HivePress avatar attachment ID',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* WP-core admin pref defaults — written by `wp_insert_user()` for EVERY
|
||||
* new user regardless of role. Without registering these, a fresh user
|
||||
* lands at ratio 1:9 (7 admin prefs + wp_capabilities + wp_user_level).
|
||||
* Once registered, Hook Bus intercepts `update_user_meta()` calls from
|
||||
* `wp_insert_user()` and routes them to `wp_wpdo_user_admin_prefs` flat
|
||||
* table → fresh user ratio drops to 1:2.
|
||||
*
|
||||
* Values are stored as text because WP itself stores 'true'/'false'
|
||||
* strings (not booleans), 'fresh' / 'classic' (admin_color enum strings),
|
||||
* and integer-as-string for `use_ssl`. Preserving WP's textual storage
|
||||
* shape ensures downstream code (e.g. theme switchers reading
|
||||
* `admin_color`) sees the exact same value as before.
|
||||
*
|
||||
* @since 2.8.4
|
||||
* @return void
|
||||
*/
|
||||
private static function register_admin_prefs_group(): void {
|
||||
TMDO_Entity_Registry::register_group(
|
||||
'user',
|
||||
'admin_prefs',
|
||||
array(
|
||||
array(
|
||||
'key' => 'rich_editing',
|
||||
'type' => 'text',
|
||||
'label' => 'Visual editor enabled (true/false string)',
|
||||
),
|
||||
array(
|
||||
'key' => 'syntax_highlighting',
|
||||
'type' => 'text',
|
||||
'label' => 'Code editor syntax highlighting (true/false string)',
|
||||
),
|
||||
array(
|
||||
'key' => 'comment_shortcuts',
|
||||
'type' => 'text',
|
||||
'label' => 'Comment moderation keyboard shortcuts (true/false string)',
|
||||
),
|
||||
array(
|
||||
'key' => 'admin_color',
|
||||
'type' => 'text',
|
||||
'label' => 'Admin colour scheme (fresh/classic/etc)',
|
||||
),
|
||||
array(
|
||||
'key' => 'use_ssl',
|
||||
'type' => 'text',
|
||||
'label' => 'Force SSL on admin (0/1 as string)',
|
||||
),
|
||||
array(
|
||||
'key' => 'show_admin_bar_front',
|
||||
'type' => 'text',
|
||||
'label' => 'Show admin bar on front-end (true/false string)',
|
||||
),
|
||||
array(
|
||||
'key' => 'dismissed_wp_pointers',
|
||||
'type' => 'textarea',
|
||||
'label' => 'Comma-separated dismissed pointer IDs',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user