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
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user