diff --git a/CHANGELOG.md b/CHANGELOG.md index be2af386..6a852631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ The format is based on Keep a Changelog. ### Changed - The `DemoWeb` and `Toolkit` module templates redirect back with `back()` instead of `redirect(get_referrer() ?? base_url())` and `redirectWith(get_referrer() ?? base_url(), $data)`, so generated code gets the same-origin check. Modules generated before keep their code (#541) +### Fixed +- `logger()->info([...])` and the other `Logger` methods no longer throw a `TypeError` for an array message. The message is reported to the adapters as pretty-printed JSON, so the file adapters write it as before, and the original array travels in the `payload` key of the context: the debug-mode `MessageAdapter` keeps it as the array, so the debugbar `messages` tab shows it as an array. `ReportableInterface` is unchanged. The `Logger` level methods and `log()` now accept `string|Stringable|array`, so a class that extends `Logger` and overrides one of them with `string|Stringable` must accept an array too, since PHP rejects a narrower parameter in an override (#613) + ## [3.1.0] - 2026-10-24 ### Added diff --git a/src/Logger/Adapters/MessageAdapter.php b/src/Logger/Adapters/MessageAdapter.php index 1832a3b8..76cafad1 100644 --- a/src/Logger/Adapters/MessageAdapter.php +++ b/src/Logger/Adapters/MessageAdapter.php @@ -20,8 +20,8 @@ class MessageAdapter implements ReportableInterface { /** - * Reported messages, as [level => message] - * @var array> + * Reported messages, as [level => message]; an array message is kept as the array + * @var array>> */ private array $messages = []; @@ -37,7 +37,7 @@ public function __construct(int $limit = 1000) public function report(string $level, string|Stringable $message, ?array $context = []): void { - $this->messages[] = [$level => $message]; + $this->messages[] = [$level => $context['payload'] ?? $message]; if ($this->limit > 0 && count($this->messages) > $this->limit) { $this->messages = array_slice($this->messages, -$this->limit); @@ -46,7 +46,7 @@ public function report(string $level, string|Stringable $message, ?array $contex /** * Gets the reported messages in the order they were reported - * @return array> + * @return array>> */ public function getMessages(): array { diff --git a/src/Logger/Logger.php b/src/Logger/Logger.php index 66a82236..2d95f991 100644 --- a/src/Logger/Logger.php +++ b/src/Logger/Logger.php @@ -35,74 +35,90 @@ public function getAdapter(): ReportableInterface /** * @inheritDoc + * @param string|Stringable|array $message */ - public function emergency(string|Stringable $message, array $context = []): void + public function emergency(string|Stringable|array $message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } /** * @inheritDoc + * @param string|Stringable|array $message */ - public function alert(string|Stringable $message, array $context = []): void + public function alert(string|Stringable|array $message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } /** * @inheritDoc + * @param string|Stringable|array $message */ - public function critical(string|Stringable $message, array $context = []): void + public function critical(string|Stringable|array $message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } /** * @inheritDoc + * @param string|Stringable|array $message */ - public function error(string|Stringable $message, array $context = []): void + public function error(string|Stringable|array $message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } /** * @inheritDoc + * @param string|Stringable|array $message */ - public function warning(string|Stringable $message, array $context = []): void + public function warning(string|Stringable|array $message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); } /** * @inheritDoc + * @param string|Stringable|array $message */ - public function notice(string|Stringable $message, array $context = []): void + public function notice(string|Stringable|array $message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } /** * @inheritDoc + * @param string|Stringable|array $message */ - public function info(string|Stringable $message, array $context = []): void + public function info(string|Stringable|array $message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } /** * @inheritDoc + * @param string|Stringable|array $message */ - public function debug(string|Stringable $message, array $context = []): void + public function debug(string|Stringable|array $message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } /** + * Logs a message. An array message is reported as pretty-printed JSON, and the array itself + * is passed to the adapter in the `payload` context key * @inheritDoc + * @param string|Stringable|array $message */ - public function log($level, string|Stringable $message, array $context = []): void + public function log($level, string|Stringable|array $message, array $context = []): void { if (is_debug_mode() || LoggerConfig::getLogLevel($level) >= LoggerConfig::getAppLogLevel()) { + if (is_array($message)) { + $context['payload'] = $message; + $message = json_encode($message, JSON_PRETTY_PRINT | JSON_PARTIAL_OUTPUT_ON_ERROR) ?: print_r($message, true); + } + $this->adapter->report($level, $message, $context); } } diff --git a/tests/Unit/Debugger/DebuggerTest.php b/tests/Unit/Debugger/DebuggerTest.php index d44eea98..f0095d66 100644 --- a/tests/Unit/Debugger/DebuggerTest.php +++ b/tests/Unit/Debugger/DebuggerTest.php @@ -404,6 +404,20 @@ public function testDebugbarRenderListsTheLoggedMessagesInTheMessagesTab(): void $this->assertSame([['info' => 'First message'], ['warning' => 'Second message']], $tabs[Debugger::MESSAGES]); } + public function testDebugbarRenderListsAnArrayMessageAsAnArrayInTheMessagesTab(): void + { + config()->set('app.debug', true); + + $payload = ['order' => 1042, 'items' => ['a', 'b']]; + + logger()->info($payload); + logger()->warning('A text message'); + + $tabs = $this->renderTabs(); + + $this->assertSame([['info' => $payload], ['warning' => 'A text message']], $tabs[Debugger::MESSAGES]); + } + public function testDebugbarRenderListsTheTransportLogOfTheMailerInTheMailsTab(): void { config()->set('app.debug', true); diff --git a/tests/Unit/Logger/Adapters/MessageAdapterTest.php b/tests/Unit/Logger/Adapters/MessageAdapterTest.php index 99ebf646..ff4b43f4 100644 --- a/tests/Unit/Logger/Adapters/MessageAdapterTest.php +++ b/tests/Unit/Logger/Adapters/MessageAdapterTest.php @@ -29,6 +29,22 @@ public function testReportKeepsTheMessagesInOrder(): void $this->assertSame([['info' => 'Test message'], ['error' => 'Error message']], $this->adapter->getMessages()); } + public function testReportKeepsTheArrayPayloadOfTheContext(): void + { + $payload = ['order' => 1042, 'items' => ['a', 'b']]; + + $this->adapter->report('info', json_encode($payload, JSON_PRETTY_PRINT), ['payload' => $payload]); + + $this->assertSame([['info' => $payload]], $this->adapter->getMessages()); + } + + public function testReportKeepsTheMessageWhenTheContextHasNoPayload(): void + { + $this->adapter->report('info', 'Test message', ['trace' => 'the trace']); + + $this->assertSame([['info' => 'Test message']], $this->adapter->getMessages()); + } + public function testReportKeepsOnlyTheLastMessagesOverTheLimit(): void { $adapter = new MessageAdapter(2); diff --git a/tests/Unit/Logger/LoggerTest.php b/tests/Unit/Logger/LoggerTest.php index aa40beab..fdf54942 100644 --- a/tests/Unit/Logger/LoggerTest.php +++ b/tests/Unit/Logger/LoggerTest.php @@ -67,6 +67,86 @@ public function __toString(): string $this->assertSame([['critical' => $message]], $this->adapter->getMessages()); } + /** + * @dataProvider levelMethodProvider + */ + public function testLoggerReportsAnArrayMessageAsPrettyPrintedJson(string $method, string $level): void + { + $adapter = $this->recordingAdapter(); + + $payload = ['order' => 1042, 'status' => 'paid']; + + (new Logger($adapter))->$method($payload); + + $this->assertCount(1, $adapter->reports); + + $this->assertSame($level, $adapter->reports[0]['level']); + + $this->assertSame(json_encode($payload, JSON_PRETTY_PRINT), $adapter->reports[0]['message']); + } + + public function testLoggerLogReportsAnArrayMessage(): void + { + $adapter = $this->recordingAdapter(); + + (new Logger($adapter))->log(LogLevel::NOTICE, ['key' => 'value']); + + $this->assertSame(json_encode(['key' => 'value'], JSON_PRETTY_PRINT), $adapter->reports[0]['message']); + } + + public function testLoggerPassesTheOriginalArrayInTheContext(): void + { + $adapter = $this->recordingAdapter(); + + $payload = ['order' => 1042, 'items' => ['a', 'b']]; + + (new Logger($adapter))->info($payload, ['trace' => 'the trace']); + + $this->assertSame($payload, $adapter->reports[0]['context']['payload']); + + $this->assertSame('the trace', $adapter->reports[0]['context']['trace']); + } + + public function testLoggerReportsAnArrayThatCannotBeEncodedAsAString(): void + { + $adapter = $this->recordingAdapter(); + + (new Logger($adapter))->info(['name' => 'order', 'value' => NAN, 'text' => "\xB1\x31"]); + + $this->assertIsString($adapter->reports[0]['message']); + + $this->assertStringContainsString('order', $adapter->reports[0]['message']); + } + + public function testLoggerLeavesTheContextOfAStringMessageAlone(): void + { + $adapter = $this->recordingAdapter(); + + (new Logger($adapter))->info('Some message', ['trace' => 'the trace']); + + $this->assertSame('Some message', $adapter->reports[0]['message']); + + $this->assertSame(['trace' => 'the trace'], $adapter->reports[0]['context']); + } + + /** + * An adapter that records what it is asked to report + */ + private function recordingAdapter(): ReportableInterface + { + return new class () implements ReportableInterface { + /** + * @var array> + */ + public array $reports = []; + + public function report(string $level, string|Stringable $message, ?array $context = []): void + { + $this->reports[] = ['level' => $level, 'message' => $message, 'context' => $context]; + } + }; + } + /** * @return array */