From 1fd606688260953c8c5bc7437b527b24832ed089 Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Wed, 25 Mar 2026 01:14:06 +0600 Subject: [PATCH 1/4] Add customizable context format and value conversion --- CHANGELOG.md | 2 +- README.md | 57 ++++++++ src/Message/Formatter.php | 130 +++++++++++++++++- src/Target.php | 45 +++++++ tests/Message/FormatterTest.php | 227 ++++++++++++++++++++++++++++++++ tests/TargetTest.php | 65 +++++++++ tests/TestAsset/DummyTarget.php | 18 +++ 7 files changed, 537 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c3c52de..42c2073c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.2.2 under development -- no changes in this release. +- New #140: Add `setConvertToString()`, `setContextTemplate()`, and `setContextFormat()` to `Formatter` (@WarLikeLaux) ## 2.2.1 March 22, 2026 diff --git a/README.md b/README.md index 82a15bb7..85f416ba 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,63 @@ $logger = new \Yiisoft\Log\Logger( ); ``` +### Customizing log format + +Each target formats log messages using a built-in `Formatter`. You can customize various aspects of formatting. + +To replace the entire message format, use `setFormat()`: + +```php +$target->setFormat(static function (\Yiisoft\Log\Message $message, array $commonContext): string { + return "[{$message->level()}][{$message->context('category')}] {$message->message()}"; +}); +``` + +To add a prefix to every message: + +```php +$target->setPrefix(static fn() => 'MyApp: '); +``` + +To change the timestamp format: + +```php +$target->setTimestampFormat('Y-m-d H:i:s'); +``` + +To replace how context values are converted to strings (default uses VarDumper): + +```php +$target->setConvertToString(static fn(mixed $value): string => json_encode($value)); +``` + +To reorder context sections (trace, message context, common context) use a template string with +`{trace}`, `{message}`, and `{common}` placeholders. Each placeholder expands to its section with +a header when non-empty, or an empty string when the section has no data: + +```php +$target->setContextTemplate("{common}{message}{trace}\n"); +``` + +For full control over context rendering, use a callable: + +```php +$target->setContextFormat( + static function (string $trace, string $messageContext, string $commonContext): string { + $result = ''; + if ($commonContext !== '') { + $result .= "\n\nCommon:\n" . $commonContext; + } + if ($messageContext !== '') { + $result .= "\n\nMessage:\n" . $messageContext; + } + return $result; + }, +); +``` + +If both `setContextFormat()` and `setContextTemplate()` are set, the callable takes precedence. + ## Documentation - [Yii guide to logging](https://github.com/yiisoft/docs/blob/master/guide/en/runtime/logging.md) diff --git a/src/Message/Formatter.php b/src/Message/Formatter.php index 6563ff00..869e20a1 100644 --- a/src/Message/Formatter.php +++ b/src/Message/Formatter.php @@ -13,6 +13,7 @@ use function is_object; use function method_exists; use function sprintf; +use function str_replace; use function is_int; /** @@ -22,6 +23,38 @@ */ final class Formatter { + /** + * @var callable|null PHP callable that returns a string representation of the log context. + * + * If not set, the default context format will be used. + * + * The signature of the callable should be + * `function (string $trace, string $messageContext, string $commonContext): string;`. + */ + private $contextFormat; + + /** + * @var string|null A template string for the context output. + * + * Supports `{trace}`, `{message}`, and `{common}` placeholders. Each placeholder is replaced with its + * formatted section (including header) if non-empty, or an empty string if the section has no data. + * + * For example, `"{common}{message}{trace}\n"` outputs common context first, then message context, then trace. + * + * If not set, the default context format will be used. + * If {@see Formatter::$contextFormat} is also set, it takes precedence over the template. + */ + private ?string $contextTemplate = null; + + /** + * @var callable|null PHP callable that converts a value to a string. + * + * If not set, {@see Formatter::convertToString()} default logic will be used. + * + * The signature of the callable should be `function (mixed $value): string;`. + */ + private $convertToString; + /** * @var callable|null PHP callable that returns a string representation of the log message. * @@ -46,6 +79,42 @@ final class Formatter */ private string $timestampFormat = 'Y-m-d H:i:s.u'; + /** + * Sets the format for the string representation of the log context. + * + * @param callable $contextFormat The PHP callable to format the log context. + * + * @see Formatter::$contextFormat + */ + public function setContextFormat(callable $contextFormat): void + { + $this->contextFormat = $contextFormat; + } + + /** + * Sets a template string for the context output. + * + * @param string $contextTemplate The template string with `{trace}`, `{message}`, and `{common}` placeholders. + * + * @see Formatter::$contextTemplate + */ + public function setContextTemplate(string $contextTemplate): void + { + $this->contextTemplate = $contextTemplate; + } + + /** + * Sets a PHP callable that converts a value to a string. + * + * @param callable $convertToString The PHP callable to convert a value to a string. + * + * @see Formatter::$convertToString + */ + public function setConvertToString(callable $convertToString): void + { + $this->convertToString = $convertToString; + } + /** * Sets the format for the string representation of the log message. * @@ -172,10 +241,6 @@ private function getContext(Message $message, array $commonContext): string $context = []; $common = []; - if ($trace !== '') { - $context[] = $trace; - } - /** * @var array-key $name * @var mixed $value @@ -193,8 +258,48 @@ private function getContext(Message $message, array $commonContext): string $common[] = "{$name}: " . $this->convertToString($value); } - return (empty($context) ? '' : "\n\nMessage context:\n\n" . implode("\n", $context)) - . (empty($common) ? '' : "\n\nCommon context:\n\n" . implode("\n", $common)) . "\n"; + $messageContext = implode("\n", $context); + $commonContextString = implode("\n", $common); + + if ($this->contextFormat !== null) { + $result = ($this->contextFormat)($trace, $messageContext, $commonContextString); + + if (!is_string($result)) { + throw new RuntimeException(sprintf( + 'The PHP callable "contextFormat" must return a string, %s received.', + get_debug_type($result), + )); + } + + return $result; + } + + if ($this->contextTemplate !== null) { + return str_replace( + ['{trace}', '{message}', '{common}'], + [ + $trace === '' ? '' : "\n\nTrace:\n\n" . $trace, + $messageContext === '' ? '' : "\n\nMessage context:\n\n" . $messageContext, + $commonContextString === '' ? '' : "\n\nCommon context:\n\n" . $commonContextString, + ], + $this->contextTemplate, + ); + } + + $messageItems = []; + + if ($trace !== '') { + $messageItems[] = $trace; + } + + if ($messageContext !== '') { + $messageItems[] = $messageContext; + } + + $messageSection = implode("\n", $messageItems); + + return ($messageSection === '' ? '' : "\n\nMessage context:\n\n" . $messageSection) + . ($commonContextString === '' ? '' : "\n\nCommon context:\n\n" . $commonContextString) . "\n"; } /** @@ -244,6 +349,19 @@ static function (mixed $trace): string { */ private function convertToString(mixed $value): string { + if ($this->convertToString !== null) { + $result = ($this->convertToString)($value); + + if (!is_string($result)) { + throw new RuntimeException(sprintf( + 'The PHP callable "convertToString" must return a string, %s received.', + get_debug_type($result), + )); + } + + return $result; + } + if (is_object($value) && method_exists($value, '__toString')) { return (string) $value; } diff --git a/src/Target.php b/src/Target.php index ceff0a86..8ebb5d3e 100644 --- a/src/Target.php +++ b/src/Target.php @@ -182,6 +182,51 @@ public function setCommonContext(array $commonContext): self return $this; } + /** + * Sets the format for the string representation of the log context. + * + * @param callable $contextFormat The PHP callable to format the log context. + * + * @return self + * + * @see Formatter::$contextFormat + */ + public function setContextFormat(callable $contextFormat): self + { + $this->formatter->setContextFormat($contextFormat); + return $this; + } + + /** + * Sets a template string for the context output. + * + * @param string $contextTemplate The template string with `{trace}`, `{message}`, and `{common}` placeholders. + * + * @return self + * + * @see Formatter::$contextTemplate + */ + public function setContextTemplate(string $contextTemplate): self + { + $this->formatter->setContextTemplate($contextTemplate); + return $this; + } + + /** + * Sets a PHP callable that converts a value to a string. + * + * @param callable $convertToString The PHP callable to convert a value to a string. + * + * @return self + * + * @see Formatter::$convertToString + */ + public function setConvertToString(callable $convertToString): self + { + $this->formatter->setConvertToString($convertToString); + return $this; + } + /** * Sets a PHP callable that returns a string representation of the log message. * diff --git a/tests/Message/FormatterTest.php b/tests/Message/FormatterTest.php index abe1ec97..67ee3f15 100644 --- a/tests/Message/FormatterTest.php +++ b/tests/Message/FormatterTest.php @@ -217,6 +217,217 @@ public function testFormatWithTraceInContext(string $expectedTrace, array $trace $this->assertSame($expected, $this->formatter->format($message, [])); } + public function testDefaultFormatWithSetConvertToString(): void + { + $this->formatter->setConvertToString( + static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), + ); + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $expected = '2017-10-16 13:26:30.000000 [info][app] message' + . "\n\nMessage context:\n\ncategory: \"app\"\ntime: 1508160390" + . "\n\nCommon context:\n\nserver: \"web\"\n" + ; + $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); + } + + public function testDefaultFormatWithSetContextFormat(): void + { + $this->formatter->setTimestampFormat('Y-m-d H:i:s'); + $this->formatter->setContextFormat( + static function (string $trace, string $messageContext, string $commonContext): string { + $result = ''; + if ($commonContext !== '') { + $result .= "\n\nCommon:\n" . $commonContext; + } + if ($trace !== '') { + $result .= "\n\nTrace:\n" . $trace; + } + if ($messageContext !== '') { + $result .= "\n\nMessage:\n" . $messageContext; + } + return $result; + }, + ); + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + 'trace' => [['file' => '/path/to/file', 'line' => 99]], + ]); + $expected = '2017-10-16 13:26:30 [info][app] message' + . "\n\nCommon:\nserver: 'web'" + . "\n\nTrace:\ntrace:\n in /path/to/file:99" + . "\n\nMessage:\ncategory: 'app'\ntime: 1508160390" + ; + $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); + } + + public function testDefaultFormatWithSetContextTemplate(): void + { + $this->formatter->setTimestampFormat('Y-m-d H:i:s'); + $this->formatter->setContextTemplate("{common}{message}{trace}\n"); + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + 'trace' => [['file' => '/path/to/file', 'line' => 99]], + ]); + $expected = '2017-10-16 13:26:30 [info][app] message' + . "\n\nCommon context:\n\nserver: 'web'" + . "\n\nMessage context:\n\ncategory: 'app'\ntime: 1508160390" + . "\n\nTrace:\n\ntrace:\n in /path/to/file:99" + . "\n" + ; + $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); + } + + public function testDefaultFormatWithSetContextTemplateEmptySections(): void + { + $this->formatter->setTimestampFormat('Y-m-d H:i:s'); + $this->formatter->setContextTemplate("{trace}{message}{common}\n"); + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $expected = '2017-10-16 13:26:30 [info][app] message' + . "\n\nMessage context:\n\ncategory: 'app'\ntime: 1508160390" + . "\n" + ; + $this->assertSame($expected, $this->formatter->format($message, [])); + } + + public function testDefaultFormatWithSetContextTemplateOnlyCommon(): void + { + $this->formatter->setTimestampFormat('Y-m-d H:i:s'); + $this->formatter->setContextTemplate("{common}\n"); + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $expected = '2017-10-16 13:26:30 [info][app] message' + . "\n\nCommon context:\n\nserver: 'web'" + . "\n" + ; + $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); + } + + public function testContextFormatTakesPrecedenceOverContextTemplate(): void + { + $this->formatter->setContextTemplate("{common}{message}\n"); + $this->formatter->setContextFormat( + static fn(string $trace, string $messageContext, string $commonContext): string => '[custom]', + ); + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $result = $this->formatter->format($message, []); + $this->assertStringContainsString('[custom]', $result); + $this->assertStringNotContainsString('Common context', $result); + } + + public function testDefaultFormatWithSetConvertToStringOverridesStringableObject(): void + { + $this->formatter->setConvertToString( + static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), + ); + $object = new class { + public function __toString(): string + { + return 'stringable-object'; + } + }; + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + 'obj' => $object, + ]); + $result = $this->formatter->format($message, []); + $this->assertStringContainsString('obj: {}', $result); + $this->assertStringNotContainsString('stringable-object', $result); + } + + public function testDefaultFormatWithSetConvertToStringDoesNotAffectTrace(): void + { + $called = false; + $this->formatter->setConvertToString(static function (mixed $value) use (&$called): string { + $called = true; + return json_encode($value, JSON_THROW_ON_ERROR); + }); + $this->formatter->setTimestampFormat('Y-m-d H:i:s'); + $message = new Message(LogLevel::INFO, 'message', [ + 'time' => 1_508_160_390, + 'trace' => [['file' => '/path/to/file', 'line' => 99]], + ]); + $result = $this->formatter->format($message, []); + $this->assertStringContainsString("trace:\n in /path/to/file:99", $result); + $this->assertTrue($called); + } + + public function testDefaultFormatWithSetContextFormatReceivesEmptyTrace(): void + { + $receivedTrace = 'not-called'; + $this->formatter->setContextFormat( + static function (string $trace, string $messageContext, string $commonContext) use (&$receivedTrace): string { + $receivedTrace = $trace; + return "\n" . $messageContext; + }, + ); + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $this->formatter->format($message, []); + $this->assertSame('', $receivedTrace); + } + + public function testDefaultFormatWithSetContextFormatReceivesEmptyCommonContext(): void + { + $receivedCommon = 'not-called'; + $this->formatter->setContextFormat( + static function (string $trace, string $messageContext, string $commonContext) use (&$receivedCommon): string { + $receivedCommon = $commonContext; + return "\n" . $messageContext; + }, + ); + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $this->formatter->format($message, []); + $this->assertSame('', $receivedCommon); + } + + public function testDefaultFormatWithSetConvertToStringAndSetContextFormat(): void + { + $this->formatter->setTimestampFormat('Y-m-d H:i:s'); + $this->formatter->setConvertToString( + static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), + ); + $this->formatter->setContextFormat( + static function (string $trace, string $messageContext, string $commonContext): string { + $result = ''; + if ($commonContext !== '') { + $result .= "\n[C] " . $commonContext; + } + if ($messageContext !== '') { + $result .= "\n[M] " . $messageContext; + } + return $result; + }, + ); + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $expected = '2017-10-16 13:26:30 [info][app] message' + . "\n[C] server: \"web\"" + . "\n[M] category: \"app\"\ntime: 1508160390" + ; + $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); + } + public function invalidCallableReturnStringProvider(): array { return [ @@ -250,6 +461,22 @@ public function testFormatMessageThrowExceptionForPrefixCallableReturnNotString( $this->formatter->format(new Message(LogLevel::INFO, 'test', ['foo' => 'bar']), []); } + public function testFormatThrowExceptionForConvertToStringCallableReturnNotString(): void + { + $this->formatter->setConvertToString(static fn(mixed $value) => 123); + $this->expectException(RuntimeException::class); + $this->formatter->format(new Message(LogLevel::INFO, 'test', ['foo' => 'bar']), []); + } + + public function testFormatThrowExceptionForContextFormatCallableReturnNotString(): void + { + $this->formatter->setContextFormat( + static fn(string $trace, string $messageContext, string $commonContext) => 123, + ); + $this->expectException(RuntimeException::class); + $this->formatter->format(new Message(LogLevel::INFO, 'test', ['foo' => 'bar']), []); + } + public static function dataTime(): array { return [ diff --git a/tests/TargetTest.php b/tests/TargetTest.php index 9caae345..775e1609 100644 --- a/tests/TargetTest.php +++ b/tests/TargetTest.php @@ -258,6 +258,71 @@ public function testSetLevelsThrowExceptionForNonStringList(array $list): void $this->target->setLevels($list); } + public function testSetConvertToString(): void + { + $this->target->setConvertToString( + static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), + ); + $this->collectOneAndExport(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $expected = '2017-10-16 13:26:30.000000 [info][app] message' + . "\n\nMessage context:\n\ncategory: \"app\"\ntime: 1508160390" + . "\n\nCommon context:\n\nserver: \"web\"\n" + ; + $this->target->setCommonContext(['server' => 'web']); + $this->collectOneAndExport(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $this->assertSame($expected, $this->target->formatMessages()); + } + + public function testSetContextTemplate(): void + { + $this->target->setTimestampFormat('Y-m-d H:i:s'); + $this->target->setContextTemplate("{common}{message}\n"); + $this->target->setCommonContext(['server' => 'web']); + $this->collectOneAndExport(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $expected = "2017-10-16 13:26:30 [info][app] message" + . "\n\nCommon context:\n\nserver: 'web'" + . "\n\nMessage context:\n\ncategory: 'app'\ntime: 1508160390" + . "\n" + ; + $this->assertSame($expected, $this->target->formatMessages()); + } + + public function testSetContextFormat(): void + { + $this->target->setTimestampFormat('Y-m-d H:i:s'); + $this->target->setContextFormat( + static function (string $trace, string $messageContext, string $commonContext): string { + $result = ''; + if ($commonContext !== '') { + $result .= "\n\nCommon:\n" . $commonContext; + } + if ($messageContext !== '') { + $result .= "\n\nMessage:\n" . $messageContext; + } + return $result; + }, + ); + $this->target->setCommonContext(['server' => 'web']); + $this->collectOneAndExport(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $expected = "2017-10-16 13:26:30 [info][app] message" + . "\n\nCommon:\nserver: 'web'" + . "\n\nMessage:\ncategory: 'app'\ntime: 1508160390" + ; + $this->assertSame($expected, $this->target->formatMessages()); + } + public function testSetFormat(): void { $this->target->setFormat(static fn(Message $message) => "[{$message->level()}][{$message->context('category')}] {$message->message()}"); diff --git a/tests/TestAsset/DummyTarget.php b/tests/TestAsset/DummyTarget.php index f311831f..4e78381b 100644 --- a/tests/TestAsset/DummyTarget.php +++ b/tests/TestAsset/DummyTarget.php @@ -82,6 +82,24 @@ public function getCommonContext(): array return parent::getCommonContext(); } + public function setContextFormat(callable $contextFormat): self + { + $this->exportFormatter->setContextFormat($contextFormat); + return parent::setContextFormat($contextFormat); + } + + public function setContextTemplate(string $contextTemplate): self + { + $this->exportFormatter->setContextTemplate($contextTemplate); + return parent::setContextTemplate($contextTemplate); + } + + public function setConvertToString(callable $convertToString): self + { + $this->exportFormatter->setConvertToString($convertToString); + return parent::setConvertToString($convertToString); + } + public function setFormat(callable $format): self { $this->exportFormatter->setFormat($format); From 6bbf64cd50b522fd66dcfa19b4c35cfea4c6b9b3 Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Thu, 26 Mar 2026 22:09:18 +0600 Subject: [PATCH 2/4] Apply review fixes: rename property, move PHPDoc, cleanup --- CHANGELOG.md | 2 +- README.md | 2 +- src/Message/Formatter.php | 54 +++++++++++++++++++-------------------- src/Target.php | 23 ++++++++++++----- tests/TargetTest.php | 6 +---- 5 files changed, 45 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42c2073c..ae2b4ed2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.2.2 under development -- New #140: Add `setConvertToString()`, `setContextTemplate()`, and `setContextFormat()` to `Formatter` (@WarLikeLaux) +- New #140: Add `setConvertToString()`, `setContextTemplate()`, and `setContextFormat()` to `Formatter` and `Target` (@WarLikeLaux) ## 2.2.1 March 22, 2026 diff --git a/README.md b/README.md index 5d52b943..e9681d88 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ $target->setTimestampFormat('Y-m-d H:i:s'); To replace how context values are converted to strings (default uses VarDumper): ```php -$target->setConvertToString(static fn(mixed $value): string => json_encode($value)); +$target->setConvertToString(static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR)); ``` To reorder context sections (trace, message context, common context) use a template string with diff --git a/src/Message/Formatter.php b/src/Message/Formatter.php index 869e20a1..dbe5bfca 100644 --- a/src/Message/Formatter.php +++ b/src/Message/Formatter.php @@ -24,34 +24,23 @@ final class Formatter { /** - * @var callable|null PHP callable that returns a string representation of the log context. + * @var callable|null * - * If not set, the default context format will be used. - * - * The signature of the callable should be - * `function (string $trace, string $messageContext, string $commonContext): string;`. + * @see Formatter::setContextFormat() */ - private $contextFormat; + private $contextFormatter; /** - * @var string|null A template string for the context output. - * - * Supports `{trace}`, `{message}`, and `{common}` placeholders. Each placeholder is replaced with its - * formatted section (including header) if non-empty, or an empty string if the section has no data. + * @var string|null * - * For example, `"{common}{message}{trace}\n"` outputs common context first, then message context, then trace. - * - * If not set, the default context format will be used. - * If {@see Formatter::$contextFormat} is also set, it takes precedence over the template. + * @see Formatter::setContextTemplate() */ private ?string $contextTemplate = null; /** - * @var callable|null PHP callable that converts a value to a string. - * - * If not set, {@see Formatter::convertToString()} default logic will be used. + * @var callable|null * - * The signature of the callable should be `function (mixed $value): string;`. + * @see Formatter::setConvertToString() */ private $convertToString; @@ -80,23 +69,30 @@ final class Formatter private string $timestampFormat = 'Y-m-d H:i:s.u'; /** - * Sets the format for the string representation of the log context. + * Sets a PHP callable that returns a string representation of the log context. * - * @param callable $contextFormat The PHP callable to format the log context. + * If not set, the default context format will be used. + * If both this and {@see Formatter::setContextTemplate()} are set, the callable takes precedence. * - * @see Formatter::$contextFormat + * The signature of the callable should be + * `function (string $trace, string $messageContext, string $commonContext): string;`. + * + * @param callable $contextFormat The PHP callable to format the log context. */ public function setContextFormat(callable $contextFormat): void { - $this->contextFormat = $contextFormat; + $this->contextFormatter = $contextFormat; } /** * Sets a template string for the context output. * - * @param string $contextTemplate The template string with `{trace}`, `{message}`, and `{common}` placeholders. + * Supports `{trace}`, `{message}`, and `{common}` placeholders. Each placeholder is replaced with its + * formatted section (including header) if non-empty, or an empty string if the section has no data. * - * @see Formatter::$contextTemplate + * For example, `"{common}{message}{trace}\n"` outputs common context first, then message context, then trace. + * + * @param string $contextTemplate The template string with `{trace}`, `{message}`, and `{common}` placeholders. */ public function setContextTemplate(string $contextTemplate): void { @@ -106,9 +102,11 @@ public function setContextTemplate(string $contextTemplate): void /** * Sets a PHP callable that converts a value to a string. * - * @param callable $convertToString The PHP callable to convert a value to a string. + * If not set, the default VarDumper-based conversion will be used. * - * @see Formatter::$convertToString + * The signature of the callable should be `function (mixed $value): string;`. + * + * @param callable $convertToString The PHP callable to convert a value to a string. */ public function setConvertToString(callable $convertToString): void { @@ -261,8 +259,8 @@ private function getContext(Message $message, array $commonContext): string $messageContext = implode("\n", $context); $commonContextString = implode("\n", $common); - if ($this->contextFormat !== null) { - $result = ($this->contextFormat)($trace, $messageContext, $commonContextString); + if ($this->contextFormatter !== null) { + $result = ($this->contextFormatter)($trace, $messageContext, $commonContextString); if (!is_string($result)) { throw new RuntimeException(sprintf( diff --git a/src/Target.php b/src/Target.php index 8ebb5d3e..ba77afbb 100644 --- a/src/Target.php +++ b/src/Target.php @@ -183,13 +183,17 @@ public function setCommonContext(array $commonContext): self } /** - * Sets the format for the string representation of the log context. + * Sets a PHP callable that returns a string representation of the log context. + * + * If not set, the default context format will be used. + * If both this and {@see Target::setContextTemplate()} are set, the callable takes precedence. + * + * The signature of the callable should be + * `function (string $trace, string $messageContext, string $commonContext): string;`. * * @param callable $contextFormat The PHP callable to format the log context. * * @return self - * - * @see Formatter::$contextFormat */ public function setContextFormat(callable $contextFormat): self { @@ -200,11 +204,14 @@ public function setContextFormat(callable $contextFormat): self /** * Sets a template string for the context output. * + * Supports `{trace}`, `{message}`, and `{common}` placeholders. Each placeholder is replaced with its + * formatted section (including header) if non-empty, or an empty string if the section has no data. + * + * For example, `"{common}{message}{trace}\n"` outputs common context first, then message context, then trace. + * * @param string $contextTemplate The template string with `{trace}`, `{message}`, and `{common}` placeholders. * * @return self - * - * @see Formatter::$contextTemplate */ public function setContextTemplate(string $contextTemplate): self { @@ -215,11 +222,13 @@ public function setContextTemplate(string $contextTemplate): self /** * Sets a PHP callable that converts a value to a string. * + * If not set, the default VarDumper-based conversion will be used. + * + * The signature of the callable should be `function (mixed $value): string;`. + * * @param callable $convertToString The PHP callable to convert a value to a string. * * @return self - * - * @see Formatter::$convertToString */ public function setConvertToString(callable $convertToString): self { diff --git a/tests/TargetTest.php b/tests/TargetTest.php index 775e1609..af16825c 100644 --- a/tests/TargetTest.php +++ b/tests/TargetTest.php @@ -263,6 +263,7 @@ public function testSetConvertToString(): void $this->target->setConvertToString( static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), ); + $this->target->setCommonContext(['server' => 'web']); $this->collectOneAndExport(LogLevel::INFO, 'message', [ 'category' => 'app', 'time' => 1_508_160_390, @@ -271,11 +272,6 @@ public function testSetConvertToString(): void . "\n\nMessage context:\n\ncategory: \"app\"\ntime: 1508160390" . "\n\nCommon context:\n\nserver: \"web\"\n" ; - $this->target->setCommonContext(['server' => 'web']); - $this->collectOneAndExport(LogLevel::INFO, 'message', [ - 'category' => 'app', - 'time' => 1_508_160_390, - ]); $this->assertSame($expected, $this->target->formatMessages()); } From 3195398de6568a325bbe2ba929ba1c9899c7aaa4 Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Sun, 31 May 2026 15:12:09 +0600 Subject: [PATCH 3/4] Apply review fixes: merge context format, extract converter --- README.md | 2 +- src/Message/Formatter.php | 93 +++++++++---------- src/Message/VarDumperValueConverter.php | 29 ++++++ src/Target.php | 4 +- tests/Message/FormatterTest.php | 28 +++++- tests/Message/VarDumperValueConverterTest.php | 53 +++++++++++ 6 files changed, 152 insertions(+), 57 deletions(-) create mode 100644 src/Message/VarDumperValueConverter.php create mode 100644 tests/Message/VarDumperValueConverterTest.php diff --git a/README.md b/README.md index 7dab0924..62473e91 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ $target->setContextFormat( ); ``` -If both `setContextFormat()` and `setContextTemplate()` are set, the callable takes precedence. +`setContextFormat()` and `setContextTemplate()` share the same setting, so the one called last takes effect. ### Configuring `LoggerInterface` in Yii3 diff --git a/src/Message/Formatter.php b/src/Message/Formatter.php index dbe5bfca..9197f68d 100644 --- a/src/Message/Formatter.php +++ b/src/Message/Formatter.php @@ -6,12 +6,9 @@ use RuntimeException; use Yiisoft\Log\Message; -use Yiisoft\VarDumper\VarDumper; use function implode; use function is_string; -use function is_object; -use function method_exists; use function sprintf; use function str_replace; use function is_int; @@ -24,25 +21,12 @@ final class Formatter { /** - * @var callable|null + * @var string|callable|null * * @see Formatter::setContextFormat() - */ - private $contextFormatter; - - /** - * @var string|null - * * @see Formatter::setContextTemplate() */ - private ?string $contextTemplate = null; - - /** - * @var callable|null - * - * @see Formatter::setConvertToString() - */ - private $convertToString; + private $contextFormat = null; /** * @var callable|null PHP callable that returns a string representation of the log message. @@ -63,16 +47,28 @@ final class Formatter */ private $prefix; + /** + * @var callable + * + * @see Formatter::setConvertToString() + */ + private $stringConverter; + /** * @var string The date format for the log timestamp. Defaults to `Y-m-d H:i:s.u`. */ private string $timestampFormat = 'Y-m-d H:i:s.u'; + public function __construct() + { + $this->stringConverter = new VarDumperValueConverter(); + } + /** * Sets a PHP callable that returns a string representation of the log context. * * If not set, the default context format will be used. - * If both this and {@see Formatter::setContextTemplate()} are set, the callable takes precedence. + * This and {@see Formatter::setContextTemplate()} share the same setting, so the one set last takes effect. * * The signature of the callable should be * `function (string $trace, string $messageContext, string $commonContext): string;`. @@ -81,7 +77,7 @@ final class Formatter */ public function setContextFormat(callable $contextFormat): void { - $this->contextFormatter = $contextFormat; + $this->contextFormat = $contextFormat; } /** @@ -92,11 +88,13 @@ public function setContextFormat(callable $contextFormat): void * * For example, `"{common}{message}{trace}\n"` outputs common context first, then message context, then trace. * + * This and {@see Formatter::setContextFormat()} share the same setting, so the one set last takes effect. + * * @param string $contextTemplate The template string with `{trace}`, `{message}`, and `{common}` placeholders. */ public function setContextTemplate(string $contextTemplate): void { - $this->contextTemplate = $contextTemplate; + $this->contextFormat = $contextTemplate; } /** @@ -110,7 +108,7 @@ public function setContextTemplate(string $contextTemplate): void */ public function setConvertToString(callable $convertToString): void { - $this->convertToString = $convertToString; + $this->stringConverter = $convertToString; } /** @@ -258,9 +256,22 @@ private function getContext(Message $message, array $commonContext): string $messageContext = implode("\n", $context); $commonContextString = implode("\n", $common); + $contextFormat = $this->contextFormat; - if ($this->contextFormatter !== null) { - $result = ($this->contextFormatter)($trace, $messageContext, $commonContextString); + if (is_string($contextFormat)) { + return str_replace( + ['{trace}', '{message}', '{common}'], + [ + $trace === '' ? '' : "\n\nTrace:\n\n" . $trace, + $messageContext === '' ? '' : "\n\nMessage context:\n\n" . $messageContext, + $commonContextString === '' ? '' : "\n\nCommon context:\n\n" . $commonContextString, + ], + $contextFormat, + ); + } + + if ($contextFormat !== null) { + $result = $contextFormat($trace, $messageContext, $commonContextString); if (!is_string($result)) { throw new RuntimeException(sprintf( @@ -272,18 +283,6 @@ private function getContext(Message $message, array $commonContext): string return $result; } - if ($this->contextTemplate !== null) { - return str_replace( - ['{trace}', '{message}', '{common}'], - [ - $trace === '' ? '' : "\n\nTrace:\n\n" . $trace, - $messageContext === '' ? '' : "\n\nMessage context:\n\n" . $messageContext, - $commonContextString === '' ? '' : "\n\nCommon context:\n\n" . $commonContextString, - ], - $this->contextTemplate, - ); - } - $messageItems = []; if ($trace !== '') { @@ -347,23 +346,15 @@ static function (mixed $trace): string { */ private function convertToString(mixed $value): string { - if ($this->convertToString !== null) { - $result = ($this->convertToString)($value); - - if (!is_string($result)) { - throw new RuntimeException(sprintf( - 'The PHP callable "convertToString" must return a string, %s received.', - get_debug_type($result), - )); - } - - return $result; - } + $result = ($this->stringConverter)($value); - if (is_object($value) && method_exists($value, '__toString')) { - return (string) $value; + if (!is_string($result)) { + throw new RuntimeException(sprintf( + 'The PHP callable "convertToString" must return a string, %s received.', + get_debug_type($result), + )); } - return VarDumper::create($value)->asString(); + return $result; } } diff --git a/src/Message/VarDumperValueConverter.php b/src/Message/VarDumperValueConverter.php new file mode 100644 index 00000000..9ac57542 --- /dev/null +++ b/src/Message/VarDumperValueConverter.php @@ -0,0 +1,29 @@ +asString(); + } +} diff --git a/src/Target.php b/src/Target.php index ba77afbb..789046ba 100644 --- a/src/Target.php +++ b/src/Target.php @@ -186,7 +186,7 @@ public function setCommonContext(array $commonContext): self * Sets a PHP callable that returns a string representation of the log context. * * If not set, the default context format will be used. - * If both this and {@see Target::setContextTemplate()} are set, the callable takes precedence. + * This and {@see Target::setContextTemplate()} share the same setting, so the one set last takes effect. * * The signature of the callable should be * `function (string $trace, string $messageContext, string $commonContext): string;`. @@ -209,6 +209,8 @@ public function setContextFormat(callable $contextFormat): self * * For example, `"{common}{message}{trace}\n"` outputs common context first, then message context, then trace. * + * This and {@see Target::setContextFormat()} share the same setting, so the one set last takes effect. + * * @param string $contextTemplate The template string with `{trace}`, `{message}`, and `{common}` placeholders. * * @return self diff --git a/tests/Message/FormatterTest.php b/tests/Message/FormatterTest.php index 2d5dc8b7..e6776838 100644 --- a/tests/Message/FormatterTest.php +++ b/tests/Message/FormatterTest.php @@ -312,8 +312,9 @@ public function testDefaultFormatWithSetContextTemplateOnlyCommon(): void $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); } - public function testContextFormatTakesPrecedenceOverContextTemplate(): void + public function testSetContextFormatOverridesContextTemplate(): void { + $this->formatter->setTimestampFormat('Y-m-d H:i:s'); $this->formatter->setContextTemplate("{common}{message}\n"); $this->formatter->setContextFormat( static fn(string $trace, string $messageContext, string $commonContext): string => '[custom]', @@ -322,9 +323,28 @@ public function testContextFormatTakesPrecedenceOverContextTemplate(): void 'category' => 'app', 'time' => 1_508_160_390, ]); - $result = $this->formatter->format($message, []); - $this->assertStringContainsString('[custom]', $result); - $this->assertStringNotContainsString('Common context', $result); + $expected = '2017-10-16 13:26:30 [info][app] message[custom]'; + + $this->assertSame($expected, $this->formatter->format($message, [])); + } + + public function testSetContextTemplateOverridesContextFormat(): void + { + $this->formatter->setTimestampFormat('Y-m-d H:i:s'); + $this->formatter->setContextFormat( + static fn(string $trace, string $messageContext, string $commonContext): string => '[custom]', + ); + $this->formatter->setContextTemplate("{common}{message}\n"); + $message = new Message(LogLevel::INFO, 'message', [ + 'category' => 'app', + 'time' => 1_508_160_390, + ]); + $expected = '2017-10-16 13:26:30 [info][app] message' + . "\n\nMessage context:\n\ncategory: 'app'\ntime: 1508160390" + . "\n" + ; + + $this->assertSame($expected, $this->formatter->format($message, [])); } public function testDefaultFormatWithSetConvertToStringOverridesStringableObject(): void diff --git a/tests/Message/VarDumperValueConverterTest.php b/tests/Message/VarDumperValueConverterTest.php new file mode 100644 index 00000000..03f6c190 --- /dev/null +++ b/tests/Message/VarDumperValueConverterTest.php @@ -0,0 +1,53 @@ + ["'foo'", 'foo'], + 'int' => ['1', 1], + 'float' => ['1.1', 1.1], + 'null' => ['null', null], + 'array' => ['[]', []], + ]; + } + + /** + * @dataProvider dataConvert + */ + public function testConvert(string $expected, mixed $value): void + { + $converter = new VarDumperValueConverter(); + + $this->assertSame($expected, $converter($value)); + } + + public function testStringableObjectUsesToString(): void + { + $converter = new VarDumperValueConverter(); + $object = new class { + public function __toString(): string + { + return 'stringable-object'; + } + }; + + $this->assertSame('stringable-object', $converter($object)); + } + + public function testObjectWithoutToStringUsesVarDumper(): void + { + $converter = new VarDumperValueConverter(); + + $this->assertStringContainsString('stdClass', $converter(new stdClass())); + } +} From 932bb0bc70ff5794bb4acc3405dbe4c72582af05 Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Sun, 31 May 2026 16:29:05 +0600 Subject: [PATCH 4/4] Move context format and converter to constructor params --- CHANGELOG.md | 2 +- README.md | 22 +++-- src/Message/Formatter.php | 64 ++++---------- src/PsrTarget.php | 12 ++- src/StreamTarget.php | 12 ++- src/Target.php | 70 +++------------- tests/Message/FormatterTest.php | 143 ++++++++++++-------------------- tests/TargetTest.php | 18 ++-- tests/TestAsset/DummyTarget.php | 29 ++----- 9 files changed, 127 insertions(+), 245 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae2b4ed2..45a3f58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.2.2 under development -- New #140: Add `setConvertToString()`, `setContextTemplate()`, and `setContextFormat()` to `Formatter` and `Target` (@WarLikeLaux) +- New #140: Add `$contextFormat` and `$stringConverter` constructor parameters to `Target` for customizing context output (@WarLikeLaux) ## 2.2.1 March 22, 2026 diff --git a/README.md b/README.md index 62473e91..4998aaeb 100644 --- a/README.md +++ b/README.md @@ -247,25 +247,29 @@ To change the timestamp format: $target->setTimestampFormat('Y-m-d H:i:s'); ``` -To replace how context values are converted to strings (default uses VarDumper): +The context output is customized via constructor parameters. + +To replace how context values are converted to strings (default uses VarDumper), pass `stringConverter`: ```php -$target->setConvertToString(static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR)); +$target = new \Yiisoft\Log\StreamTarget( + stringConverter: static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), +); ``` -To reorder context sections (trace, message context, common context) use a template string with -`{trace}`, `{message}`, and `{common}` placeholders. Each placeholder expands to its section with +To reorder context sections (trace, message context, common context), pass a `contextFormat` template string +with `{trace}`, `{message}`, and `{common}` placeholders. Each placeholder expands to its section with a header when non-empty, or an empty string when the section has no data: ```php -$target->setContextTemplate("{common}{message}{trace}\n"); +$target = new \Yiisoft\Log\StreamTarget(contextFormat: "{common}{message}{trace}\n"); ``` -For full control over context rendering, use a callable: +For full control over context rendering, pass a `contextFormat` callable instead: ```php -$target->setContextFormat( - static function (string $trace, string $messageContext, string $commonContext): string { +$target = new \Yiisoft\Log\StreamTarget( + contextFormat: static function (string $trace, string $messageContext, string $commonContext): string { $result = ''; if ($commonContext !== '') { $result .= "\n\nCommon:\n" . $commonContext; @@ -278,7 +282,7 @@ $target->setContextFormat( ); ``` -`setContextFormat()` and `setContextTemplate()` share the same setting, so the one called last takes effect. +`contextFormat` accepts either a template string or a callable. ### Configuring `LoggerInterface` in Yii3 diff --git a/src/Message/Formatter.php b/src/Message/Formatter.php index 9197f68d..01814df9 100644 --- a/src/Message/Formatter.php +++ b/src/Message/Formatter.php @@ -23,8 +23,7 @@ final class Formatter /** * @var string|callable|null * - * @see Formatter::setContextFormat() - * @see Formatter::setContextTemplate() + * @see Formatter::__construct() */ private $contextFormat = null; @@ -50,7 +49,7 @@ final class Formatter /** * @var callable * - * @see Formatter::setConvertToString() + * @see Formatter::__construct() */ private $stringConverter; @@ -59,56 +58,21 @@ final class Formatter */ private string $timestampFormat = 'Y-m-d H:i:s.u'; - public function __construct() - { - $this->stringConverter = new VarDumperValueConverter(); - } - /** - * Sets a PHP callable that returns a string representation of the log context. - * - * If not set, the default context format will be used. - * This and {@see Formatter::setContextTemplate()} share the same setting, so the one set last takes effect. - * - * The signature of the callable should be - * `function (string $trace, string $messageContext, string $commonContext): string;`. - * - * @param callable $contextFormat The PHP callable to format the log context. + * @param string|callable|null $contextFormat A context format. A template string supports `{trace}`, + * `{message}` and `{common}` placeholders, each replaced with its formatted section (including header) if + * non-empty, or an empty string otherwise. For example, `"{common}{message}{trace}\n"` outputs common + * context first, then message context, then trace. A PHP callable gives full control over context rendering; + * its signature should be `function (string $trace, string $messageContext, string $commonContext): string;`. + * @param callable|null $stringConverter A PHP callable that converts a context value to a string. Its + * signature should be `function (mixed $value): string;`. Defaults to {@see VarDumperValueConverter}. */ - public function setContextFormat(callable $contextFormat): void - { + public function __construct( + string|callable|null $contextFormat = null, + ?callable $stringConverter = null, + ) { $this->contextFormat = $contextFormat; - } - - /** - * Sets a template string for the context output. - * - * Supports `{trace}`, `{message}`, and `{common}` placeholders. Each placeholder is replaced with its - * formatted section (including header) if non-empty, or an empty string if the section has no data. - * - * For example, `"{common}{message}{trace}\n"` outputs common context first, then message context, then trace. - * - * This and {@see Formatter::setContextFormat()} share the same setting, so the one set last takes effect. - * - * @param string $contextTemplate The template string with `{trace}`, `{message}`, and `{common}` placeholders. - */ - public function setContextTemplate(string $contextTemplate): void - { - $this->contextFormat = $contextTemplate; - } - - /** - * Sets a PHP callable that converts a value to a string. - * - * If not set, the default VarDumper-based conversion will be used. - * - * The signature of the callable should be `function (mixed $value): string;`. - * - * @param callable $convertToString The PHP callable to convert a value to a string. - */ - public function setConvertToString(callable $convertToString): void - { - $this->stringConverter = $convertToString; + $this->stringConverter = $stringConverter ?? new VarDumperValueConverter(); } /** diff --git a/src/PsrTarget.php b/src/PsrTarget.php index d4172dc2..541ce66b 100644 --- a/src/PsrTarget.php +++ b/src/PsrTarget.php @@ -17,10 +17,16 @@ final class PsrTarget extends Target * * @param LoggerInterface $logger The logger instance to be used for messages processing. * @param string[] $levels The {@see LogLevel log message levels} that this target is interested in. + * @param string|callable|null $contextFormat A context format for the log context output. See {@see Target::__construct()}. + * @param callable|null $stringConverter A PHP callable that converts a context value to a string. See {@see Target::__construct()}. */ - public function __construct(private LoggerInterface $logger, array $levels = []) - { - parent::__construct($levels); + public function __construct( + private LoggerInterface $logger, + array $levels = [], + string|callable|null $contextFormat = null, + ?callable $stringConverter = null, + ) { + parent::__construct($levels, $contextFormat, $stringConverter); } /** diff --git a/src/StreamTarget.php b/src/StreamTarget.php index e307018f..294ff4e2 100644 --- a/src/StreamTarget.php +++ b/src/StreamTarget.php @@ -31,10 +31,16 @@ final class StreamTarget extends Target /** * @param resource|string $stream A string stream identifier or a stream resource. * @param string[] $levels The {@see LogLevel log message levels} that this target is interested in. + * @param string|callable|null $contextFormat A context format for the log context output. See {@see Target::__construct()}. + * @param callable|null $stringConverter A PHP callable that converts a context value to a string. See {@see Target::__construct()}. */ - public function __construct(private $stream = 'php://stdout', array $levels = []) - { - parent::__construct($levels); + public function __construct( + private $stream = 'php://stdout', + array $levels = [], + string|callable|null $contextFormat = null, + ?callable $stringConverter = null, + ) { + parent::__construct($levels, $contextFormat, $stringConverter); } protected function export(): void diff --git a/src/Target.php b/src/Target.php index 789046ba..e1225ef4 100644 --- a/src/Target.php +++ b/src/Target.php @@ -77,11 +77,19 @@ abstract class Target * When defining a constructor in child classes, you must call `parent::__construct()`. * * @param string[] $levels The {@see \Psr\Log\LogLevel log message levels} that this target is interested in. + * @param string|callable|null $contextFormat A context format for the log context output. A template string + * supports `{trace}`, `{message}` and `{common}` placeholders, or a PHP callable for full control. See + * {@see Formatter::__construct()}. + * @param callable|null $stringConverter A PHP callable that converts a context value to a string. + * See {@see Formatter::__construct()}. */ - public function __construct(array $levels = []) - { + public function __construct( + array $levels = [], + string|callable|null $contextFormat = null, + ?callable $stringConverter = null, + ) { $this->categories = new CategoryFilter(); - $this->formatter = new Formatter(); + $this->formatter = new Formatter($contextFormat, $stringConverter); $this->setLevels($levels); } @@ -182,62 +190,6 @@ public function setCommonContext(array $commonContext): self return $this; } - /** - * Sets a PHP callable that returns a string representation of the log context. - * - * If not set, the default context format will be used. - * This and {@see Target::setContextTemplate()} share the same setting, so the one set last takes effect. - * - * The signature of the callable should be - * `function (string $trace, string $messageContext, string $commonContext): string;`. - * - * @param callable $contextFormat The PHP callable to format the log context. - * - * @return self - */ - public function setContextFormat(callable $contextFormat): self - { - $this->formatter->setContextFormat($contextFormat); - return $this; - } - - /** - * Sets a template string for the context output. - * - * Supports `{trace}`, `{message}`, and `{common}` placeholders. Each placeholder is replaced with its - * formatted section (including header) if non-empty, or an empty string if the section has no data. - * - * For example, `"{common}{message}{trace}\n"` outputs common context first, then message context, then trace. - * - * This and {@see Target::setContextFormat()} share the same setting, so the one set last takes effect. - * - * @param string $contextTemplate The template string with `{trace}`, `{message}`, and `{common}` placeholders. - * - * @return self - */ - public function setContextTemplate(string $contextTemplate): self - { - $this->formatter->setContextTemplate($contextTemplate); - return $this; - } - - /** - * Sets a PHP callable that converts a value to a string. - * - * If not set, the default VarDumper-based conversion will be used. - * - * The signature of the callable should be `function (mixed $value): string;`. - * - * @param callable $convertToString The PHP callable to convert a value to a string. - * - * @return self - */ - public function setConvertToString(callable $convertToString): self - { - $this->formatter->setConvertToString($convertToString); - return $this; - } - /** * Sets a PHP callable that returns a string representation of the log message. * diff --git a/tests/Message/FormatterTest.php b/tests/Message/FormatterTest.php index e6776838..6c64914c 100644 --- a/tests/Message/FormatterTest.php +++ b/tests/Message/FormatterTest.php @@ -217,10 +217,10 @@ public function testFormatWithTraceInContext(string $expectedTrace, array $trace $this->assertSame($expected, $this->formatter->format($message, [])); } - public function testDefaultFormatWithSetConvertToString(): void + public function testDefaultFormatWithStringConverter(): void { - $this->formatter->setConvertToString( - static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), + $formatter = new Formatter( + stringConverter: static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), ); $message = new Message(LogLevel::INFO, 'message', [ 'category' => 'app', @@ -230,14 +230,13 @@ public function testDefaultFormatWithSetConvertToString(): void . "\n\nMessage context:\n\ncategory: \"app\"\ntime: 1508160390" . "\n\nCommon context:\n\nserver: \"web\"\n" ; - $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); + $this->assertSame($expected, $formatter->format($message, ['server' => 'web'])); } - public function testDefaultFormatWithSetContextFormat(): void + public function testDefaultFormatWithContextFormatCallable(): void { - $this->formatter->setTimestampFormat('Y-m-d H:i:s'); - $this->formatter->setContextFormat( - static function (string $trace, string $messageContext, string $commonContext): string { + $formatter = new Formatter( + contextFormat: static function (string $trace, string $messageContext, string $commonContext): string { $result = ''; if ($commonContext !== '') { $result .= "\n\nCommon:\n" . $commonContext; @@ -251,6 +250,7 @@ static function (string $trace, string $messageContext, string $commonContext): return $result; }, ); + $formatter->setTimestampFormat('Y-m-d H:i:s'); $message = new Message(LogLevel::INFO, 'message', [ 'category' => 'app', 'time' => 1_508_160_390, @@ -261,13 +261,13 @@ static function (string $trace, string $messageContext, string $commonContext): . "\n\nTrace:\ntrace:\n in /path/to/file:99" . "\n\nMessage:\ncategory: 'app'\ntime: 1508160390" ; - $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); + $this->assertSame($expected, $formatter->format($message, ['server' => 'web'])); } - public function testDefaultFormatWithSetContextTemplate(): void + public function testDefaultFormatWithContextTemplate(): void { - $this->formatter->setTimestampFormat('Y-m-d H:i:s'); - $this->formatter->setContextTemplate("{common}{message}{trace}\n"); + $formatter = new Formatter(contextFormat: "{common}{message}{trace}\n"); + $formatter->setTimestampFormat('Y-m-d H:i:s'); $message = new Message(LogLevel::INFO, 'message', [ 'category' => 'app', 'time' => 1_508_160_390, @@ -279,13 +279,13 @@ public function testDefaultFormatWithSetContextTemplate(): void . "\n\nTrace:\n\ntrace:\n in /path/to/file:99" . "\n" ; - $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); + $this->assertSame($expected, $formatter->format($message, ['server' => 'web'])); } - public function testDefaultFormatWithSetContextTemplateEmptySections(): void + public function testDefaultFormatWithContextTemplateEmptySections(): void { - $this->formatter->setTimestampFormat('Y-m-d H:i:s'); - $this->formatter->setContextTemplate("{trace}{message}{common}\n"); + $formatter = new Formatter(contextFormat: "{trace}{message}{common}\n"); + $formatter->setTimestampFormat('Y-m-d H:i:s'); $message = new Message(LogLevel::INFO, 'message', [ 'category' => 'app', 'time' => 1_508_160_390, @@ -294,13 +294,13 @@ public function testDefaultFormatWithSetContextTemplateEmptySections(): void . "\n\nMessage context:\n\ncategory: 'app'\ntime: 1508160390" . "\n" ; - $this->assertSame($expected, $this->formatter->format($message, [])); + $this->assertSame($expected, $formatter->format($message, [])); } - public function testDefaultFormatWithSetContextTemplateOnlyCommon(): void + public function testDefaultFormatWithContextTemplateOnlyCommon(): void { - $this->formatter->setTimestampFormat('Y-m-d H:i:s'); - $this->formatter->setContextTemplate("{common}\n"); + $formatter = new Formatter(contextFormat: "{common}\n"); + $formatter->setTimestampFormat('Y-m-d H:i:s'); $message = new Message(LogLevel::INFO, 'message', [ 'category' => 'app', 'time' => 1_508_160_390, @@ -309,48 +309,13 @@ public function testDefaultFormatWithSetContextTemplateOnlyCommon(): void . "\n\nCommon context:\n\nserver: 'web'" . "\n" ; - $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); - } - - public function testSetContextFormatOverridesContextTemplate(): void - { - $this->formatter->setTimestampFormat('Y-m-d H:i:s'); - $this->formatter->setContextTemplate("{common}{message}\n"); - $this->formatter->setContextFormat( - static fn(string $trace, string $messageContext, string $commonContext): string => '[custom]', - ); - $message = new Message(LogLevel::INFO, 'message', [ - 'category' => 'app', - 'time' => 1_508_160_390, - ]); - $expected = '2017-10-16 13:26:30 [info][app] message[custom]'; - - $this->assertSame($expected, $this->formatter->format($message, [])); + $this->assertSame($expected, $formatter->format($message, ['server' => 'web'])); } - public function testSetContextTemplateOverridesContextFormat(): void + public function testStringConverterOverridesStringableObject(): void { - $this->formatter->setTimestampFormat('Y-m-d H:i:s'); - $this->formatter->setContextFormat( - static fn(string $trace, string $messageContext, string $commonContext): string => '[custom]', - ); - $this->formatter->setContextTemplate("{common}{message}\n"); - $message = new Message(LogLevel::INFO, 'message', [ - 'category' => 'app', - 'time' => 1_508_160_390, - ]); - $expected = '2017-10-16 13:26:30 [info][app] message' - . "\n\nMessage context:\n\ncategory: 'app'\ntime: 1508160390" - . "\n" - ; - - $this->assertSame($expected, $this->formatter->format($message, [])); - } - - public function testDefaultFormatWithSetConvertToStringOverridesStringableObject(): void - { - $this->formatter->setConvertToString( - static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), + $formatter = new Formatter( + stringConverter: static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), ); $object = new class { public function __toString(): string @@ -363,33 +328,35 @@ public function __toString(): string 'time' => 1_508_160_390, 'obj' => $object, ]); - $result = $this->formatter->format($message, []); + $result = $formatter->format($message, []); $this->assertStringContainsString('obj: {}', $result); $this->assertStringNotContainsString('stringable-object', $result); } - public function testDefaultFormatWithSetConvertToStringDoesNotAffectTrace(): void + public function testStringConverterDoesNotAffectTrace(): void { $called = false; - $this->formatter->setConvertToString(static function (mixed $value) use (&$called): string { - $called = true; - return json_encode($value, JSON_THROW_ON_ERROR); - }); - $this->formatter->setTimestampFormat('Y-m-d H:i:s'); + $formatter = new Formatter( + stringConverter: static function (mixed $value) use (&$called): string { + $called = true; + return json_encode($value, JSON_THROW_ON_ERROR); + }, + ); + $formatter->setTimestampFormat('Y-m-d H:i:s'); $message = new Message(LogLevel::INFO, 'message', [ 'time' => 1_508_160_390, 'trace' => [['file' => '/path/to/file', 'line' => 99]], ]); - $result = $this->formatter->format($message, []); + $result = $formatter->format($message, []); $this->assertStringContainsString("trace:\n in /path/to/file:99", $result); $this->assertTrue($called); } - public function testDefaultFormatWithSetContextFormatReceivesEmptyTrace(): void + public function testContextFormatReceivesEmptyTrace(): void { $receivedTrace = 'not-called'; - $this->formatter->setContextFormat( - static function (string $trace, string $messageContext, string $commonContext) use (&$receivedTrace): string { + $formatter = new Formatter( + contextFormat: static function (string $trace, string $messageContext, string $commonContext) use (&$receivedTrace): string { $receivedTrace = $trace; return "\n" . $messageContext; }, @@ -398,15 +365,15 @@ static function (string $trace, string $messageContext, string $commonContext) u 'category' => 'app', 'time' => 1_508_160_390, ]); - $this->formatter->format($message, []); + $formatter->format($message, []); $this->assertSame('', $receivedTrace); } - public function testDefaultFormatWithSetContextFormatReceivesEmptyCommonContext(): void + public function testContextFormatReceivesEmptyCommonContext(): void { $receivedCommon = 'not-called'; - $this->formatter->setContextFormat( - static function (string $trace, string $messageContext, string $commonContext) use (&$receivedCommon): string { + $formatter = new Formatter( + contextFormat: static function (string $trace, string $messageContext, string $commonContext) use (&$receivedCommon): string { $receivedCommon = $commonContext; return "\n" . $messageContext; }, @@ -415,18 +382,14 @@ static function (string $trace, string $messageContext, string $commonContext) u 'category' => 'app', 'time' => 1_508_160_390, ]); - $this->formatter->format($message, []); + $formatter->format($message, []); $this->assertSame('', $receivedCommon); } - public function testDefaultFormatWithSetConvertToStringAndSetContextFormat(): void + public function testDefaultFormatWithStringConverterAndContextFormat(): void { - $this->formatter->setTimestampFormat('Y-m-d H:i:s'); - $this->formatter->setConvertToString( - static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), - ); - $this->formatter->setContextFormat( - static function (string $trace, string $messageContext, string $commonContext): string { + $formatter = new Formatter( + contextFormat: static function (string $trace, string $messageContext, string $commonContext): string { $result = ''; if ($commonContext !== '') { $result .= "\n[C] " . $commonContext; @@ -436,7 +399,9 @@ static function (string $trace, string $messageContext, string $commonContext): } return $result; }, + stringConverter: static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), ); + $formatter->setTimestampFormat('Y-m-d H:i:s'); $message = new Message(LogLevel::INFO, 'message', [ 'category' => 'app', 'time' => 1_508_160_390, @@ -445,7 +410,7 @@ static function (string $trace, string $messageContext, string $commonContext): . "\n[C] server: \"web\"" . "\n[M] category: \"app\"\ntime: 1508160390" ; - $this->assertSame($expected, $this->formatter->format($message, ['server' => 'web'])); + $this->assertSame($expected, $formatter->format($message, ['server' => 'web'])); } public function testTraceWithFileWithoutLineUsesFunction(): void @@ -519,20 +484,20 @@ public function testFormatMessageThrowExceptionForPrefixCallableReturnNotString( $this->formatter->format(new Message(LogLevel::INFO, 'test', ['foo' => 'bar']), []); } - public function testFormatThrowExceptionForConvertToStringCallableReturnNotString(): void + public function testFormatThrowExceptionForStringConverterReturnNotString(): void { - $this->formatter->setConvertToString(static fn(mixed $value) => 123); + $formatter = new Formatter(stringConverter: static fn(mixed $value) => 123); $this->expectException(RuntimeException::class); - $this->formatter->format(new Message(LogLevel::INFO, 'test', ['foo' => 'bar']), []); + $formatter->format(new Message(LogLevel::INFO, 'test', ['foo' => 'bar']), []); } public function testFormatThrowExceptionForContextFormatCallableReturnNotString(): void { - $this->formatter->setContextFormat( - static fn(string $trace, string $messageContext, string $commonContext) => 123, + $formatter = new Formatter( + contextFormat: static fn(string $trace, string $messageContext, string $commonContext) => 123, ); $this->expectException(RuntimeException::class); - $this->formatter->format(new Message(LogLevel::INFO, 'test', ['foo' => 'bar']), []); + $formatter->format(new Message(LogLevel::INFO, 'test', ['foo' => 'bar']), []); } public static function dataTime(): array diff --git a/tests/TargetTest.php b/tests/TargetTest.php index 2026ca0e..8c72f9be 100644 --- a/tests/TargetTest.php +++ b/tests/TargetTest.php @@ -258,10 +258,10 @@ public function testSetLevelsThrowExceptionForNonStringList(array $list): void $this->target->setLevels($list); } - public function testSetConvertToString(): void + public function testStringConverter(): void { - $this->target->setConvertToString( - static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), + $this->target = new DummyTarget( + stringConverter: static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR), ); $this->target->setCommonContext(['server' => 'web']); $this->collectOneAndExport(LogLevel::INFO, 'message', [ @@ -275,10 +275,10 @@ public function testSetConvertToString(): void $this->assertSame($expected, $this->target->formatMessages()); } - public function testSetContextTemplate(): void + public function testContextTemplate(): void { + $this->target = new DummyTarget(contextFormat: "{common}{message}\n"); $this->target->setTimestampFormat('Y-m-d H:i:s'); - $this->target->setContextTemplate("{common}{message}\n"); $this->target->setCommonContext(['server' => 'web']); $this->collectOneAndExport(LogLevel::INFO, 'message', [ 'category' => 'app', @@ -292,11 +292,10 @@ public function testSetContextTemplate(): void $this->assertSame($expected, $this->target->formatMessages()); } - public function testSetContextFormat(): void + public function testContextFormat(): void { - $this->target->setTimestampFormat('Y-m-d H:i:s'); - $this->target->setContextFormat( - static function (string $trace, string $messageContext, string $commonContext): string { + $this->target = new DummyTarget( + contextFormat: static function (string $trace, string $messageContext, string $commonContext): string { $result = ''; if ($commonContext !== '') { $result .= "\n\nCommon:\n" . $commonContext; @@ -307,6 +306,7 @@ static function (string $trace, string $messageContext, string $commonContext): return $result; }, ); + $this->target->setTimestampFormat('Y-m-d H:i:s'); $this->target->setCommonContext(['server' => 'web']); $this->collectOneAndExport(LogLevel::INFO, 'message', [ 'category' => 'app', diff --git a/tests/TestAsset/DummyTarget.php b/tests/TestAsset/DummyTarget.php index 4e78381b..704a74fa 100644 --- a/tests/TestAsset/DummyTarget.php +++ b/tests/TestAsset/DummyTarget.php @@ -14,10 +14,13 @@ final class DummyTarget extends Target private array $exportMessages = []; private Formatter $exportFormatter; - public function __construct(array $levels = []) - { - parent::__construct($levels); - $this->exportFormatter = new Formatter(); + public function __construct( + array $levels = [], + string|callable|null $contextFormat = null, + ?callable $stringConverter = null, + ) { + parent::__construct($levels, $contextFormat, $stringConverter); + $this->exportFormatter = new Formatter($contextFormat, $stringConverter); } public function export(): void @@ -82,24 +85,6 @@ public function getCommonContext(): array return parent::getCommonContext(); } - public function setContextFormat(callable $contextFormat): self - { - $this->exportFormatter->setContextFormat($contextFormat); - return parent::setContextFormat($contextFormat); - } - - public function setContextTemplate(string $contextTemplate): self - { - $this->exportFormatter->setContextTemplate($contextTemplate); - return parent::setContextTemplate($contextTemplate); - } - - public function setConvertToString(callable $convertToString): self - { - $this->exportFormatter->setConvertToString($convertToString); - return parent::setConvertToString($convertToString); - } - public function setFormat(callable $format): self { $this->exportFormatter->setFormat($format);