Files
2meet-data-optimizer/includes/class-tmdo-core.php
T
wpdev 76c01e44df refactor: 全部 128 個生產檔加入 declare(strict_types=1)(PR-H)
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入
並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 06:13:33 +08:00

848 lines
29 KiB
PHP

<?php
/**
* Main loader for WP Data Optimizer components.
*
* @package WP_Data_Optimizer
*/
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Main loader — bootstraps all WPDO components.
*
* Loaded at plugins_loaded priority 4 (before HPCT at priority 5).
*/
class TMDO_Core {
/**
* Boot the plugin.
*/
public function run(): void {
// Schema upgrade check.
TMDO_Installer::maybe_upgrade();
// Run compatibility checks.
$compat = TMDO_Compatibility::check();
// Fire the schema registry hook so integrations can register their fields.
$registry = TMDO_Schema_Registry::instance();
// v2.0.x: Pre-register the 7 partner plugin integrations BEFORE firing the
// `wpdo_register_fields` and `wpdo_register_custom_tables` hooks. Each
// `register()` is a thin `add_action()` wrapper guarded by a partner-plugin
// `class_exists()` probe (TMDO_Infocards / Bookings / Quotation / Events /
// Mobile_Bridge / Collab / Playlist). Without this, partner add_actions
// arrive too late and the do_action below misses them — observed via
// 2meet-infocards 9 hp_vendor meta keys never landing in Schema Registry.
// register() is side-effect-free aside from add_action(), so pre-registering
// is provably safe (no hidden dependency on later-state global init).
// v2.1.3 R1 (B step): TMDO_Events removed — 2meet-events now self-registers
// via its own bridge class. Other partners still pre-registered here pending
// their own decentralized migrations (R1 continuation in v2.2.x).
$partner_integrations = array(
'TMDO_Infocards',
'TMDO_Bookings',
'TMDO_Quotation',
'TMDO_Mobile_Bridge',
'TMDO_Collab',
'TMDO_Playlist',
'TMDO_WooCommerce',
'TMDO_Member_Fields',
'TMDO_Post_Fields',
'TMDO_Hivepress_Term_Comment_Fields',
);
foreach ( $partner_integrations as $cls ) {
if ( class_exists( $cls ) ) {
call_user_func( array( $cls, 'register' ) );
}
}
/**
* Action: wpdo_register_fields
*
* Plugins register their meta_key → zone mappings here.
*
* @param TMDO_Schema_Registry $registry The schema registry instance.
*/
do_action( 'wpdo_register_fields', $registry );
// v2.1.3 R2 safety net: re-fire on init:1 to catch partners that registered
// AFTER plugins_loaded:4 (e.g., partners loaded by HP modules at plugins_loaded:5+,
// or lazy-loaded modules booted on init:0). Schema_Registry::register() has
// internal dedup, so re-firing is idempotent — same partner calls won't double-register.
// This deprecates the need for the TMDO_Core hardcoded partner pre-registration list above.
//
// v2.1.6: also re-fire `wpdo_register_entity_fields` so partner plugins booting on
// plugins_loaded:20 (e.g. 2meet-spoke-sso) can register entity-based user/term/comment
// fields. TMDO_Entity_Registry::register_group() has internal dedup. After all
// registrations land, kick `TMDO_Schema_Manager::process_pending_migrations()` once
// per request to materialize any newly-defined entity flat tables (idempotent via
// schema_hash compare — no DDL when unchanged).
if ( ! did_action( 'wpdo_late_bind_setup' ) ) {
add_action(
'init',
static function () use ( $registry ) {
do_action( 'wpdo_register_fields', $registry );
if ( class_exists( 'TMDO_Entity_Registry' ) ) {
do_action( 'wpdo_register_entity_fields', TMDO_Entity_Registry::class );
}
if ( class_exists( 'TMDO_Custom_Table_Registry' ) ) {
TMDO_Custom_Table_Registry::instance()->fire_registration();
}
if ( class_exists( 'TMDO_Schema_Manager' ) && class_exists( 'TMDO_Entity_Registry' ) ) {
$pending = TMDO_Entity_Registry::get_pending_schemas();
if ( ! empty( $pending ) ) {
TMDO_Schema_Manager::process_pending_migrations();
}
}
do_action( 'wpdo_late_bind_setup' );
},
1
);
}
// HivePress 整合已移至 2meet-data-optimizer-hivepress-addon。
// AddOn bootstrap 自行偵測 + register。Core 完全 HP-agnostic。
// Cache Layer hooks (Zone C prefetch + save_post warming).
TMDO_Cache_Layer::register_hooks();
// Register HPCT-inherited module interceptors if allowed.
if ( TMDO_Compatibility::can_register_hpct_hooks() ) {
$this->register_hpct_interceptors();
}
// v2.1.2 fix: WC commission interceptor is independent of HPCT — writes
// to its own wp_wpdo_wc_commissions table. Register whenever WC is active.
if ( class_exists( 'TMDO_WooCommerce' ) && TMDO_WooCommerce::is_active() && class_exists( 'TMDO_WC_Orders_Interceptor' ) ) {
( new TMDO_WC_Orders_Interceptor() )->register_hooks();
}
// Zone interceptors are always safe to register.
if ( TMDO_Compatibility::can_register_zone_hooks() ) {
$this->register_zone_hooks();
}
// v2.0.0: register entity adapters into Entity Registry. Adapters are
// REGISTERED unconditionally (so TMDO_API::get_entity / Hook Bus can
// find them), but Hook Bus only initializes when feature flag is on
// (default off — preserves v1.3.x behaviour).
$this->register_entity_adapters();
// v2.0.0: boot the unified Hook Bus when explicitly enabled.
TMDO_Hook_Bus_Bridge::maybe_init_hook_bus();
// Register audit log listeners for wpdo_after_write / wpdo_after_delete.
if ( class_exists( 'TMDO_Audit_Logger' ) ) {
TMDO_Audit_Logger::init();
}
// v2.0.0: fire the Custom Table Registry hook so partner plugins can register.
TMDO_Custom_Table_Registry::instance()->fire_registration();
// v2.0.0: production-time conflict monitor (admin notice + admin bar).
if ( class_exists( 'TMDO_Conflict_Monitor' ) ) {
TMDO_Conflict_Monitor::register_hooks();
}
// v2.0.x: partner plugin integrations have been pre-registered earlier
// (see top of run() before the wpdo_register_fields do_action). Removing
// the duplicate late-binding block — see commit log for time-ordering bug.
// Schedule cron events.
$this->schedule_cron();
// Admin notice for HPCT import.
if ( is_admin() && TMDO_Compatibility::should_show_hpct_notice() ) {
add_action( 'admin_notices', array( $this, 'render_hpct_notice' ) );
}
// v2.6.6: async entity backfill cron handler.
add_action( 'wpdo_entity_backfill_batch', array( $this, 'run_entity_backfill_batch' ), 10, 2 );
// v2.6.7: user stress-test batch cron handler.
add_action( TMDO_User_Stress_Tester::CRON_HOOK, array( __CLASS__, 'run_stress_test_batch' ) );
// v2.11.4: post stress-test batch cron handler.
if ( class_exists( 'TMDO_Post_Stress_Tester' ) ) {
add_action( TMDO_Post_Stress_Tester::CRON_HOOK, array( __CLASS__, 'run_post_stress_test_batch' ) );
}
// v2.13.0: term stress-test batch cron handler.
if ( class_exists( 'TMDO_Term_Stress_Tester' ) ) {
add_action( TMDO_Term_Stress_Tester::CRON_HOOK, array( __CLASS__, 'run_term_stress_test_batch' ) );
}
// v2.13.1: comment stress-test batch cron handler.
if ( class_exists( 'TMDO_Comment_Stress_Tester' ) ) {
add_action( TMDO_Comment_Stress_Tester::CRON_HOOK, array( __CLASS__, 'run_comment_stress_test_batch' ) );
}
// v2.11.5: HivePress per-post transient cache → wp_options reroute.
// Filters registered on `init` so HivePress's own bootstrap (plugins_loaded:5)
// runs first and our filter chain catches the actual cache writes happening
// during save_post and term-cache rebuilds.
if ( class_exists( 'TMDO_Hivepress_Transient_Filter' ) ) {
add_action( 'init', array( 'TMDO_Hivepress_Transient_Filter', 'init' ), 5 );
}
// v2.12.1: Term + Comment garbage write-time filter
// (silent-drop _wxr_import_* / _2meet_demo_* / orphan post-meta keys).
if ( class_exists( 'TMDO_Term_Comment_Garbage_Filter' ) ) {
add_action( 'init', array( 'TMDO_Term_Comment_Garbage_Filter', 'init' ), 5 );
}
// v2.12.3: WooCommerce term count cache → wp_options reroute
// (product_count_<taxonomy> rows out of wp_termmeta).
if ( class_exists( 'TMDO_WC_Term_Count_Filter' ) ) {
add_action( 'init', array( 'TMDO_WC_Term_Count_Filter', 'init' ), 5 );
}
// v2.12.4: Term + Comment misc bucket (catch-all flat for unregistered keys).
// Filters at priority 99 (LAST in chain) so all other v2.12.x filters
// + Hook Bus get first crack; only truly unhandled keys land here.
if ( class_exists( 'TMDO_Term_Comment_Misc_Bucket' ) ) {
add_action( 'init', array( 'TMDO_Term_Comment_Misc_Bucket', 'init' ), 5 );
}
}
/**
* Cron handler: 執行壓力測試的單一批次(v2.6.7)。
*/
public static function run_stress_test_batch(): void {
if ( class_exists( 'TMDO_User_Stress_Tester' ) ) {
TMDO_User_Stress_Tester::run_batch();
}
}
/**
* Cron handler: post entity 壓力測試單一批次(v2.11.4)。
*
* @return void
*/
public static function run_post_stress_test_batch(): void {
if ( class_exists( 'TMDO_Post_Stress_Tester' ) ) {
TMDO_Post_Stress_Tester::run_batch();
}
}
/**
* Cron handler: term entity 壓力測試單一批次(v2.13.0)。
*
* @return void
*/
public static function run_term_stress_test_batch(): void {
if ( class_exists( 'TMDO_Term_Stress_Tester' ) ) {
TMDO_Term_Stress_Tester::run_batch();
}
}
/**
* Cron handler: comment entity 壓力測試單一批次(v2.13.1)。
*
* @return void
*/
public static function run_comment_stress_test_batch(): void {
if ( class_exists( 'TMDO_Comment_Stress_Tester' ) ) {
TMDO_Comment_Stress_Tester::run_batch();
}
}
/**
* Register the four entity adapters into TMDO_Entity_Registry.
*
* Registration is unconditional but write/read paths only activate when
* the corresponding module is in a write-active or read-custom state
* (feature flag controlled).
*
* @return void
*/
private function register_entity_adapters(): void {
if ( ! class_exists( 'TMDO_Entity_Registry' ) ) {
return;
}
TMDO_Entity_Registry::register_adapter( 'post', new TMDO_Adapter_Post() );
TMDO_Entity_Registry::register_adapter( 'user', new TMDO_Adapter_User() );
TMDO_Entity_Registry::register_adapter( 'term', new TMDO_Adapter_Term() );
TMDO_Entity_Registry::register_adapter( 'comment', new TMDO_Adapter_Comment() );
/**
* Action: wpdo_register_entity_fields
*
* Partner plugins register their entity meta_key → adapter mappings here.
* Mirrors `wpdo_register_fields` but for non-postmeta entities.
*
* @since 2.0.0
*/
do_action( 'wpdo_register_entity_fields', TMDO_Entity_Registry::class );
}
/**
* Register default HivePress field mappings.
*
* These are the known HivePress meta_keys and their zone classifications.
*
* @param TMDO_Schema_Registry $registry The schema registry instance.
* @return void
*/
private function register_hivepress_defaults( TMDO_Schema_Registry $registry ): void {
// Zone A (Hot) — Listing search/filter fields.
$registry->register_many(
'hivepress',
array(
array(
'post_type' => 'hp_listing',
'meta_key' => 'hp_price',
'zone' => 'hot',
'data_type' => 'decimal(10,2) NOT NULL DEFAULT 0',
'column' => 'hp_price',
'indexed' => true,
),
array(
'post_type' => 'hp_listing',
'meta_key' => 'hp_featured',
'zone' => 'hot',
'data_type' => 'tinyint(1) NOT NULL DEFAULT 0',
'column' => 'hp_featured',
'indexed' => true,
),
array(
'post_type' => 'hp_listing',
'meta_key' => 'hp_verified',
'zone' => 'hot',
'data_type' => 'tinyint(1) NOT NULL DEFAULT 0',
'column' => 'hp_verified',
'indexed' => true,
),
array(
'post_type' => 'hp_listing',
'meta_key' => 'hp_expired_time',
'zone' => 'hot',
'data_type' => 'bigint(20) NOT NULL DEFAULT 0',
'column' => 'hp_expired_time',
),
array(
'post_type' => 'hp_listing',
'meta_key' => 'hp_featured_time',
'zone' => 'hot',
'data_type' => 'bigint(20) NOT NULL DEFAULT 0',
'column' => 'hp_featured_time',
),
// Vendor hot fields.
array(
'post_type' => 'hp_vendor',
'meta_key' => 'hp_verified',
'zone' => 'hot',
'data_type' => 'tinyint(1) NOT NULL DEFAULT 0',
'column' => 'hp_verified',
'indexed' => true,
),
array(
'post_type' => 'hp_vendor',
'meta_key' => 'hp_hourly_rate',
'zone' => 'hot',
'data_type' => 'decimal(10,2) NOT NULL DEFAULT 0',
'column' => 'hp_hourly_rate',
'indexed' => true,
),
array(
'post_type' => 'hp_vendor',
'meta_key' => 'hp_rating_count',
'zone' => 'hot',
'data_type' => 'int(11) NOT NULL DEFAULT 0',
'column' => 'hp_rating_count',
'indexed' => false,
),
)
);
// Zone C (Cold) — Profile/display fields.
$registry->register_many(
'hivepress',
array(
array(
'post_type' => 'hp_vendor',
'meta_key' => 'hp_description',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_hp_vendor',
'cache_ttl' => HOUR_IN_SECONDS,
),
array(
'post_type' => 'hp_listing',
'meta_key' => 'hp_description',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_hp_listing',
'cache_ttl' => HOUR_IN_SECONDS,
),
)
);
// ── HivePress extension fields (filled the 47% → 94% coverage gap) ──
// Reviews extension (hp_review post type).
$registry->register_many(
'hivepress-reviews',
array(
array(
'post_type' => 'hp_review',
'meta_key' => 'hp_rating',
'zone' => 'hot',
'data_type' => 'tinyint(1) NOT NULL DEFAULT 0',
'column' => 'hp_rating',
'indexed' => true,
),
array(
'post_type' => 'hp_vendor',
'meta_key' => 'hp_rating',
'zone' => 'hot',
'data_type' => 'decimal(3,2) NOT NULL DEFAULT 0',
'column' => 'hp_rating',
'indexed' => true,
),
array(
'post_type' => 'hp_review',
'meta_key' => 'hp_text',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_hp_review',
'cache_ttl' => HOUR_IN_SECONDS,
),
)
);
// Bookings extension (hp_booking post type).
$registry->register_many(
'hivepress-bookings',
array(
array(
'post_type' => 'hp_booking',
'meta_key' => 'hp_start_time',
'zone' => 'hot',
'data_type' => 'bigint(20) NOT NULL DEFAULT 0',
'column' => 'hp_start_time',
'indexed' => true,
),
array(
'post_type' => 'hp_booking',
'meta_key' => 'hp_end_time',
'zone' => 'hot',
'data_type' => 'bigint(20) NOT NULL DEFAULT 0',
'column' => 'hp_end_time',
'indexed' => true,
),
array(
'post_type' => 'hp_booking',
'meta_key' => 'hp_status',
'zone' => 'hot',
'data_type' => "varchar(20) NOT NULL DEFAULT ''",
'column' => 'hp_status',
'indexed' => true,
),
array(
'post_type' => 'hp_listing',
'meta_key' => 'hp_booking_enabled',
'zone' => 'hot',
'data_type' => 'tinyint(1) NOT NULL DEFAULT 0',
'column' => 'hp_booking_enabled',
'indexed' => true,
),
)
);
// Marketplace / Requests extension.
$registry->register_many(
'hivepress-marketplace',
array(
array(
'post_type' => 'hp_request',
'meta_key' => 'hp_request_category',
'zone' => 'hot',
'data_type' => 'bigint(20) NOT NULL DEFAULT 0',
'column' => 'hp_request_category',
'indexed' => true,
),
array(
'post_type' => 'hp_offer',
'meta_key' => 'hp_amount',
'zone' => 'hot',
'data_type' => 'decimal(10,2) NOT NULL DEFAULT 0',
'column' => 'hp_amount',
'indexed' => true,
),
array(
'post_type' => 'hp_offer',
'meta_key' => 'hp_request',
'zone' => 'hot',
'data_type' => 'bigint(20) NOT NULL DEFAULT 0',
'column' => 'hp_request',
'indexed' => true,
),
)
);
}
/**
* Register HPCT-inherited module interceptors and query interceptors.
*/
private function register_hpct_interceptors(): void {
// 7 HPCT-era meta interceptors 已搬到 2meet-data-optimizer-hivepress-addon。
// 1 LatePoint interceptor 已搬到 2meet-data-optimizer-latepoint-addon。
// 5 query interceptors 已搬到 2meet-data-optimizer-hivepress-addon。
// 此方法保留為空殼以保持 ABI 相容;AddOn 各自於自身 bootstrap 內 register_hooks。
}
/**
* Register zone-specific hooks.
*/
private function register_zone_hooks(): void {
// Sync Bridge: Zone-aware dual-write + read routing for all zone fields.
$sync_bridge = new TMDO_Sync_Bridge();
$sync_bridge->register_hooks();
// Query Router: Zone A flat-column JOIN rewrite for WP_Query.
$query_router = new TMDO_Query_Router();
$query_router->register_hooks();
// v2.10.1 Post Entity Query Router — only registers when post mode
// is shadow_read or aeav_only (zero overhead in disabled/dual_write).
// is_router_active() guards against early-bind issues; once mode flips
// to reads_from_flat the hooks fire on subsequent requests.
if ( class_exists( 'TMDO_Post_Query_Router' ) && TMDO_Post_Query_Router::is_router_active() ) {
$post_router = new TMDO_Post_Query_Router();
$post_router->register_hooks();
}
// Zone B: Warm cleanup cron handler.
add_action( 'wpdo_warm_cleanup', array( $this, 'cleanup_warm_zone' ) );
// v2.10.3: Post shadow_read verifier cron handler. Only fires when
// post mode is shadow_read (verifier::cron_tick() is internally gated).
if ( class_exists( 'TMDO_Post_Shadow_Verifier' ) ) {
add_action( TMDO_Post_Shadow_Verifier::CRON_HOOK, array( 'TMDO_Post_Shadow_Verifier', 'cron_tick' ) );
}
// v2.12.5: Term + Comment shadow verifier cron handler. Only fires when
// either term or comment mode is shadow_read (gated internally).
if ( class_exists( 'TMDO_Term_Comment_Shadow_Verifier' ) ) {
add_action( TMDO_Term_Comment_Shadow_Verifier::CRON_HOOK, array( 'TMDO_Term_Comment_Shadow_Verifier', 'cron_tick' ) );
}
// v2.10.3: react to bridge mode changes — register/unregister verifier
// cron when post mode crosses the shadow_read threshold.
add_action( 'wpdo_bridge_mode_changed', array( $this, 'maybe_toggle_post_verifier_cron' ), 10, 3 );
// v2.12.5: same listener for term/comment.
add_action( 'wpdo_bridge_mode_changed', array( $this, 'maybe_toggle_term_comment_verifier_cron' ), 10, 3 );
// Zone D: Archive sweep cron handler.
add_action( 'wpdo_archive_sweep', array( $this, 'sweep_archive_zone' ) );
// Errors GC cron handler — keeps wp_wpdo_errors bounded (default 90 days).
add_action( 'wpdo_errors_gc', array( $this, 'gc_errors' ) );
// Monthly health-snapshot cron handler.
add_action( 'wpdo_health_snapshot_monthly', array( $this, 'monthly_health_snapshot' ) );
// v2.3.0 M6: daily health probe cron.
add_action( 'wpdo_daily_health_check', array( 'TMDO_Health_Cron', 'run' ) );
// v2.3.0 M6: daily snapshot prune cron (paired with health check).
add_action( 'wpdo_snapshot_prune_daily', array( 'TMDO_Snapshot_Pruner', 'cron_run' ) );
// v2.5.0 M13: daily FSM automator cron (opt-in).
add_action( 'wpdo_fsm_automator_run', array( 'TMDO_FSM_Automator', 'run' ) );
// v2.6.2: daily site-wide EAV health metrics snapshot.
TMDO_Site_Metrics_Collector::register();
// Add 'monthly' cron interval (WP only ships hourly/twicedaily/daily).
add_filter(
'cron_schedules',
static function ( $schedules ) {
if ( ! isset( $schedules['monthly'] ) ) {
$schedules['monthly'] = array(
'interval' => 30 * DAY_IN_SECONDS,
'display' => __( 'Once Monthly', '2meet-data-optimizer' ),
);
}
return $schedules;
}
);
// Admin notice when DB grows > 10% MoM.
if ( is_admin() ) {
add_action( 'admin_notices', array( $this, 'render_health_alert_notice' ) );
}
// Zone B/D: Listing stats integration(已搬到 2meet-data-optimizer-hivepress-addon)。
if ( class_exists( 'TMDO_Listing_Stats' ) ) {
TMDO_Listing_Stats::register_hooks();
}
}
/**
* Schedule cron events if not already scheduled.
*/
private function schedule_cron(): void {
if ( ! wp_next_scheduled( 'wpdo_warm_cleanup' ) ) {
wp_schedule_event( time(), 'hourly', 'wpdo_warm_cleanup' );
}
// v2.10.3: Post shadow_read verifier — only schedule when post mode
// is shadow_read (auto-cleared in mode_changed listener below).
if ( class_exists( 'TMDO_Post_Shadow_Verifier' )
&& class_exists( 'TMDO_Mode_Manager' )
&& 'shadow_read' === TMDO_Mode_Manager::get( 'post' )
&& ! wp_next_scheduled( TMDO_Post_Shadow_Verifier::CRON_HOOK )
) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', TMDO_Post_Shadow_Verifier::CRON_HOOK );
}
// v2.12.5: Term + Comment shadow verifier — schedule when EITHER term
// or comment mode is shadow_read.
if ( class_exists( 'TMDO_Term_Comment_Shadow_Verifier' )
&& class_exists( 'TMDO_Mode_Manager' )
&& ( 'shadow_read' === TMDO_Mode_Manager::get( 'term' )
|| 'shadow_read' === TMDO_Mode_Manager::get( 'comment' ) )
&& ! wp_next_scheduled( TMDO_Term_Comment_Shadow_Verifier::CRON_HOOK )
) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', TMDO_Term_Comment_Shadow_Verifier::CRON_HOOK );
}
if ( ! wp_next_scheduled( 'wpdo_archive_sweep' ) ) {
wp_schedule_event( time(), 'daily', 'wpdo_archive_sweep' );
}
// Daily errors GC — keep 90 days, prevents wpdo_errors growing unbounded.
if ( ! wp_next_scheduled( 'wpdo_errors_gc' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'wpdo_errors_gc' );
}
// Monthly health snapshot — auto-runs first of each month.
if ( ! wp_next_scheduled( 'wpdo_health_snapshot_monthly' ) ) {
// Schedule at 03:00 on the 1st of next month.
$next = strtotime( 'first day of next month 03:00' );
wp_schedule_event( $next ?: ( time() + 30 * DAY_IN_SECONDS ), 'monthly', 'wpdo_health_snapshot_monthly' );
}
// v2.3.0 M6: daily health probe at 03:30 UTC (avoids hourly warm cleanup).
if ( ! wp_next_scheduled( 'wpdo_daily_health_check' ) ) {
$tomorrow_330 = strtotime( 'tomorrow 03:30 UTC' );
wp_schedule_event( $tomorrow_330 ?: ( time() + DAY_IN_SECONDS ), 'daily', 'wpdo_daily_health_check' );
}
// v2.3.0 M6: daily snapshot prune at 04:00 UTC.
if ( ! wp_next_scheduled( 'wpdo_snapshot_prune_daily' ) ) {
$tomorrow_400 = strtotime( 'tomorrow 04:00 UTC' );
wp_schedule_event( $tomorrow_400 ?: ( time() + DAY_IN_SECONDS ), 'daily', 'wpdo_snapshot_prune_daily' );
}
// v2.5.0 M13: FSM Automator daily cron at 04:30 UTC (after prune).
if ( ! wp_next_scheduled( 'wpdo_fsm_automator_run' ) ) {
$tomorrow_430 = strtotime( 'tomorrow 04:30 UTC' );
wp_schedule_event( $tomorrow_430 ?: ( time() + DAY_IN_SECONDS ), 'daily', 'wpdo_fsm_automator_run' );
}
// v2.6.2: daily site metrics collection at 05:00 UTC.
if ( ! wp_next_scheduled( TMDO_Site_Metrics_Collector::CRON_HOOK ) ) {
$tomorrow_500 = strtotime( 'tomorrow 05:00 UTC' );
wp_schedule_event( $tomorrow_500 ?: ( time() + DAY_IN_SECONDS ), 'daily', TMDO_Site_Metrics_Collector::CRON_HOOK );
}
// Zone B: Flush view counts to postmeta (hourly)(已搬到 hivepress addon)。
if ( class_exists( 'TMDO_Listing_Stats' ) ) {
TMDO_Listing_Stats::schedule_cron();
}
}
/**
* Cron handler: prune wpdo_errors > 90 days.
*
* @return void
*/
public function gc_errors(): void {
TMDO_Logger::purge( 90 );
}
/**
* Cron handler: monthly health snapshot via WP_CLI subprocess.
*
* @return void
*/
public function monthly_health_snapshot(): void {
// Only do anything when WP-CLI is invocable in the current request — the
// cron runs via `wp cron event run` typically. Otherwise, dispatch via
// shell.
if ( ! class_exists( 'TMDO_CLI' ) || ! defined( 'WP_CLI' ) || ! WP_CLI ) {
return;
}
$cli = new TMDO_CLI();
$cli->health_snapshot( array(), array() );
}
/**
* Render admin notice when DB has grown > 10% since last snapshot.
*
* Reads `wpdo_health_alert` option set by `health_snapshot` CLI when
* threshold breached. Auto-dismissed after fix or next snapshot.
*
* @return void
*/
public function render_health_alert_notice(): void {
$alert = (string) get_option( 'wpdo_health_alert', '' );
if ( '' === $alert ) {
return;
}
if ( ! TMDO_Capability::current_user_can_admin() ) {
return;
}
// Allow user to dismiss until next snapshot.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display.
if ( ! empty( $_GET['wpdo_dismiss_health_alert'] ) ) {
$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ) : '';
if ( wp_verify_nonce( $nonce, 'wpdo_dismiss_health_alert' ) ) {
delete_option( 'wpdo_health_alert' );
return;
}
}
$dismiss_url = wp_nonce_url(
add_query_arg( 'wpdo_dismiss_health_alert', '1' ),
'wpdo_dismiss_health_alert'
);
?>
<div class="notice notice-warning">
<p>
<strong>WP Data Optimizer:</strong>
<?php echo esc_html( $alert ); ?>
&nbsp;<a href="<?php echo esc_url( $dismiss_url ); ?>"><?php esc_html_e( '關閉', '2meet-data-optimizer' ); ?></a>
</p>
</div>
<?php
}
/**
* Cron handler: Clean up expired Zone B (warm) entries.
*/
public function cleanup_warm_zone(): void {
TMDO_Zone_Warm::purge_expired();
}
/**
* Toggle the post shadow verifier cron event when post mode crosses
* the shadow_read threshold (v2.10.3).
*
* Fires on `wpdo_bridge_mode_changed` action emitted by Mode_Manager::set().
*
* @param string $entity_type Entity type that changed.
* @param string $new_mode New mode value.
* @param string $old_mode Previous mode value.
* @return void
*/
public function maybe_toggle_post_verifier_cron( string $entity_type, string $new_mode, string $old_mode ): void {
if ( 'post' !== $entity_type || ! class_exists( 'TMDO_Post_Shadow_Verifier' ) ) {
return;
}
$hook = TMDO_Post_Shadow_Verifier::CRON_HOOK;
if ( 'shadow_read' === $new_mode && ! wp_next_scheduled( $hook ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', $hook );
} elseif ( 'shadow_read' !== $new_mode ) {
wp_clear_scheduled_hook( $hook );
}
}
/**
* Toggle the term/comment shadow verifier cron when EITHER term or
* comment mode crosses the shadow_read threshold (v2.12.5).
*
* @param string $entity_type Entity that changed (term/comment/user/post).
* @param string $new_mode New mode.
* @param string $old_mode Previous mode (unused).
* @return void
*/
public function maybe_toggle_term_comment_verifier_cron( string $entity_type, string $new_mode, string $old_mode ): void {
unset( $old_mode );
if ( ! in_array( $entity_type, array( 'term', 'comment' ), true ) ) {
return;
}
if ( ! class_exists( 'TMDO_Term_Comment_Shadow_Verifier' ) || ! class_exists( 'TMDO_Mode_Manager' ) ) {
return;
}
$hook = TMDO_Term_Comment_Shadow_Verifier::CRON_HOOK;
// Recompute "should run" based on BOTH entities' current modes.
$should_run = 'shadow_read' === $new_mode
|| 'shadow_read' === TMDO_Mode_Manager::get( 'term' )
|| 'shadow_read' === TMDO_Mode_Manager::get( 'comment' );
if ( $should_run && ! wp_next_scheduled( $hook ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', $hook );
} elseif ( ! $should_run ) {
wp_clear_scheduled_hook( $hook );
}
}
/**
* Cron handler: Archive stale postmeta for trashed posts.
*/
public function sweep_archive_zone(): void {
TMDO_Zone_Archive::sweep();
}
/**
* Render admin notice when hp-custom-tables is detected but not imported.
*/
public function render_hpct_notice(): void {
$import_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=hpct-import' );
?>
<div class="notice notice-warning is-dismissible">
<p>
<strong>WP Data Optimizer:</strong>
<?php
printf(
/* translators: %s: URL to import page */
esc_html__( 'HP Custom Tables 外掛已偵測到。請前往 %s 匯入其設定,然後停用 HP Custom Tables。', '2meet-data-optimizer' ),
'<a href="' . esc_url( $import_url ) . '">' . esc_html__( 'HPCT 匯入頁面', '2meet-data-optimizer' ) . '</a>'
);
?>
</p>
</div>
<?php
}
/**
* WP Cron handler: process one backfill batch for an entity/group pair.
*
* Re-schedules itself (via wp_schedule_single_event) until migration is done.
*
* @param string $entity_type Entity type (user|term|comment).
* @param string $group_name Registered group name.
*/
public function run_entity_backfill_batch( string $entity_type, string $group_name ): void {
if ( ! class_exists( 'TMDO_Entity_Migration_Engine' ) ) {
return;
}
$result = TMDO_Entity_Migration_Engine::migrate_group_batch( $entity_type, $group_name );
if ( ! $result['done'] ) {
// Re-schedule next batch (5 s delay to avoid overwhelming DB).
wp_schedule_single_event( time() + 5, 'wpdo_entity_backfill_batch', array( $entity_type, $group_name ) );
}
TMDO_Logger::info(
'entity_backfill_batch',
array(
'entity_type' => $entity_type,
'group_name' => $group_name,
'migrated' => $result['migrated'],
'total' => $result['total'],
'done' => $result['done'],
'status' => $result['status'],
)
);
}
}