$context Structured context. * @return void */ private static function write_info( string $event, array $context ): void { error_log( sprintf( '[WPDO][INFO][%s] %s', $event, $context ? wp_json_encode( $context, JSON_UNESCAPED_UNICODE ) : '' ) ); } /** * Log a NOTICE-level event — alias of info() for callers that want a stronger * level intent. Same routing (error_log only). * * @param string $event Event name. * @param array $context Structured context. * @return void */ public static function notice( string $event, array $context = array() ): void { self::info( $event, $context ); } /** * Log a WARNING-level event — error_log + wpdo_errors row (for admin visibility). * * Use for "anomaly worth attention but not breaking" (e.g. invalid filter * return, unexpected fallback path). The event_name becomes the `hook` column; * message gets a `[WARN]` prefix to distinguish from hard errors. * * @param string $event Event name (e.g. 'wpdo_route_decision_invalid_return'). * @param array $context Structured context. * @return void */ public static function warning( string $event, array $context = array() ): void { error_log( sprintf( '[WPDO][WARN][%s] %s', $event, $context ? wp_json_encode( $context, JSON_UNESCAPED_UNICODE ) : '' ) ); self::error( 'engine', $event, '[WARN] ' . $event, $context ); } /** * Log a DEBUG-level event — only when WP_DEBUG is true; routes to error_log. * * @param string $event Event name. * @param array $context Structured context. * @return void */ public static function debug( string $event, array $context = array() ): void { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { self::write_info( $event, $context ); } } /** * Log a module error. * * @param string $module Module name (e.g. 'reviews', 'hot_hp_listing'). * @param string $hook The hook or method where the error occurred. * @param string $message Human-readable error message. * @param array $context Optional extra context (will be JSON-encoded). * @param string $zone Optional zone identifier (hot/warm/cold/archive). */ public static function error( string $module, string $hook, string $message, array $context = array(), string $zone = '' ): void { global $wpdb; error_log( sprintf( '[WPDO][%s][%s] %s', $module, $hook, $message ) ); $table = TMDO_DB::table( 'wpdo_errors' ); $wpdb->insert( $table, array( 'module' => sanitize_key( $module ), 'zone' => sanitize_key( $zone ), 'hook' => substr( sanitize_text_field( $hook ), 0, 255 ), 'message' => $message, 'context' => $context ? wp_json_encode( $context, JSON_UNESCAPED_UNICODE ) : null, 'created_at' => current_time( 'mysql', true ), ), array( '%s', '%s', '%s', '%s', '%s', '%s' ) ); } /** * Return recent errors, optionally filtered by module. * * @param string $module Module name (empty = all modules). * @param int $limit Max rows to return. * @return array */ public static function get_recent( string $module = '', int $limit = 100 ): array { global $wpdb; $table = TMDO_DB::table( 'wpdo_errors' ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from TMDO_DB::table(). if ( $module ) { return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$table}` WHERE module = %s ORDER BY id DESC LIMIT %d", $module, $limit ), ARRAY_A ) ?: array(); } return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$table}` ORDER BY id DESC LIMIT %d", $limit ), ARRAY_A ) ?: array(); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Delete errors older than a given number of days. * * @param int $days Keep errors from the last N days. * @return int Number of rows deleted. */ public static function purge( int $days = 30 ): int { global $wpdb; $table = TMDO_DB::table( 'wpdo_errors' ); $now = current_time( 'mysql', true ); if ( TMDO_IS_SQLITE ) { $sql = $wpdb->prepare( "DELETE FROM `{$table}` WHERE created_at < datetime(%s, %s)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $now, "-{$days} days" ); } else { $sql = $wpdb->prepare( "DELETE FROM `{$table}` WHERE created_at < DATE_SUB(%s, INTERVAL %d DAY)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $now, $days ); } return (int) $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } }