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
25 changes: 24 additions & 1 deletion src/Service/Infrastructure/LoggerService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}
109 changes: 109 additions & 0 deletions tests/Unit/LoggerServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace SeQura\Middleware\Tests\Unit;

use PHPUnit\Framework\TestCase;
use SeQura\Middleware\Service\Infrastructure\LoggerService;

/**
* Class LoggerServiceTest
*
* @package SeQura\Middleware\Tests\Unit
*/
class LoggerServiceTest extends TestCase
{
private \ReflectionMethod $formatContextValue;
private LoggerService $loggerService;

protected function setUp(): void
{
parent::setUp();

$this->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);
}
}