76c01e44df
對齊 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
848 lines
29 KiB
PHP
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 ); ?>
|
|
<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'],
|
|
)
|
|
);
|
|
}
|
|
}
|