'Object Cache', 'icon' => 'Server', 'description' => 'Configure a persistent object cache (Redis / Memcached) and generate a paste-ready wp-config.php snippet.', 'custom_panel' => 'ObjectCachePanel', ); } public function settings_schema(): array { return array( 'backend' => array( 'type' => 'enum', 'default' => 'redis', 'options' => array( 'redis', 'memcached' ), 'option_labels' => array( 'redis' => 'Redis', 'memcached' => 'Memcached', ), 'label' => 'Backend', 'description' => 'Which cache server you intend to use. Affects the generated wp-config snippet.', ), 'redis_host' => array( 'type' => 'string', 'default' => '127.0.0.1', 'label' => 'Redis Host', 'description' => 'Hostname or IP of the Redis server. Use 127.0.0.1 for a local socket on the same machine as PHP.', ), 'redis_port' => array( 'type' => 'int', 'default' => 6379, 'min' => 1, 'max' => 65535, 'label' => 'Redis Port', 'description' => 'Default Redis port is 6379.', ), 'redis_user' => array( 'type' => 'string', 'default' => '', 'label' => 'Redis User', 'description' => 'Optional. Set this only if your host provisioned a dedicated Redis ACL user (Redis 6+) โ€” e.g. some managed hosts issue a Redis User alongside the password. Leave blank to authenticate as the default user (legacy password-only Redis).', ), 'redis_password' => array( 'type' => 'string', 'default' => '', 'label' => 'Redis Password', 'description' => 'Leave blank if your Redis server runs without auth.', ), 'redis_database' => array( 'type' => 'int', 'default' => 0, 'min' => 0, 'max' => 15, 'label' => 'Redis Database', 'description' => 'Redis logical DB number (0-15). Use a dedicated DB per site if Redis is shared.', ), 'memcached_host' => array( 'type' => 'string', 'default' => '127.0.0.1', 'label' => 'Memcached Host', 'description' => 'Used when Backend = Memcached.', ), 'memcached_port' => array( 'type' => 'int', 'default' => 11211, 'min' => 1, 'max' => 65535, 'label' => 'Memcached Port', 'description' => 'Default Memcached port is 11211.', ), 'key_prefix' => array( 'type' => 'string', 'default' => '', 'label' => 'Cache Key Prefix', 'description' => 'Unique salt for this site\'s cache keys. Critical when multiple WP sites share one Redis/Memcached server. On ACL/namespaced Redis (e.g. xCloud), this MUST match the host\'s "Redis Object Cache Key" โ€” otherwise cache writes are denied (NOPERM) and nothing persists.', ), 'connection_timeout' => array( 'type' => 'int', 'default' => 1, 'min' => 0, 'max' => 60, 'label' => 'Connection Timeout (seconds)', 'description' => 'How long to wait for a connection. Keep low (1-2s) so a misconfigured cache never stalls the page.', ), 'persistent' => array( 'type' => 'bool', 'default' => true, 'label' => 'Persistent Connections', 'description' => 'Reuse the connection across PHP requests when supported. Generally a win unless the cache server complains about idle connections.', ), ); } public function rest_routes(): array { $default = parent::rest_routes(); return array_merge( $default, array( array( 'path' => '/detect', 'methods' => 'GET', 'callback' => array( $this, 'rest_detect' ), ), array( 'path' => '/flush', 'methods' => 'POST', 'callback' => array( $this, 'rest_flush' ), ), array( 'path' => '/snippet', 'methods' => 'GET', 'callback' => array( $this, 'rest_snippet' ), ), array( 'path' => '/test-connection', 'methods' => 'POST', 'callback' => array( $this, 'rest_test_connection' ), ), array( 'path' => '/enable', 'methods' => 'POST', 'callback' => array( $this, 'rest_enable' ), ), array( 'path' => '/disable', 'methods' => 'POST', 'callback' => array( $this, 'rest_disable' ), ), ) ); } public function rest_detect( \WP_REST_Request $request ) { return rest_ensure_response( Object_Cache::detect() ); } public function rest_flush( \WP_REST_Request $request ) { $ok = Object_Cache::flush(); if ( $ok && class_exists( '\\XSpeed\\Activity_Log' ) ) { \XSpeed\Activity_Log::record( 'object_cache_flushed', 'Object cache flushed.', \XSpeed\Activity_Log::INFO ); } return rest_ensure_response( array( 'ok' => $ok ) ); } public function rest_snippet( \WP_REST_Request $request ) { return rest_ensure_response( array( 'snippet' => Object_Cache::render_config_snippet( $this->get_settings() ) ) ); } /** * Merge any settings sent in the request body over the saved settings, so * the UI can "Test connection" with unsaved values. Only known keys pass. */ private function settings_with_overrides( \WP_REST_Request $request ): array { $settings = $this->get_settings(); $body = $request->get_json_params(); if ( is_array( $body ) ) { foreach ( $settings as $key => $value ) { if ( array_key_exists( $key, $body ) ) { $settings[ $key ] = $body[ $key ]; } } } return $settings; } public function rest_test_connection( \WP_REST_Request $request ) { return rest_ensure_response( Object_Cache::test_connection( $this->settings_with_overrides( $request ) ) ); } public function rest_enable( \WP_REST_Request $request ) { // Persist any settings sent with the enable call first, then act on them. $body = $request->get_json_params(); if ( is_array( $body ) && ! empty( $body ) ) { \XSpeed\Settings_Manager::update( self::SLUG, $body ); } $result = Object_Cache::enable( $this->get_settings() ); if ( $result['ok'] && class_exists( '\\XSpeed\\Activity_Log' ) ) { \XSpeed\Activity_Log::record( 'object_cache_enabled', 'Object cache enabled (' . ( $result['test']['backend'] ?? '' ) . ').', \XSpeed\Activity_Log::INFO ); } return rest_ensure_response( $result ); } public function rest_disable( \WP_REST_Request $request ) { $result = Object_Cache::disable(); if ( $result['ok'] && class_exists( '\\XSpeed\\Activity_Log' ) ) { \XSpeed\Activity_Log::record( 'object_cache_disabled', 'Object cache disabled.', \XSpeed\Activity_Log::INFO ); } return rest_ensure_response( $result ); } public function cli_commands(): array { return array( array( 'name' => 'xspeed objcache', 'callback' => array( $this, 'cli_handler' ), 'shortdesc' => 'Show object cache status, flush, or print the wp-config snippet.', 'synopsis' => array( array( 'type' => 'positional', 'name' => 'action', 'options' => array( 'status', 'flush', 'snippet', 'enable', 'disable', 'test' ), 'optional' => true, ), ), ), ); } public function cli_handler( array $args, array $assoc ): void { $action = $args[0] ?? 'status'; switch ( $action ) { case 'status': $d = Object_Cache::detect(); \WP_CLI::log( 'drop-in installed: ' . ( $d['drop_in_installed'] ? 'yes' : 'no' ) ); \WP_CLI::log( 'label: ' . $d['drop_in_label'] ); \WP_CLI::log( 'backend: ' . $d['backend'] ); \WP_CLI::log( 'ext object cache: ' . ( $d['wp_cache_active'] ? 'yes' : 'no' ) ); return; case 'flush': $ok = Object_Cache::flush(); $ok ? \WP_CLI::success( 'Flushed.' ) : \WP_CLI::error( 'Flush failed.' ); return; case 'snippet': \WP_CLI::log( Object_Cache::render_config_snippet( $this->get_settings() ) ); return; case 'test': $t = Object_Cache::test_connection( $this->get_settings() ); $t['ok'] ? \WP_CLI::success( $t['message'] ) : \WP_CLI::error( $t['message'] ); return; case 'enable': $r = Object_Cache::enable( $this->get_settings() ); $r['ok'] ? \WP_CLI::success( $r['message'] ) : \WP_CLI::error( $r['message'] ); return; case 'disable': $r = Object_Cache::disable(); $r['ok'] ? \WP_CLI::success( $r['message'] ) : \WP_CLI::error( $r['message'] ); return; default: \WP_CLI::error( "Unknown action: $action" ); } } }