d36bb954d1
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
233 lines
7.2 KiB
PHP
233 lines
7.2 KiB
PHP
<?php
|
|
/**
|
|
* TMDO_Hook_Bus_Bridge — feature-flagged Hook Bus integration layer.
|
|
*
|
|
* PR-3 introduces a unified Hook Bus that will eventually replace the per-
|
|
* interceptor `add_filter()` registrations. To avoid breaking production
|
|
* during the transition, the unified bus is opt-in via the
|
|
* `wpdo_hook_bus_enabled` option (default: false).
|
|
*
|
|
* When enabled, the Hook Bus takes over `update_post_metadata` /
|
|
* `get_post_metadata` at priority 8, dispatching to handlers (via
|
|
* TMDO_Adapter_Post + TMDO_Entity_Registry). When disabled, the existing
|
|
* 9 interceptors continue to operate at priority 10 untouched.
|
|
*
|
|
* Use `TMDO_Hook_Bus_Bridge::is_enabled()` to gate any code path that
|
|
* should defer to the unified bus.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
* @since 2.0.0
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Bridge / feature-flag layer between the legacy interceptors and the unified
|
|
* TMDO_Hook_Bus introduced by the UAE port.
|
|
*/
|
|
final class TMDO_Hook_Bus_Bridge {
|
|
|
|
/** Option name for the feature flag. */
|
|
public const OPTION = 'wpdo_hook_bus_enabled';
|
|
|
|
/**
|
|
* Request-level cache for is_enabled() checks.
|
|
*
|
|
* @var bool|null
|
|
*/
|
|
private static ?bool $cache = null;
|
|
|
|
/**
|
|
* Whether the unified Hook Bus is enabled for this site.
|
|
*
|
|
* Default: false (legacy interceptors at priority 10 remain active).
|
|
* To enable: `update_option( 'wpdo_hook_bus_enabled', '1' )` or
|
|
* `wp option update wpdo_hook_bus_enabled 1`.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function is_enabled(): bool {
|
|
if ( null !== self::$cache ) {
|
|
return self::$cache;
|
|
}
|
|
$value = get_option( self::OPTION, '1' ); // Default ON as of v2.5.4.
|
|
self::$cache = ( '1' === (string) $value || true === $value );
|
|
return self::$cache;
|
|
}
|
|
|
|
/**
|
|
* Reset request cache — for tests and runtime mode toggles.
|
|
*
|
|
* @internal
|
|
*/
|
|
public static function reset_cache(): void {
|
|
self::$cache = null;
|
|
}
|
|
|
|
/**
|
|
* Boot the unified Hook Bus when enabled.
|
|
*
|
|
* Called from TMDO_Core::run() after legacy interceptors have a chance to
|
|
* register. The Hook Bus will silently skip startup if disabled.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function maybe_init_hook_bus(): void {
|
|
if ( ! self::is_enabled() ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! class_exists( 'TMDO_Hook_Bus' ) ) {
|
|
return;
|
|
}
|
|
|
|
// Initialize the unified bus. Only Adapter_Post handlers will be
|
|
// registered by default (PR-3 scope); other entity adapters land in PR-5.
|
|
TMDO_Hook_Bus::init();
|
|
}
|
|
|
|
/**
|
|
* Whitelist of WPDO interceptors that are designed to coexist on the same
|
|
* hook + priority. They each filter on a disjoint meta_key prefix, so
|
|
* "multiple callbacks per hook" does not imply a real conflict.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private const COEXIST_WHITELIST = array(
|
|
'TMDO_Reviews_Interceptor',
|
|
'TMDO_Messages_Interceptor',
|
|
'TMDO_Favorites_Interceptor',
|
|
'TMDO_Memberships_Interceptor',
|
|
'TMDO_Statistics_Interceptor',
|
|
'TMDO_Requests_Interceptor',
|
|
'TMDO_Listing_Meta_Interceptor',
|
|
'TMDO_WC_Orders_Interceptor',
|
|
'TMDO_LatePoint_Interceptor',
|
|
'TMDO_Sync_Bridge',
|
|
'TMDO_Cache_Layer',
|
|
'TMDO_Listing_Stats',
|
|
'TMDO_Hook_Bus',
|
|
// v2.11.5+: HivePress per-post transient cache rerouter. Filters at
|
|
// priority 9 (before Hook Bus at 10) and matches only `_transient_hp_*`
|
|
// meta_keys — fully disjoint from any other interceptor.
|
|
'TMDO_Hivepress_Transient_Filter',
|
|
// v2.12.1+: Term + Comment garbage write-time filter. Filters
|
|
// add/update_term_metadata + add/update_comment_metadata at priority 9
|
|
// and matches only `_wxr_import_*` / `_2meet_demo_*` / 8 orphan
|
|
// post-meta keys — fully disjoint from any other interceptor.
|
|
'TMDO_Term_Comment_Garbage_Filter',
|
|
// v2.12.3+: WC term count cache rerouter. Filters at priority 9 and
|
|
// matches only `product_count_*` term meta keys — fully disjoint
|
|
// from any other interceptor.
|
|
'TMDO_WC_Term_Count_Filter',
|
|
// v2.12.4+: Term + Comment misc bucket (catch-all). Filters at
|
|
// priority 99 (LAST), only handles writes where every other filter
|
|
// returned null. Disjoint by design.
|
|
'TMDO_Term_Comment_Misc_Bucket',
|
|
// v2.13.0+: Term Stress Tester. Bulk fixture generator that doesn't
|
|
// register any metadata filter — listed here so admin Conflict Detector
|
|
// recognizes it as a known WPDO class even though it's filter-disjoint.
|
|
'TMDO_Term_Stress_Tester',
|
|
// v2.13.1+: Comment Stress Tester. Same role for comment entity —
|
|
// bulk fixture generator with no metadata filter registration.
|
|
'TMDO_Comment_Stress_Tester',
|
|
);
|
|
|
|
/**
|
|
* Detect interceptor priority overlap that risks duplicate writes.
|
|
*
|
|
* Inspects the metadata filters and reports cases where two non-whitelisted
|
|
* WPDO callbacks compete on the same hook. Whitelisted interceptors (the 9
|
|
* HPCT-inherited modules + Sync_Bridge) are designed to coexist by
|
|
* filtering on disjoint meta_key prefixes — they are NOT real conflicts.
|
|
*
|
|
* Real conflicts: a third-party plugin registers `TMDO_Custom_*` AND
|
|
* collides with an existing whitelisted interceptor on the same priority.
|
|
*
|
|
* Used by the admin Conflict Detector tab + `wp wpdo conflict-scan`.
|
|
*
|
|
* @return array<int, array{hook:string, priority:int, callback:string}>
|
|
*/
|
|
public static function detect_intra_wpdo_conflicts(): array {
|
|
global $wp_filter;
|
|
|
|
$findings = array();
|
|
$hooks_to_check = array(
|
|
'update_post_metadata',
|
|
'add_post_metadata',
|
|
'delete_post_metadata',
|
|
'get_post_metadata',
|
|
);
|
|
|
|
foreach ( $hooks_to_check as $hook ) {
|
|
if ( ! isset( $wp_filter[ $hook ] ) ) {
|
|
continue;
|
|
}
|
|
|
|
// Collect TMDO_* callbacks AND identify which are non-whitelisted.
|
|
$wpdo_callbacks = array();
|
|
$non_whitelist_callbacks = array();
|
|
$priorities = $wp_filter[ $hook ]->callbacks ?? array();
|
|
|
|
foreach ( $priorities as $priority => $callbacks ) {
|
|
foreach ( $callbacks as $cb ) {
|
|
$cb = $cb['function'] ?? null;
|
|
if ( null === $cb ) {
|
|
continue;
|
|
}
|
|
$class_name = self::callable_class_name( $cb );
|
|
if ( null === $class_name || ! str_starts_with( $class_name, 'TMDO_' ) ) {
|
|
continue;
|
|
}
|
|
$entry = array(
|
|
'class' => $class_name,
|
|
'priority' => (int) $priority,
|
|
);
|
|
$wpdo_callbacks[] = $entry;
|
|
if ( ! in_array( $class_name, self::COEXIST_WHITELIST, true ) ) {
|
|
$non_whitelist_callbacks[] = $entry;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Real conflict: any non-whitelisted WPDO callback overlapping with
|
|
// other WPDO callbacks on the same hook.
|
|
if ( ! empty( $non_whitelist_callbacks ) && count( $wpdo_callbacks ) > 1 ) {
|
|
foreach ( $non_whitelist_callbacks as $cb ) {
|
|
$findings[] = array(
|
|
'hook' => $hook,
|
|
'priority' => $cb['priority'],
|
|
'callback' => $cb['class'],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $findings;
|
|
}
|
|
|
|
/**
|
|
* Extract the class name from a callable, or return null when not class-bound.
|
|
*
|
|
* @param mixed $cb Any PHP callable.
|
|
* @return string|null Fully qualified class name, or null.
|
|
*/
|
|
private static function callable_class_name( $cb ): ?string {
|
|
if ( is_array( $cb ) && isset( $cb[0] ) ) {
|
|
$obj = $cb[0];
|
|
if ( is_object( $obj ) ) {
|
|
return get_class( $obj );
|
|
}
|
|
if ( is_string( $obj ) ) {
|
|
return $obj;
|
|
}
|
|
}
|
|
if ( is_string( $cb ) && str_contains( $cb, '::' ) ) {
|
|
return strtok( $cb, ':' );
|
|
}
|
|
return null;
|
|
}
|
|
}
|