, consts:array, name:string}> */ private const CONFLICTS = array( 'hp-custom-tables' => array( 'classes' => array( 'HPCT_Core', 'HPCT_Plugin', 'HP_Custom_Tables', ), 'consts' => array( 'HPCT_VERSION', 'HP_CUSTOM_TABLES_VERSION', ), 'name' => 'HP Custom Tables', ), 'hp-info-cards' => array( 'classes' => array( 'HP_Info_Cards', 'HPIC_Plugin', ), 'consts' => array( 'HP_INFO_CARDS_VERSION', 'HPIC_VERSION', ), 'name' => 'HP Info Cards', ), ); /** * Memoized detection result for the current request. * * @var array|null */ private static ?array $memo = null; /** * Whether the admin_notices hook has been bound this request. * * @var bool */ private static bool $notice_bound = false; /** * Run the conflict check at boot time. * * Called by `TMDO_HivePress_Bootstrap::boot()` BEFORE adapter binding * so the `wpdo_hivepress_should_bind` filter has chance to short-circuit * adapter wiring when conflicts exist. */ public static function check_and_warn(): void { $conflicts = self::detect_conflicts(); if ( empty( $conflicts ) ) { return; } self::log_conflicts( $conflicts ); self::ensure_notice_bound(); self::install_should_bind_short_circuit( $conflicts ); } /** * Detect currently-active legacy plugins. * * @return array Slugs of detected conflicting plugins. */ public static function detect_conflicts(): array { if ( null !== self::$memo ) { return self::$memo; } $out = array(); foreach ( self::CONFLICTS as $slug => $probe ) { if ( self::probe_one( $probe ) ) { $out[] = $slug; } } self::$memo = $out; return $out; } /** * Reset internal state for unit tests. * * @internal */ public static function reset_for_tests(): void { self::$memo = null; self::$notice_bound = false; } /** * Whether any conflict is currently active. */ public static function has_conflict(): bool { return ! empty( self::detect_conflicts() ); } /** * Render the admin notice (bound only when conflicts exist). * * @internal Called by WordPress; do not call directly. */ public static function render_notice(): void { if ( ! function_exists( 'is_admin' ) || ! is_admin() ) { return; } $conflicts = self::detect_conflicts(); if ( empty( $conflicts ) ) { return; } $names = array_map( static function ( string $slug ): string { $probe = self::CONFLICTS[ $slug ] ?? null; if ( null === $probe ) { return $slug; } $name = (string) ( $probe['name'] ?? $slug ); return esc_html( $name ); }, $conflicts ); $message = sprintf( /* translators: %s: comma-separated list of conflicting plugin names. */ __( 'wp-data-optimizer detected legacy HivePress data-layer plugins: %s. WPDO HivePress integration is paused to prevent dual-write corruption. Run `wp wpdo hivepress migrate-from-hpct` to migrate, then deactivate the legacy plugins.', 'tmdo-hivepress' ), implode( ', ', $names ) ); printf( '

%s

%s

', esc_html__( 'WP Data Optimizer — HivePress integration paused', 'tmdo-hivepress' ), esc_html( $message ) ); } // ── Internal helpers ──────────────────────────────────────────────── /** * Probe a single legacy plugin for presence. * * @param array{classes:array, consts:array, name:string} $probe Probe spec. */ private static function probe_one( array $probe ): bool { foreach ( (array) $probe['classes'] as $cls ) { if ( '' !== $cls && class_exists( $cls ) ) { return true; } } foreach ( (array) $probe['consts'] as $const ) { if ( '' !== $const && defined( $const ) ) { return true; } } return false; } /** * Log conflicts to WPDO error log so audit script picks them up. * * @param array $conflicts Conflict slugs. */ private static function log_conflicts( array $conflicts ): void { if ( ! class_exists( 'TMDO_Logger' ) ) { return; } $message = sprintf( 'Conflict detected: %s. wp-data-optimizer fully supersedes these. Run: wp wpdo hivepress migrate-from-hpct', implode( ', ', $conflicts ) ); // Prefer warn() but fall back to log() if the API differs. if ( method_exists( 'TMDO_Logger', 'warn' ) ) { TMDO_Logger::warn( 'hivepress-integration', $message ); } elseif ( method_exists( 'TMDO_Logger', 'log' ) ) { TMDO_Logger::log( 'warning', 'hivepress-integration', $message ); } } /** * Bind the admin notice exactly once per request. */ private static function ensure_notice_bound(): void { if ( self::$notice_bound ) { return; } if ( function_exists( 'add_action' ) ) { add_action( 'admin_notices', array( __CLASS__, 'render_notice' ) ); } self::$notice_bound = true; } /** * Install the short-circuit so HivePress adapter binding is skipped. * * Bootstrap reads `apply_filters('wpdo_hivepress_should_bind', true, $context)` * before binding adapters. We force `false` whenever conflicts exist * so dual-write cannot occur. The conflict context is passed to filter * so other code can react. * * @param array $conflicts Conflict slugs. */ private static function install_should_bind_short_circuit( array $conflicts ): void { if ( ! function_exists( 'add_filter' ) ) { return; } add_filter( 'wpdo_hivepress_should_bind', static function ( $should_bind, $context = array() ) use ( $conflicts ) { unset( $context ); return false; // Note: $conflicts captured purely for greppability via closure inspection. // PHPCS may flag the `unset` if context is unused, but keeping the symmetry // with the documented filter signature. }, 1, 2 ); unset( $conflicts ); // Satisfy use-after for static analysis. } } } // end if ( ! class_exists )