'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', ), array( 'key' => 'known_login_ips', 'type' => 'textarea', 'label' => 'Recent login IPs JSON array (new-device notification) — replaces _tmso_known_ips usermeta', ), ) ); } /** * 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', ), array( 'key' => 'wp_user_level', 'type' => 'integer', 'label' => 'WP legacy user level (0-10); written by WP on every role change', ), // WP admin-UI prefs written on first dashboard visit or explicit user action. array( 'key' => 'show_welcome_panel', 'type' => 'text', 'label' => 'Dashboard welcome panel visibility (true/false/1/0)', ), array( 'key' => 'wp_persisted_preferences', 'type' => 'textarea', 'label' => 'Block editor persisted preferences (JSON blob)', ), array( 'key' => 'nav_menu_recently_edited', 'type' => 'text', 'label' => 'ID of nav menu most recently edited', ), array( 'key' => 'wp_dashboard_quick_press_last_post_id', 'type' => 'integer', 'label' => 'Post ID from last Quick Draft save', ), array( 'key' => 'edit_page_per_page', 'type' => 'integer', 'label' => 'Rows-per-page in Pages list table', ), array( 'key' => 'edit_post_per_page', 'type' => 'integer', 'label' => 'Rows-per-page in Posts list table', ), array( 'key' => 'edit_hp_listing_per_page', 'type' => 'integer', 'label' => 'Rows-per-page in hp_listing list table', ), // Keys with hyphens: sanitize_column_name() strips hyphens entirely. // community-events-location → communityeventslocation (column name) // wp_user-settings → wpusersettings // wp_user-settings-time → wpusersettingstime // managenav-menuscolumnshidden → managenavmenuscolumnshidden // metaboxhidden_nav-menus → metaboxhidden_navmenus. array( 'key' => 'community-events-location', 'type' => 'textarea', 'label' => 'Dashboard community-events saved location (JSON); column: communityeventslocation', ), array( 'key' => 'wp_user-settings', 'type' => 'text', 'label' => 'WP admin UI settings string; column: wpusersettings', ), array( 'key' => 'wp_user-settings-time', 'type' => 'integer', 'label' => 'Timestamp when wp_user-settings was last written; column: wpusersettingstime', ), array( 'key' => 'managenav-menuscolumnshidden', 'type' => 'text', 'label' => 'Hidden columns in nav-menus screen; column: managenavmenuscolumnshidden', ), array( 'key' => 'metaboxhidden_nav-menus', 'type' => 'text', 'label' => 'Hidden meta-boxes on nav-menus screen; column: metaboxhidden_navmenus', ), array( 'key' => 'dismissed_no_secure_connection_notice', 'type' => 'text', 'label' => 'Admin dismissed "no secure connection" notice (1/empty)', ), array( 'key' => 'meta-box-order_product', 'type' => 'textarea', 'label' => 'Meta-box order on WC Products screen (serialized); column: metaboxorder_product', ), ) ); } }