Files
2meet-data-optimizer/includes/integrations/class-tmdo-member-fields.php
T
wpdev d36bb954d1 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
2026-07-31 05:06:36 +08:00

499 lines
13 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
/**
* 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 1050× 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',
),
)
);
}
}