Files
2meet-data-optimizer/includes/class-tmdo-custom-table-registry.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

298 lines
8.3 KiB
PHP

<?php
/**
* Custom Table Registry — third-party plugin custom-table awareness.
*
* Allows partner plugins (2meet-courses, 2meet-bookings, 2meet-infocards, etc.)
* to register their own custom tables so WPDO can include them in:
* - `wp wpdo doctor` health checks
* - `wp wpdo benchmark --custom-tables`
* - Backup/cleanup tooling
* - Site monitor metrics
*
* Solves audit finding R-3 (Custom Table Provider missing).
*
* @package WP_Data_Optimizer
* @since 2.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Singleton registry for partner plugin custom tables.
*
* Usage (from a partner plugin):
* add_action( 'wpdo_register_custom_tables', function ( TMDO_Custom_Table_Registry $r ) {
* $r->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<string, array{provider:string, table_name:string, primary_key:string, post_type_link:?string, doctor_callback:?callable, benchmark_callback:?callable, expected_columns:array, indexes: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<string, array<string>> 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<string,string>) Column => SQL type for schema drift detection.
* - indexes (array<string,string[]>) 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<string, array>
*/
public function all(): array {
return $this->tables;
}
/**
* Get tables for a specific provider.
*
* @param string $provider Plugin slug.
* @return array<string, 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<string, 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<string, array{provider:string, table:string, ok:bool, message:string}>
*/
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;
}
}