diff --git a/src/Infrastructure/Exception/ExceptionDebugFormatter.php b/src/Infrastructure/Exception/ExceptionDebugFormatter.php new file mode 100644 index 0000000..29f6945 --- /dev/null +++ b/src/Infrastructure/Exception/ExceptionDebugFormatter.php @@ -0,0 +1,30 @@ + \get_class($e), + 'message' => $e->getMessage(), + 'thrownAt' => $e->getFile() . ':' . $e->getLine(), + ]; + + if (!$skipContext && $e instanceof HasContextInterface) { + $debugInfo = array_merge($debugInfo, [ + 'context' => $e->getContext(), + ]); + } + + if ($root = $e->getPrevious()) { + $debugInfo['parentException'] = $this->format($root, $skipContext); + } + return $debugInfo; + } +} diff --git a/src/exception/HasContext.php b/src/exception/HasContext.php index a928c80..c24c2c1 100644 --- a/src/exception/HasContext.php +++ b/src/exception/HasContext.php @@ -4,6 +4,7 @@ namespace hidev\exception; +use hidev\Infrastructure\Exception\ExceptionDebugFormatter; use Throwable; use yii\helpers\ArrayHelper; @@ -37,4 +38,37 @@ public function getContextValue(string $fieldName) { return ArrayHelper::getValue($this->context, $fieldName, ''); } + + public function getFormattedContext(): string + { + $text = ''; + $context = $this->getContext(); + if ($previous = $this->getPrevious()) { + $context['previousException'] = $this->getExceptionDebugInfo($previous); + } + + if ($context) { + $text .= PHP_EOL . PHP_EOL . 'Context:'; + + foreach ($context as $key => $value) { + $stringValue = is_array($value) || is_object($value) + ? $this->jsonEncode($value) + : trim((string)$value); + + $text .= PHP_EOL . '− ' . $key . ': ' . $stringValue; + } + } + + return $text; + } + + private function getExceptionDebugInfo(?Throwable $throwable): array + { + return (new ExceptionDebugFormatter())->format($throwable); + } + + private function jsonEncode($value): string + { + return \json_encode($value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); + } } diff --git a/src/exception/HasContextInterface.php b/src/exception/HasContextInterface.php index 1b6b578..aeacc20 100644 --- a/src/exception/HasContextInterface.php +++ b/src/exception/HasContextInterface.php @@ -7,4 +7,6 @@ interface HasContextInterface extends \Throwable public function addContext(array $data): self; public function getContext(): array; + + public function getFormattedContext(): string; } diff --git a/tests/unit/exception/HasContextTest.php b/tests/unit/exception/HasContextTest.php index d1a04b4..3dc2edb 100644 --- a/tests/unit/exception/HasContextTest.php +++ b/tests/unit/exception/HasContextTest.php @@ -59,4 +59,59 @@ public function testGetContextValueReturnsDefaultWhenMissing(): void $this->assertSame('', $e->getContextValue('missing.key')); } + + // ----------------------------------------------------- + // getFormattedContext() Tests + // ----------------------------------------------------- + + public function testGetFormattedContextReturnsEmptyStringWhenNoContext(): void + { + $e = new TestException('Error'); + + $this->assertSame('', $e->getFormattedContext()); + } + + public function testGetFormattedContextFormatsSimpleContext(): void + { + $e = new TestException('Error'); + $e->addContext([ + 'key' => 'value', + 'number' => 123, + ]); + + $output = $e->getFormattedContext(); + + $this->assertStringContainsString('Context:', $output); + $this->assertStringContainsString('key: value', $output); + $this->assertStringContainsString('number: 123', $output); + } + + public function testGetFormattedContextFormatsArrayValuesAsJson(): void + { + $e = new TestException('Error'); + $e->addContext([ + 'data' => ['a' => 1, 'b' => 2], + ]); + + $output = $e->getFormattedContext(); + + // Pretty-printed JSON + $this->assertStringContainsString('"a": 1', $output); + $this->assertStringContainsString('"b": 2', $output); + } + + public function testGetFormattedContextIncludesPreviousException(): void + { + $previous = new TestException('Previous error'); + $previous->addContext(['prevKey' => 'prevValue']); + + $e = new TestException('Main error', 0, $previous); + $e->addContext(['key' => 'value']); + + $output = $e->getFormattedContext(); + + $this->assertStringContainsString('previousException', $output); + $this->assertStringContainsString('prevKey', $output); + $this->assertStringContainsString('prevValue', $output); + } }