diff --git a/src/Service/Infrastructure/LoggerService.php b/src/Service/Infrastructure/LoggerService.php index d7114b0..5ea47d5 100644 --- a/src/Service/Infrastructure/LoggerService.php +++ b/src/Service/Infrastructure/LoggerService.php @@ -45,7 +45,7 @@ public function logMessage(LogData $data): void if (!empty($context)) { $contextData = array(); foreach ($context as $item) { - $contextData[$item->getName()] = print_r($item->getValue(), true); + $contextData[$item->getName()] = $this->formatContextValue($item->getValue()); } $logMessage .= PHP_EOL . 'Context data: ' . print_r($contextData, true); @@ -65,4 +65,27 @@ public function logMessage(LogData $data): void Log::info($logMessage); } } + + /** + * Formats a context value for safe logging without unbounded memory allocation. + * + * @param mixed $value + * + * @return string + */ + private function formatContextValue(mixed $value): string + { + if ($value instanceof \Throwable) { + return get_class($value) . ': ' . $value->getMessage() + . ' in ' . $value->getFile() . ':' . $value->getLine(); + } + + if (is_object($value)) { + $encoded = json_encode($value, JSON_PARTIAL_OUTPUT_ON_ERROR, 5); + + return $encoded !== false ? $encoded : get_class($value) . ' (not serializable)'; + } + + return print_r($value, true); + } } diff --git a/tests/Unit/LoggerServiceTest.php b/tests/Unit/LoggerServiceTest.php new file mode 100644 index 0000000..a0f7d8c --- /dev/null +++ b/tests/Unit/LoggerServiceTest.php @@ -0,0 +1,109 @@ +loggerService = LoggerService::getInstance(); + + $this->formatContextValue = new \ReflectionMethod(LoggerService::class, 'formatContextValue'); + $this->formatContextValue->setAccessible(true); + } + + public function testThrowableIsFormattedWithoutPrintR(): void + { + $exception = new \RuntimeException('Something went wrong'); + + $result = $this->formatContextValue->invoke($this->loggerService, $exception); + + $this->assertStringContainsString('RuntimeException: Something went wrong', $result); + $this->assertStringContainsString(__FILE__, $result); + // Must NOT contain full stack trace output from print_r + $this->assertStringNotContainsString('#0 ', $result); + $this->assertStringNotContainsString('Array', $result); + } + + public function testNestedExceptionOnlyShowsOuterException(): void + { + $previous = new \InvalidArgumentException('Root cause'); + $exception = new \RuntimeException('Wrapper', 0, $previous); + + $result = $this->formatContextValue->invoke($this->loggerService, $exception); + + $this->assertStringContainsString('RuntimeException: Wrapper', $result); + // Should NOT recurse into the previous exception + $this->assertStringNotContainsString('Root cause', $result); + $this->assertStringNotContainsString('#0 ', $result); + } + + public function testObjectUsesJsonEncode(): void + { + $object = new \stdClass(); + $object->key = 'value'; + $object->number = 42; + + $result = $this->formatContextValue->invoke($this->loggerService, $object); + + $this->assertStringContainsString('"key":"value"', $result); + $this->assertStringContainsString('"number":42', $result); + } + + public function testNonSerializableObjectFallsBackToClassName(): void + { + $object = new class { + public float $value = NAN; + }; + + $result = $this->formatContextValue->invoke($this->loggerService, $object); + + // JSON_PARTIAL_OUTPUT_ON_ERROR will produce output or fall back to class name + $this->assertNotEmpty($result); + // Should not crash or produce unbounded output + $this->assertLessThan(1000, strlen($result)); + } + + public function testScalarStringIsReturnedAsIs(): void + { + $result = $this->formatContextValue->invoke($this->loggerService, 'simple string'); + + $this->assertEquals('simple string', $result); + } + + public function testIntegerIsReturnedViaPrintR(): void + { + $result = $this->formatContextValue->invoke($this->loggerService, 42); + + $this->assertEquals('42', $result); + } + + public function testArrayIsReturnedViaPrintR(): void + { + $result = $this->formatContextValue->invoke($this->loggerService, ['a', 'b', 'c']); + + $this->assertStringContainsString('a', $result); + $this->assertStringContainsString('b', $result); + $this->assertStringContainsString('c', $result); + } + + public function testNullIsHandled(): void + { + $result = $this->formatContextValue->invoke($this->loggerService, null); + + $this->assertIsString($result); + } +}