Files
2meet-data-optimizer-hivepr…/includes/hivepress/class-tmdo-hivepress-conflict-guard.php
T
wpdev b4400a68e5 chore: initial snapshot of 2meet-data-optimizer-hivepress-addon 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
2026-07-31 05:06:36 +08:00

267 lines
7.4 KiB
PHP

<?php
/**
* Conflict guard against legacy HivePress data-layer plugins.
*
* `wp-data-optimizer` v3.0.0 fully supersedes `hp-custom-tables` (HPCT) and
* `hp-info-cards`. When either is detected alongside WPDO HivePress
* integration, dual-write would corrupt zone tables (HPCT writes to its own
* `hpct_*` tables while WPDO writes to `wpdo_hot_hp_*`, then both copies
* drift) and admin tooling would show contradictory state.
*
* Strategy: detect, log, render admin notice with guidance, AND short-circuit
* the WPDO HivePress bootstrap via `wpdo_hivepress_should_bind` filter so
* adapters are NOT bound. Operator must explicitly run
* `wp wpdo hivepress migrate-from-hpct` to switch to WPDO and deactivate
* the legacy plugins.
*
* @package WP_Data_Optimizer
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'TMDO_HivePress_Conflict_Guard' ) ) {
/**
* Static guard checking for legacy plugin conflicts.
*
* Stateful across requests via memo cache only; persists no DB state.
*/
final class TMDO_HivePress_Conflict_Guard {
/**
* Conflicting legacy plugins to detect.
*
* Probe by either `class_exists()` OR `defined()` — both signals are
* checked since plugin authors are inconsistent about which they expose.
*
* @var array<string, array{classes:array<int,string>, consts:array<int,string>, 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<int,string>|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<int,string> 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(
'<div class="notice notice-warning"><p><strong>%s</strong></p><p>%s</p></div>',
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<int,string>, consts:array<int,string>, 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<int,string> $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<int,string> $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 )