Logger.php
PHP
Path: src/Logger/Logger.php
<?php
namespace mini\Logger;
use mini\Mini;
use Psr\Log\AbstractLogger;
use Psr\Log\LogLevel;
/**
* Built-in logger implementation that logs to PHP's error_log
*
* Uses MessageFormatter for string interpolation with the application's default locale.
* Supports all PSR-3 log levels.
*/
class Logger extends AbstractLogger
{
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string|\Stringable $message
* @param array $context
* @return void
*/
public function log($level, string|\Stringable $message, array $context = []): void
{
$message = (string) $message;
// Interpolate message with context using MessageFormatter
if (!empty($context)) {
$interpolated = $this->interpolate($message, $context);
} else {
$interpolated = $message;
}
// Format log entry
$formattedMessage = $this->formatLogEntry($level, $interpolated, $context);
// Write to PHP error log
error_log($formattedMessage);
}
/**
* Interpolate message with context values using MessageFormatter
*
* @param string $message
* @param array $context
* @return string
*/
private function interpolate(string $message, array $context): string
{
// Preprocess context values for MessageFormatter compatibility
$processedContext = $this->preprocessContext($context);
// Use application's default locale (not per-user locale)
$locale = Mini::$mini->locale;
// Try MessageFormatter, fall back to simple replacement on failure
$result = msgfmt_format_message($locale, $message, $processedContext);
return $result !== false
? $result
: $this->simplePlaceholderReplacement($message, $context);
}
/**
* Preprocess context values for MessageFormatter compatibility
*
* Converts non-scalar values to string representations with markdown-style formatting:
* - null → `null`
* - true/false → `true`/`false`
* - arrays → JSON
* - exceptions → skipped (handled separately)
* - objects with __toString → string value
* - other objects → `ClassName`
*
* @param array $context
* @return array
*/
private function preprocessContext(array $context): array
{
$processed = [];
foreach ($context as $key => $value) {
// Skip exception key - it's handled separately in formatLogEntry
if ($key === 'exception' && $value instanceof \Throwable) {
continue;
}
if (is_null($value)) {
$processed[$key] = '`null`';
} elseif (is_bool($value)) {
$processed[$key] = $value ? '`true`' : '`false`';
} elseif (is_scalar($value)) {
$processed[$key] = $value;
} elseif (is_array($value)) {
$processed[$key] = '`' . json_encode($value) . '`';
} elseif (is_object($value) && method_exists($value, '__toString')) {
$processed[$key] = (string) $value;
} elseif (is_object($value)) {
$processed[$key] = '`' . get_class($value) . '`';
} else {
$processed[$key] = '`' . gettype($value) . '`';
}
}
return $processed;
}
/**
* Simple placeholder replacement for basic {key} patterns
*
* Uses preprocessContext for consistent value formatting.
*
* @param string $message
* @param array $context
* @return string
*/
private function simplePlaceholderReplacement(string $message, array $context): string
{
$processed = $this->preprocessContext($context);
$replace = [];
foreach ($processed as $key => $value) {
$replace['{' . $key . '}'] = (string) $value;
}
return strtr($message, $replace);
}
/**
* Format a log entry with timestamp and level
*
* @param mixed $level
* @param string $message
* @param array $context
* @return string
*/
private function formatLogEntry(mixed $level, string $message, array $context): string
{
$timestamp = date('Y-m-d H:i:s');
$levelStr = strtoupper((string) $level);
$formatted = "[{$timestamp}] [{$levelStr}] {$message}";
// Append exception details if present
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
$exception = $context['exception'];
$formatted .= "\n" . $this->formatException($exception);
}
return $formatted;
}
/**
* Format exception for logging
*
* @param \Throwable $exception
* @return string
*/
private function formatException(\Throwable $exception): string
{
return sprintf(
"Exception: %s\nMessage: %s\nFile: %s:%d\nTrace:\n%s",
get_class($exception),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine(),
$exception->getTraceAsString()
);
}
}