Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,47 @@ A comprehensive WordPress logging service with Wonolog integration, secure file
- **Zero Dependencies**: Works with or without external logging libraries
- **Plugin Isolation**: Each plugin/theme gets independent log directories and settings

## Design Philosophy: Mixed Types for Maximum Flexibility

WP Logger intentionally uses `mixed` type hints throughout its API, diverging from strict PSR Log ^2.0+ signatures. This design choice aligns with our primary integration target and WordPress ecosystem philosophy:

### Why Mixed Types?

**Wonolog Compatibility**: Our primary target, [Inpsyde Wonolog](https://github.com/inpsyde/Wonolog), accepts truly mixed types natively (Throwable, Arrays, Objects, WP_Error):
```php
// Wonolog native usage
do_action('wonolog.log', $exception); // ✅ Throwable
do_action('wonolog.log', ['data' => 'log']); // ✅ Array
do_action('wonolog.log', $wpError); // ✅ WP_Error
```

**PSR Log Version Differences**:
- **PSR Log ^1.0**: Uses `mixed` type hint but primarily expects strings
- **PSR Log ^2.0+**: Strictly enforces `string|\Stringable` only

**WordPress Ecosystem**: WordPress prioritizes pragmatism and developer experience over strict typing. Our design reflects this philosophy.

### Developer Benefits

```php
// ✅ Natural and intuitive - all supported
$logger->error($exception); // Throwable objects
$logger->warning($wpError); // WP_Error objects
$logger->info(['user' => 123]); // Structured data
$logger->debug($customObject); // Any object

// vs. ❌ Verbose PSR ^2.0+ requirement
$logger->error($exception->getMessage(), ['exception' => $exception]);
```

### How It Works

- **With Wonolog**: Mixed types passed through natively without conversion
- **Fallback Mode**: Mixed types converted to strings via `formatMessage()`
- **Best of Both**: WordPress flexibility + PSR compatibility + Wonolog power

This approach ensures zero breaking changes while maximizing compatibility across the WordPress ecosystem.

## Installation

Install via Composer:
Expand Down
181 changes: 102 additions & 79 deletions src/Logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,69 @@ public function getConfig(): array
}

/**
* System is unusable.
* Logs with an arbitrary level with environment-aware behavior.
*
* MESSAGE TYPE COMPATIBILITY:
*
* - Wonolog (primary target): Accepts truly mixed types natively
* ✅ Throwable, WP_Error, Arrays, Objects, Strings - all supported
*
* - PSR Log ^1.0: Uses mixed type hint but expects primarily strings
* ⚠️ Throwable/Objects/Arrays not officially supported by spec
*
* - PSR Log ^2.0+: Strictly enforces string|\Stringable only
* ❌ Throwable, Arrays, Objects cause TypeError
*
* NOTE: Type hints removed for PSR Log ^1.0 compatibility.
* TODO: Add back `string|\Stringable $message` type hint when PSR Log ^1.0 support is dropped.
* DESIGN DECISION:
* WP Logger maintains Wonolog-compatible design for maximum flexibility
* in the WordPress ecosystem. When Wonolog is active, all mixed types
* are passed through natively. In fallback mode, types are converted
* to strings via formatMessage().
*
* @param mixed $message
* ACCEPTED TYPES:
* - Throwable objects: $logger->error($exception)
* - WP_Error objects: $logger->error($wpError)
* - Strings: $logger->error('message')
* - Arrays: $logger->error(['user' => 123])
* - Objects: $logger->error($customObject)
*
* @param mixed $level
* @param mixed $message - Accepts any type (Throwable, WP_Error, string, array, object)
* @param array<string, mixed> $context
*
* @throws \Psr\Log\InvalidArgumentException
*/
public function log($level, $message, array $context = []): void
{
// Check if this log level should be recorded
if (!$this->shouldLog($level)) {
return;
}

// Allow hook to override entire logging behavior
if (\function_exists('apply_filters')) {
$overrideResult = apply_filters('wp_logger_override_log', null, $level, $message, $context, $this->config);
if (null !== $overrideResult) {
return; // Custom logging handler took over
}
}

if ($this->isWonologActive()) {
$this->logViaWonolog($level, $message, $context);
} else {
$this->logViaFallback($level, $message, $context);
}

// Allow third-party plugins to hook into all logging
if (\function_exists('do_action')) {
do_action('wp_logger_logged', $level, $message, $context, $this->config['plugin_name']);
}
}

