version` map of detected addons. * * Detection happens during `TMDO_HivePress_Bootstrap::boot()` on * `plugins_loaded:5`, AFTER WPDO core (priority 4) but BEFORE HivePress own * boot (priority 8). The bootstrap then instantiates an adapter per detected * addon and binds its lifecycle hooks. * * Result is cached in a 5-min transient (`wpdo_hivepress_detector_cache`) so * repeated calls within a single page-load (e.g. admin tab + REST endpoint) * don't re-probe. Cache busts on plugin activation/deactivation via the * static `bust_cache()` method. * * Cost: when HivePress core is NOT installed, detector short-circuits after * a single `class_exists('HivePress\\Core')` check — zero-cost fallback path * for sites that don't use HivePress. * * @package WP_Data_Optimizer * @since 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'TMDO_HivePress_Detector' ) ) { /** * Static detector for HivePress family addons. * * Stateless apart from the request-level memo cache; reset between tests * via `reset_for_tests()`. */ final class TMDO_HivePress_Detector { /** Transient key for the detection cache. */ private const CACHE_KEY = 'wpdo_hivepress_detector_cache'; /** Transient TTL (5 minutes — short enough to pick up plugin activation, long enough to avoid per-request cost). */ private const CACHE_TTL = 300; /** * Request-level memo cache. Survives only the current request. * * @var array|null */ private static ?array $memo = null; /** * Detection probe table: slug => [class, version_const, name]. * * Keep this list in sync with the HivePress family on wordpress.org. * Detection prefers `class_exists()` over `defined()` because version * constants sometimes leak to other globals; the class probe is more * specific. Both signals are checked — either one suffices. * * For addons not yet installed locally (bookings, marketplace, * statistics) the probe still runs — it just returns no match. The * adapter file exists for future installs. * * @var array */ private const PROBES = array( 'hivepress' => array( 'class' => 'HivePress\\Core', 'const' => 'HIVEPRESS_VERSION', 'name' => 'HivePress', ), 'hivepress-blocks' => array( 'class' => 'HivePress\\Blocks\\Plugin', 'const' => 'HIVEPRESS_BLOCKS_VERSION', 'name' => 'HivePress Blocks', ), 'hivepress-bookings' => array( 'class' => 'HivePress\\Bookings\\Plugin', 'const' => 'HIVEPRESS_BOOKINGS_VERSION', 'name' => 'HivePress Bookings', ), 'hivepress-favorites' => array( 'class' => 'HivePress\\Favorites\\Plugin', 'const' => 'HIVEPRESS_FAVORITES_VERSION', 'name' => 'HivePress Favorites', ), 'hivepress-marketplace' => array( 'class' => 'HivePress\\Marketplace\\Plugin', 'const' => 'HIVEPRESS_MARKETPLACE_VERSION', 'name' => 'HivePress Marketplace', ), 'hivepress-memberships' => array( 'class' => 'HivePress\\Memberships\\Plugin', 'const' => 'HIVEPRESS_MEMBERSHIPS_VERSION', 'name' => 'HivePress Memberships', ), 'hivepress-messages' => array( 'class' => 'HivePress\\Messages\\Plugin', 'const' => 'HIVEPRESS_MESSAGES_VERSION', 'name' => 'HivePress Messages', ), 'hivepress-requests' => array( 'class' => 'HivePress\\Requests\\Plugin', 'const' => 'HIVEPRESS_REQUESTS_VERSION', 'name' => 'HivePress Requests', ), 'hivepress-reviews' => array( 'class' => 'HivePress\\Reviews\\Plugin', 'const' => 'HIVEPRESS_REVIEWS_VERSION', 'name' => 'HivePress Reviews', ), 'hivepress-seo' => array( 'class' => 'HivePress\\Seo\\Plugin', 'const' => 'HIVEPRESS_SEO_VERSION', 'name' => 'HivePress SEO', ), 'hivepress-social-links' => array( 'class' => 'HivePress\\SocialLinks\\Plugin', 'const' => 'HIVEPRESS_SOCIAL_LINKS_VERSION', 'name' => 'HivePress Social Links', ), 'hivepress-statistics' => array( 'class' => 'HivePress\\Statistics\\Plugin', 'const' => 'HIVEPRESS_STATISTICS_VERSION', 'name' => 'HivePress Statistics', ), 'hivepress-tags' => array( 'class' => 'HivePress\\Tags\\Plugin', 'const' => 'HIVEPRESS_TAGS_VERSION', 'name' => 'HivePress Tags', ), ); /** * Detect installed HivePress family addons. * * Returns map of slug → version-string. Empty array when HivePress * core is not installed (zero-overhead short circuit for non-HP sites). * * @return array */ public static function detect(): array { if ( null !== self::$memo ) { return self::$memo; } $cached = function_exists( 'get_transient' ) ? get_transient( self::CACHE_KEY ) : false; if ( is_array( $cached ) ) { self::$memo = $cached; return $cached; } // Short-circuit: if HivePress core is not present, no addon can be loaded. if ( ! self::probe_one( self::PROBES['hivepress'] ) ) { self::$memo = array(); if ( function_exists( 'set_transient' ) ) { set_transient( self::CACHE_KEY, self::$memo, self::CACHE_TTL ); } return self::$memo; } $found = array(); foreach ( self::PROBES as $slug => $probe ) { if ( self::probe_one( $probe ) ) { $found[ $slug ] = self::version_for( $probe ); } } self::$memo = $found; if ( function_exists( 'set_transient' ) ) { set_transient( self::CACHE_KEY, $found, self::CACHE_TTL ); } return $found; } /** * Force re-detection on next call. * * Call from plugin activation / deactivation hooks if the listening * code wants up-to-date detection state immediately. */ public static function bust_cache(): void { self::$memo = null; if ( function_exists( 'delete_transient' ) ) { delete_transient( self::CACHE_KEY ); } } /** * Reset internal state for unit tests. * * @internal */ public static function reset_for_tests(): void { self::$memo = null; } /** * Catalog of supported addons for UI display (slug => human-readable name). * * @return array */ public static function catalog(): array { $out = array(); foreach ( self::PROBES as $slug => $probe ) { $out[ $slug ] = $probe['name']; } return $out; } /** * Whether a given addon slug is in the supported catalog. * * @param string $slug Addon slug. */ public static function is_known( string $slug ): bool { return isset( self::PROBES[ $slug ] ); } // ── Internal probes ───────────────────────────────────────────────── /** * Probe a single addon. Either the class OR the const must exist. * * @param array{class:string, const:string, name:string} $probe Probe definition. */ private static function probe_one( array $probe ): bool { $class = (string) ( $probe['class'] ?? '' ); $const = (string) ( $probe['const'] ?? '' ); if ( '' !== $class && class_exists( $class ) ) { return true; } if ( '' !== $const && defined( $const ) ) { return true; } return false; } /** * Best-effort version extraction. Empty string when no version available. * * @param array{class:string, const:string, name:string} $probe Probe definition. */ private static function version_for( array $probe ): string { $const = (string) ( $probe['const'] ?? '' ); if ( '' !== $const && defined( $const ) ) { $value = constant( $const ); if ( is_string( $value ) || is_numeric( $value ) ) { return (string) $value; } } return ''; } } } // end if ( ! class_exists )