From 50f9dac54bee3a1d93f3bbdc9c87c86174dbb30f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 7 Jul 2026 23:00:35 +0200 Subject: [PATCH 1/6] Fix Logger PSR-3 compliance and interpolation issues --- system/Log/Logger.php | 14 ++++-- tests/system/Log/LoggerTest.php | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 011920e15bd1..3399bf77471f 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -291,10 +291,14 @@ public function log($level, string|Stringable $message, array $context = []): vo */ protected function interpolate($message, array $context = []) { - if (! is_string($message)) { + + if (! is_string($message) && ! ($message instanceof Stringable)) { return print_r($message, true); } + // Cast to string in case $message is a Stringable object + $message = (string) $message; + $replace = []; foreach ($context as $key => $val) { @@ -305,6 +309,10 @@ protected function interpolate($message, array $context = []) } // todo - sanitize input before writing to file? + if (is_array($val) || (is_object($val) && ! method_exists($val, '__toString'))) { + $val = print_r($val, true); + } + $replace['{' . $key . '}'] = $val; } @@ -322,9 +330,9 @@ protected function interpolate($message, array $context = []) // Match up environment variables in {env:foo} tags. if (str_contains($message, 'env:')) { - preg_match('/env:[^}]+/', $message, $matches); + preg_match_all('/env:[^}]+/', $message, $matches); - foreach ($matches as $str) { + foreach ($matches[0] as $str) { $key = str_replace('env:', '', $str); $replace["{{$str}}"] = $_ENV[$key] ?? 'n/a'; } diff --git a/tests/system/Log/LoggerTest.php b/tests/system/Log/LoggerTest.php index 80d42d6c83b3..d7d96dcff2a1 100644 --- a/tests/system/Log/LoggerTest.php +++ b/tests/system/Log/LoggerTest.php @@ -22,6 +22,8 @@ use PHPUnit\Framework\Attributes\Group; use ReflectionMethod; use ReflectionNamedType; +use stdClass; +use Stringable; use Tests\Support\Log\Handlers\TestHandler; /** @@ -438,4 +440,85 @@ public function testDetermineFileNoStackTrace(): void $this->assertSame($expected, $logger->determineFile()); } + + public function testLogInterpolatesArrayContext(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message ' . print_r(['foo' => 'bar'], true); + + $logger->log('debug', 'Test message {foo}', ['foo' => ['foo' => 'bar']]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogInterpolatesObjectContext(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $obj = new stdClass(); + $obj->foo = 'bar'; + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message ' . print_r($obj, true); + + $logger->log('debug', 'Test message {foo}', ['foo' => $obj]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogInterpolatesMultipleEnvironmentVars(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $_ENV['foo'] = 'bar'; + $_ENV['baz'] = 'qux'; + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message bar and qux'; + + $logger->log('debug', 'Test message {env:foo} and {env:baz}'); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogAcceptsStringableMessage(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $message = new class () implements Stringable { + public function __toString(): string + { + return 'Stringable message {foo}'; + } + }; + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Stringable message bar'; + + $logger->log('debug', $message, ['foo' => 'bar']); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } } From 8837e2e072f730360c288da632ef5e8a53d67d55 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 7 Jul 2026 23:07:50 +0200 Subject: [PATCH 2/6] style: apply code standards (cs-fix) --- system/Log/Logger.php | 1 - 1 file changed, 1 deletion(-) diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 3399bf77471f..dc5559752763 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -291,7 +291,6 @@ public function log($level, string|Stringable $message, array $context = []): vo */ protected function interpolate($message, array $context = []) { - if (! is_string($message) && ! ($message instanceof Stringable)) { return print_r($message, true); } From 368cd4539fd0743e4e29c051c908ee1f28f13b37 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 7 Jul 2026 23:24:35 +0200 Subject: [PATCH 3/6] chore: trigger tests From 6b25f1e1bb1099a0cdd1076068bc0b1da918b5bd Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Jul 2026 18:08:44 +0200 Subject: [PATCH 4/6] test: add tests for context object properties and boolean false stringification --- tests/system/Log/LoggerTest.php | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/system/Log/LoggerTest.php b/tests/system/Log/LoggerTest.php index d7d96dcff2a1..d6769eae8452 100644 --- a/tests/system/Log/LoggerTest.php +++ b/tests/system/Log/LoggerTest.php @@ -521,4 +521,43 @@ public function __toString(): string $this->assertCount(1, $logs); $this->assertSame($expected, $logs[0]); } + + public function testLogObjectContextLosesProperties(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $user = new stdClass(); + $user->name = 'John'; + $user->role = 'admin'; + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> User: ' . print_r($user, true); + + $logger->log('debug', 'User: {user}', ['user' => $user]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogBooleanFalseIsLoggedAsEmptyString(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Active: false'; + + $logger->log('debug', 'Active: {status}', ['status' => false]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } } + From 603b9d5cca324ce9ba64ca3a0d1c38c7d201ffc3 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Jul 2026 18:10:30 +0200 Subject: [PATCH 5/6] fix: format boolean values as true/false during interpolation --- system/Log/Logger.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/Log/Logger.php b/system/Log/Logger.php index dc5559752763..2308aece1962 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -307,6 +307,10 @@ protected function interpolate($message, array $context = []) $val = $val->getMessage() . ' ' . clean_path($val->getFile()) . ':' . $val->getLine(); } + if (is_bool($val)) { + $val = $val ? 'true' : 'false'; + } + // todo - sanitize input before writing to file? if (is_array($val) || (is_object($val) && ! method_exists($val, '__toString'))) { $val = print_r($val, true); From 5c624cbcf62c7ae9aba4391cb8ee0cb3933205ce Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Jul 2026 18:14:31 +0200 Subject: [PATCH 6/6] style: apply code standards (cs-fix) --- tests/system/Log/LoggerTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/system/Log/LoggerTest.php b/tests/system/Log/LoggerTest.php index d6769eae8452..0b309b9ee8c5 100644 --- a/tests/system/Log/LoggerTest.php +++ b/tests/system/Log/LoggerTest.php @@ -529,7 +529,7 @@ public function testLogObjectContextLosesProperties(): void Time::setTestNow('2023-11-25 12:00:00'); - $user = new stdClass(); + $user = new stdClass(); $user->name = 'John'; $user->role = 'admin'; @@ -560,4 +560,3 @@ public function testLogBooleanFalseIsLoggedAsEmptyString(): void $this->assertSame($expected, $logs[0]); } } -