/**
* System is unusable.
*
* @param mixed $message - Accepts any type (Throwable, WP_Error, string, array, object)
*/
public function emergency($message, array $context = []): void
{
Expand All @@ -155,10 +212,7 @@ public function emergency($message, array $context = []): void
/**
* Action must be taken immediately.
*
* NOTE: Type hints removed for PSR Log ^1.0 compatibility.
* TODO: Add back `string|\Stringable $message` type hint when PSR Log ^1.0 support is dropped.
*
* @param mixed $message
* @param mixed $message - Accepts any type (Throwable, WP_Error, string, array, object)
*/
public function alert($message, array $context = []): void
{
Expand All @@ -168,10 +222,7 @@ public function alert($message, array $context = []): void
/**
* Critical conditions.
*
* NOTE: Type hints removed for PSR Log ^1.0 compatibility.
* TODO: Add back `string|\Stringable $message` type hint when PSR Log ^1.0 support is dropped.
*
* @param mixed $message
* @param mixed $message - Accepts any type (Throwable, WP_Error, string, array, object)
*/
public function critical($message, array $context = []): void
{
Expand All @@ -182,10 +233,7 @@ public function critical($message, array $context = []): void
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* NOTE: Type hints removed for PSR Log ^1.0 compatibility.
* TODO: Add back `string|\Stringable $message` type hint when PSR Log ^1.0 support is dropped.
*
* @param mixed $message
* @param mixed $message - Accepts any type (Throwable, WP_Error, string, array, object)
*/
public function error($message, array $context = []): void
{
Expand All @@ -195,10 +243,7 @@ public function error($message, array $context = []): void
/**
* Exceptional occurrences that are not errors.
*
* NOTE: Type hints removed for PSR Log ^1.0 compatibility.
* TODO: Add back `string|\Stringable $message` type hint when PSR Log ^1.0 support is dropped.
*
* @param mixed $message
* @param mixed $message - Accepts any type (Throwable, WP_Error, string, array, object)
*/
public function warning($message, array $context = []): void
{
Expand All @@ -208,10 +253,7 @@ public function warning($message, array $context = []): void
/**
* Normal but significant events.
*
* NOTE: Type hints removed for PSR Log ^1.0 compatibility.
* TODO: Add back `string|\Stringable $message` type hint when PSR Log ^1.0 support is dropped.
*
* @param mixed $message
* @param mixed $message - Accepts any type (Throwable, WP_Error, string, array, object)
*/
public function notice($message, array $context = []): void
{
Expand All @@ -221,10 +263,7 @@ public function notice($message, array $context = []): void
/**
* Interesting events.
*
* NOTE: Type hints removed for PSR Log ^1.0 compatibility.
* TODO: Add back `string|\Stringable $message` type hint when PSR Log ^1.0 support is dropped.
*
* @param mixed $message
* @param mixed $message - Accepts any type (Throwable, WP_Error, string, array, object)
*/
public function info($message, array $context = []): void
{
Expand All @@ -234,55 +273,13 @@ public function info($message, array $context = []): void
/**
* Detailed debug information.
*
* NOTE: Type hints removed for PSR Log ^1.0 compatibility.
* TODO: Add back `string|\Stringable $message` type hint when PSR Log ^1.0 support is dropped.
*
* @param mixed $message
* @param mixed $message - Accepts any type (Throwable, WP_Error, string, array, object)
*/
public function debug($message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}

/**
* Logs with an arbitrary level with environment-aware behavior.
*
* NOTE: Type hints removed for PSR Log ^1.0 compatibility.
* TODO: Add back `string|\Stringable $message` type hint when PSR Log ^1.0 support is dropped.
*
* @param mixed $level
* @param mixed $message
* @param array<string, mixed> $context
*
* @throws \Psr\Log\InvalidArgumentException
*/
public function log($level, $message, array $context = []): void
{
// Check if this log level should be recorded
if (!$this->shouldLog($level)) {
return;
}

// Allow hook to override entire logging behavior
if (\function_exists('apply_filters')) {
$overrideResult = apply_filters('wp_logger_override_log', null, $level, $message, $context, $this->config);
if (null !== $overrideResult) {
return; // Custom logging handler took over
}
}

if ($this->isWonologActive()) {
$this->logViaWonolog($level, $message, $context);
} else {
$this->logViaFallback($level, $message, $context);
}

// Allow third-party plugins to hook into all logging
if (\function_exists('do_action')) {
do_action('wp_logger_logged', $level, $message, $context, $this->config['plugin_name']);
}
}

/**
* Get Wonolog logger instance if available.
*/
Expand Down Expand Up @@ -497,11 +494,21 @@ private function getWonologNamespace(): string
}

/**
* Log via Wonolog with hook support.
* Log via Wonolog with native mixed type support.
*
* Unlike PSR Log ^2.0+ (string|\Stringable only), Wonolog was designed
* for WordPress ecosystem flexibility and accepts truly mixed types:
* - Throwable objects (exceptions)
* - WP_Error objects
* - Arrays and Objects (structured data)
* - Strings and primitives
*
* This allows natural WordPress-style logging without type conversions.
*
* @param mixed $message - Accepts mixed types, conversion happens in formatMessage()
* @param array<string, mixed> $context
*/
private function logViaWonolog(string $level, string|\Stringable $message, array $context): void
private function logViaWonolog(string $level, mixed $message, array $context): void
{
$wonologNamespace = $this->getWonologNamespace();
$logConstant = $wonologNamespace.'\LOG';
Expand Down Expand Up @@ -540,9 +547,10 @@ private function logViaWonolog(string $level, string|\Stringable $message, array
/**
* Fallback logging when Wonolog is not available with environment-aware behavior.
*
* @param mixed $message - Accepts mixed types, conversion happens in formatMessage()
* @param array<string, mixed> $context
*/
private function logViaFallback(string $level, string|\Stringable $message, array $context): void
private function logViaFallback(string $level, mixed $message, array $context): void
{
// Allow third-party plugins to handle logging when Wonolog is not available
if (\function_exists('do_action')) {
Expand Down Expand Up @@ -660,9 +668,10 @@ private function getLogDirectory(): string
/**
* Log to protected file in WordPress uploads directory.
*
* @param mixed $message - Passes mixed message to formatLogEntry() for conversion
* @param array<string, mixed> $context
*/
private function logToFile(string $level, string|\Stringable $message, array $context): void
private function logToFile(string $level, mixed $message, array $context): void
{
if (!\function_exists('wp_upload_dir') || !\function_exists('wp_mkdir_p')) {
return;
Expand Down Expand Up @@ -700,9 +709,10 @@ private function createLogDirectoryStructure(string $logDir): void
/**
* Format log entry for consistent output.
*
* @param mixed $message - Calls formatMessage() to convert mixed → string
* @param array<string, mixed> $context
*/
private function formatLogEntry(string $level, string|\Stringable $message, array $context): string
private function formatLogEntry(string $level, mixed $message, array $context): string
{
$timestamp = gmdate('Y-m-d H:i:s');
$environmentInfo = Environment::isProduction() ? '' : ' ['.Environment::getEnvironment().']';
Expand Down Expand Up @@ -760,9 +770,10 @@ private function maybeCleanupLogs(string $logDir): void
/**
* Traditional error_log (used in development/debug mode).
*
* @param mixed $message - Calls formatMessage() to convert mixed → string
* @param array<string, mixed> $context
*/
private function logToErrorLog(string $level, string|\Stringable $message, array $context): void
private function logToErrorLog(string $level, mixed $message, array $context): void
{
$environmentInfo = '['.Environment::getEnvironment().']';

Expand All @@ -786,9 +797,21 @@ private function logToErrorLog(string $level, string|\Stringable $message, array
}

/**
* Format message for consistent logging output.
* Format message for fallback logging (when Wonolog not available).
*
* Converts mixed types to strings for traditional logging methods.
* Only used in fallback mode - when Wonolog is active, mixed types
* are passed through natively without conversion.
*
* SUPPORTED CONVERSIONS:
* - Throwable → "Message in file.php:123"
* - WP_Error → error message string
* - Arrays/Objects → JSON representation
* - Primitives → string cast
*
* @param mixed $message - Accepts any type (Throwable, WP_Error, string, array, object)
*/
private function formatMessage(string|\Stringable $message): string
private function formatMessage(mixed $message): string
{
// Handle Throwable objects
if ($message instanceof \Throwable) {
Expand Down