module ); } /** * Return true when dual-write is active (writes go to both native + custom). * * @return bool True if module is in a write-active state. */ protected function is_write_active(): bool { return TMDO_Feature_Flags::is_write_active( $this->module ); } /** * Return true when the module should intercept reads or writes. * * @return bool True if module is enabled or write-active. */ protected function is_active(): bool { return $this->is_enabled() || $this->is_write_active(); } /** * Execute a custom-table callable safely. * * If $custom throws, log and return $native_fallback(). * After 3 consecutive errors in a request, disable the module. * * @param callable $custom Custom table read/write callable. * @param callable $native_fallback Original WordPress callable. * @param string $hook Hook name for logging context. * @return mixed * @throws \RuntimeException When the custom callable returns a WP_Error. */ protected function intercept( callable $custom, callable $native_fallback, string $hook = '' ): mixed { if ( ! $this->is_enabled() ) { return $native_fallback(); } try { $result = $custom(); if ( is_wp_error( $result ) ) { throw new \RuntimeException( $result->get_error_message() ); } return $result; } catch ( \Throwable $e ) { $this->handle_error( $hook ?: 'intercept', $e->getMessage(), array( 'exception' => get_class( $e ), 'file' => $e->getFile(), 'line' => $e->getLine(), ) ); return $native_fallback(); } } /** * Dual-write: call native first, then sync to custom table. * Custom failure is non-fatal. * * @param callable $native Original write callable (always executed). * @param callable $custom Custom table write callable. * @param string $hook Hook name for logging. * @return mixed Return value of $native. */ protected function dual_write( callable $native, callable $custom, string $hook = '' ): mixed { $result = $native(); if ( $this->is_write_active() || $this->is_enabled() ) { try { $custom( $result ); } catch ( \Throwable $e ) { $this->handle_error( $hook ?: 'dual_write', $e->getMessage(), array( 'exception' => get_class( $e ), ) ); } } return $result; } /** * Register all hooks. Called by TMDO_Core. * * @return void */ abstract public function register_hooks(): void; // ── Private helpers ─────────────────────────────────────────────────── /** * Per-request consecutive error counter, keyed by module. * * @var array */ private static array $error_counts = array(); /** * Handles an interceptor error and auto-disables the module after 3 consecutive errors. * * @param string $hook Hook name for logging context. * @param string $message Error message. * @param array $context Additional context data. * @return void */ private function handle_error( string $hook, string $message, array $context = array() ): void { TMDO_Logger::error( $this->module, $hook, $message, $context ); self::$error_counts[ $this->module ] = ( self::$error_counts[ $this->module ] ?? 0 ) + 1; if ( self::$error_counts[ $this->module ] >= 3 ) { TMDO_Feature_Flags::reset( $this->module ); TMDO_Logger::error( $this->module, $hook, 'Module auto-disabled after 3 consecutive errors.' ); if ( $this->email_on_error ) { $this->notify_admin( $message ); $this->email_on_error = false; } } } /** * Sends an admin notification email when a module is auto-disabled. * * @param string $message Error message to include in the notification. * @return void */ private function notify_admin( string $message ): void { $admin_email = get_option( 'admin_email' ); if ( ! $admin_email ) { return; } wp_mail( $admin_email, sprintf( '[WPDO] Module "%s" auto-disabled', $this->module ), sprintf( "The WPDO module \"%s\" has been automatically disabled due to repeated errors.\n\nLast error: %s\n\nPlease review the error log at Tools > WP Data Optimizer > Logs.", $this->module, $message ) ); } }