register( 'hivepress', [ * 'post_type' => 'hp_listing', * 'meta_key' => 'hp_price', * 'zone' => 'hot', * 'data_type' => 'decimal(10,2) NOT NULL DEFAULT 0', * 'column' => 'hp_price', * 'indexed' => true, * ]); */ class TMDO_Schema_Registry { /** * Singleton instance. * * @var self|null */ private static ?self $instance = null; /** * Registered field mappings. * * Structure: [ 'post_type:meta_key' => field_config, ... ] * * @var array */ private array $fields = array(); /** * Zone → post_type → columns map for Zone A (hot) table creation. * * @var array> */ private array $hot_columns = array(); /** * Zone C (cold) fields grouped by post_type. * * @var array */ private array $cold_fields = array(); /** * Zone B (warm) fields with TTL config. * * @var array */ private array $warm_fields = array(); /** * Private constructor — use instance() to get the singleton. */ private function __construct() {} /** * Get the singleton instance. */ public static function instance(): self { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Register a field mapping. * * @param string $provider Provider name (e.g. 'hivepress', 'woocommerce'). * @param array $config Field configuration: * - post_type (string) WordPress post type. * - meta_key (string) The wp_postmeta meta_key. * - zone (string) hot|warm|cold|archive * - data_type (string) SQL column type for Zone A (e.g. 'decimal(10,2) NOT NULL DEFAULT 0'). * - column (string) Column name in zone table (defaults to sanitized meta_key). * - indexed (bool) Whether to add an index (Zone A only). * - ttl (int) TTL in seconds (Zone B only, null = no expiry). * - cache_group (string) Object cache group (Zone C only). * - cache_ttl (int) Cache TTL in seconds (Zone C only). * - show_in_rest (bool) Whether to expose via GET /wpdo/v1/listings. Default true. */ public function register( string $provider, array $config ): void { $config = wp_parse_args( $config, array( 'post_type' => '', 'entity_type' => '', 'meta_key' => '', 'zone' => 'hot', 'data_type' => 'longtext', 'column' => '', 'indexed' => false, 'ttl' => null, 'cache_group' => '', 'cache_ttl' => HOUR_IN_SECONDS, 'provider' => $provider, 'show_in_rest' => true, ) ); // v2.1.2 fix: normalize post_type vs entity_type. Some partner plugins // (e.g. 2meet-liff) register fields against entities other than posts // (user / term / comment) using `entity_type` instead of `post_type`. // Without normalization, those fields end up with empty-string post_type, // polluting `get_hot_post_types()` output. Use entity_type as bucket key. if ( empty( $config['post_type'] ) && ! empty( $config['entity_type'] ) ) { $config['post_type'] = sanitize_key( $config['entity_type'] ); } if ( empty( $config['column'] ) ) { $config['column'] = sanitize_key( $config['meta_key'] ); } $key = $config['post_type'] . ':' . $config['meta_key']; $config['provider'] = $provider; $this->fields[ $key ] = $config; // Index by zone for efficient lookup. switch ( $config['zone'] ) { case 'hot': $this->hot_columns[ $config['post_type'] ][ $config['column'] ] = $config['data_type']; break; case 'warm': $this->warm_fields[ $config['meta_key'] ] = $config; break; case 'cold': $this->cold_fields[ $config['post_type'] ][] = $config['meta_key']; break; } } /** * Bulk register multiple fields. * * @param string $provider Provider name. * @param array $configs Array of field configs. */ public function register_many( string $provider, array $configs ): void { foreach ( $configs as $config ) { $this->register( $provider, $config ); } } /** * Get the zone configuration for a specific field. * * @param string $post_type Post type. * @param string $meta_key Meta key. * @return array|null Field config or null if not registered. */ public function get_field( string $post_type, string $meta_key ): ?array { $key = $post_type . ':' . $meta_key; return $this->fields[ $key ] ?? null; } /** * Get all fields for a specific zone. * * @param string $zone Zone identifier (hot/warm/cold/archive). * @return array Array of field configs. */ public function get_zone_fields( string $zone ): array { return array_filter( $this->fields, fn( $f ) => $f['zone'] === $zone ); } /** * Get all fields for a specific zone and post type. * * @param string $zone Zone identifier. * @param string $post_type Post type. * @return array */ public function get_zone_fields_for_type( string $zone, string $post_type ): array { return array_filter( $this->fields, fn( $f ) => $f['zone'] === $zone && $f['post_type'] === $post_type ); } /** * Get hot column definitions for a post type (for table creation). * * @param string $post_type Post type. * @return array Column name => SQL type. */ public function get_hot_columns( string $post_type ): array { return $this->hot_columns[ $post_type ] ?? array(); } /** * Get all post types that have hot zone fields. * * @return string[] */ public function get_hot_post_types(): array { return array_keys( $this->hot_columns ); } /** * Get all post types that have cold zone fields. * * @return string[] */ public function get_cold_post_types(): array { return array_keys( $this->cold_fields ); } /** * Get cold field meta keys for a post type. * * @param string $post_type Post type. * @return string[] Array of meta keys. */ public function get_cold_meta_keys( string $post_type ): array { return array_unique( $this->cold_fields[ $post_type ] ?? array() ); } /** * Returns hot column names for a post type where show_in_rest is true. * Used by REST endpoints to restrict publicly exposed fields. * * @param string $post_type Post type. * @return string[] Column names that are REST-visible. */ public function get_rest_visible_hot_columns( string $post_type ): array { $visible = array(); foreach ( $this->fields as $field ) { if ( 'hot' === $field['zone'] && $field['post_type'] === $post_type && ! empty( $field['show_in_rest'] ) ) { $visible[] = $field['column']; } } return $visible; } /** * Returns cold meta keys for a post type where show_in_rest is true. * Used by REST endpoints to restrict publicly exposed fields. * * @param string $post_type Post type. * @return string[] Meta keys that are REST-visible. */ public function get_rest_visible_cold_keys( string $post_type ): array { $visible = array(); foreach ( $this->fields as $field ) { if ( 'cold' === $field['zone'] && $field['post_type'] === $post_type && ! empty( $field['show_in_rest'] ) ) { $visible[] = $field['meta_key']; } } return array_unique( $visible ); } /** * Get warm field configuration. * * @param string $meta_key Meta key. * @return array|null */ public function get_warm_field( string $meta_key ): ?array { return $this->warm_fields[ $meta_key ] ?? null; } /** * Check which zone a meta_key belongs to for a given post type. * * @param string $post_type Post type. * @param string $meta_key Meta key. * @return string|null Zone name or null if not registered. */ public function get_field_zone( string $post_type, string $meta_key ): ?string { $field = $this->get_field( $post_type, $meta_key ); return $field['zone'] ?? null; } /** * Get all registered fields (for admin dashboard / analysis). * * @return array */ public function all(): array { return $this->fields; } /** * Get summary statistics for the admin dashboard. * * @return array{hot: int, warm: int, cold: int, archive: int, total: int} */ public function get_stats(): array { $stats = array( 'hot' => 0, 'warm' => 0, 'cold' => 0, 'archive' => 0, 'total' => 0, ); foreach ( $this->fields as $field ) { if ( isset( $stats[ $field['zone'] ] ) ) { ++$stats[ $field['zone'] ]; } ++$stats['total']; } return $stats; } }