100, self::INFO => 200, self::NOTICE => 250, self::WARNING => 300, self::ERROR => 400, self::CRITICAL => 500, self::ALERT => 550, self::EMERGENCY => 600, ]; private string $channel; public function __construct(string $channel = 'app') { $this->channel = $channel; } public static function channel(string $channel = 'app'): self { return new self($channel); } public function emergency(string $message, array $context = []): void { $this->log(self::EMERGENCY, $message, $context); } public function alert(string $message, array $context = []): void { $this->log(self::ALERT, $message, $context); } public function critical(string $message, array $context = []): void { $this->log(self::CRITICAL, $message, $context); } public function error(string $message, array $context = []): void { $this->log(self::ERROR, $message, $context); } public function warning(string $message, array $context = []): void { $this->log(self::WARNING, $message, $context); } public function notice(string $message, array $context = []): void { $this->log(self::NOTICE, $message, $context); } public function info(string $message, array $context = []): void { $this->log(self::INFO, $message, $context); } public function debug(string $message, array $context = []): void { $this->log(self::DEBUG, $message, $context); } /** * Log an exception with its class, message, and location. */ public function exception(\Throwable $e, string $level = self::ERROR, array $context = []): void { $context = array_merge($context, [ 'exception' => get_class($e), 'file' => $e->getFile() . ':' . $e->getLine(), ]); $this->log($level, $e->getMessage(), $context); } public function log(string $level, string $message, array $context = []): void { if (!isset(self::$levels[$level])) { $level = self::INFO; } // Suppress debug noise unless explicitly debugging. if ($level === self::DEBUG && !(defined('WP_DEBUG') && WP_DEBUG)) { return; } $line = sprintf( '[softminal_cache_%s] %s %s', $this->channel, strtoupper($level), $this->interpolate($message, $context) ); if (!empty($context)) { $encoded = function_exists('wp_json_encode') ? wp_json_encode($context) : json_encode($context); if ($encoded !== false) { $line .= ' ' . $encoded; } } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log($line); /** * Allow external log sinks (Sentry, files, etc.) to observe entries. */ do_action('softminal_cache_log', $level, $message, $context, $this->channel); } /** * Replace {placeholders} in the message with scalar context values. */ private function interpolate(string $message, array $context): string { if (strpos($message, '{') === false) { return $message; } $replacements = []; foreach ($context as $key => $value) { if (is_scalar($value) || $value === null || (is_object($value) && method_exists($value, '__toString'))) { $replacements['{' . $key . '}'] = (string) $value; } } return strtr($message, $replacements); } }