register( '2meet-courses', [ * 'table_name' => '2mc_courses', // raw, without $wpdb->prefix * 'primary_key' => 'id', * 'post_type_link' => null, * 'doctor_callback' => [ '2meet_Courses', 'doctor_check' ], * 'benchmark_callback' => [ '2meet_Courses', 'benchmark_run' ], * ] ); * } ); */ final class TMDO_Custom_Table_Registry { /** * Singleton instance. * * @var self|null */ private static ?self $instance = null; /** * Registered custom tables, keyed by `provider:table_name`. * * @var array */ private array $tables = array(); /** * V2.1.2: Secondary index by provider for O(1) `for_provider()` lookup. * Avoids array_filter scan over the full registry — matters at 100+ tables. * * @var array> provider => list of full keys ('provider:table_name'). */ private array $by_provider = array(); /** * Whether the registration hook has fired. * * @var bool */ private bool $registration_done = false; /** * Private constructor — use instance(). */ private function __construct() {} /** * Singleton accessor. */ public static function instance(): self { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Reset for tests only. * * @internal */ public static function reset_for_tests(): void { self::$instance = null; } /** * Fire `wpdo_register_custom_tables` action so partner plugins can register. * * Called by TMDO_Core::run() and again on plugins_loaded:30 (after all * partner plugins have had a chance to attach their listeners). Safe to * call multiple times — `register()` is idempotent on (provider, table_name). * * @return void */ public function fire_registration(): void { $this->registration_done = true; /** * Action: wpdo_register_custom_tables * * Partner plugins should register their custom tables here. * * @param TMDO_Custom_Table_Registry $registry Registry instance. */ do_action( 'wpdo_register_custom_tables', $this ); } /** * Register a custom table from a partner plugin. * * @param string $provider Plugin slug (e.g. '2meet-courses'). * @param array $config Table configuration: * - table_name (string, required) Raw table name without $wpdb->prefix. * - primary_key (string, default 'id') Primary key column name. * - post_type_link (?string) Linked post_type, or null if standalone. * - doctor_callback (?callable) Returns array of {ok:bool, message:string} for doctor. * - benchmark_callback (?callable) Returns array of {duration_ms:float, sample_size:int}. * - expected_columns (array) Column => SQL type for schema drift detection. * - indexes (array) Index name => column list for index health. * @return bool True on success, false on validation failure or duplicate. */ public function register( string $provider, array $config ): bool { $config = wp_parse_args( $config, array( 'table_name' => '', 'primary_key' => 'id', 'post_type_link' => null, 'doctor_callback' => null, 'benchmark_callback' => null, 'expected_columns' => array(), 'indexes' => array(), ) ); if ( '' === $config['table_name'] || '' === $provider ) { return false; } // Sanitize table name to a safe identifier. $config['table_name'] = sanitize_key( $config['table_name'] ); if ( '' === $config['table_name'] ) { return false; } $config['provider'] = $provider; $key = $provider . ':' . $config['table_name']; if ( isset( $this->tables[ $key ] ) ) { return false; // Already registered. } $this->tables[ $key ] = $config; // v2.1.2: maintain secondary index by provider for O(1) lookup. $this->by_provider[ $provider ][] = $key; return true; } /** * Unregister a custom table (test or runtime cleanup). * * @param string $provider Plugin slug. * @param string $table_name Raw table name. */ public function unregister( string $provider, string $table_name ): bool { $key = $provider . ':' . sanitize_key( $table_name ); if ( ! isset( $this->tables[ $key ] ) ) { return false; } unset( $this->tables[ $key ] ); // v2.1.2: keep secondary index in sync. if ( isset( $this->by_provider[ $provider ] ) ) { $this->by_provider[ $provider ] = array_values( array_diff( $this->by_provider[ $provider ], array( $key ) ) ); if ( empty( $this->by_provider[ $provider ] ) ) { unset( $this->by_provider[ $provider ] ); } } return true; } /** * Get all registered tables. * * @return array */ public function all(): array { return $this->tables; } /** * Get tables for a specific provider. * * @param string $provider Plugin slug. * @return array */ public function for_provider( string $provider ): array { // v2.1.2: O(1) via secondary index instead of O(n) array_filter. // Drop-in equivalent — same return shape (key=full_key, value=config). if ( ! isset( $this->by_provider[ $provider ] ) ) { return array(); } $out = array(); foreach ( $this->by_provider[ $provider ] as $key ) { if ( isset( $this->tables[ $key ] ) ) { $out[ $key ] = $this->tables[ $key ]; } } return $out; } /** * Get tables linked to a specific post_type. * * @param string $post_type Post type slug. * @return array */ public function for_post_type( string $post_type ): array { return array_filter( $this->tables, static fn( $cfg ) => isset( $cfg['post_type_link'] ) && $cfg['post_type_link'] === $post_type ); } /** * Get all unique provider names. * * @return string[] */ public function providers(): array { $set = array(); foreach ( $this->tables as $cfg ) { $set[ $cfg['provider'] ] = true; } return array_keys( $set ); } /** * Summary stats for admin dashboard. * * @return array{tables_count:int, providers_count:int, with_doctor:int, with_benchmark:int} */ public function get_stats(): array { $with_doctor = 0; $with_benchmark = 0; foreach ( $this->tables as $cfg ) { if ( is_callable( $cfg['doctor_callback'] ?? null ) ) { ++$with_doctor; } if ( is_callable( $cfg['benchmark_callback'] ?? null ) ) { ++$with_benchmark; } } return array( 'tables_count' => count( $this->tables ), 'providers_count' => count( $this->providers() ), 'with_doctor' => $with_doctor, 'with_benchmark' => $with_benchmark, ); } /** * Run all registered doctor callbacks and aggregate results. * * @return array */ public function run_doctor_checks(): array { $results = array(); foreach ( $this->tables as $key => $cfg ) { if ( ! is_callable( $cfg['doctor_callback'] ?? null ) ) { continue; } try { $result = call_user_func( $cfg['doctor_callback'], $cfg['table_name'] ); $results[ $key ] = array( 'provider' => $cfg['provider'], 'table' => $cfg['table_name'], 'ok' => (bool) ( $result['ok'] ?? false ), 'message' => (string) ( $result['message'] ?? '' ), ); } catch ( \Throwable $e ) { $results[ $key ] = array( 'provider' => $cfg['provider'], 'table' => $cfg['table_name'], 'ok' => false, 'message' => 'doctor_callback threw: ' . $e->getMessage(), ); } } return $results; } }