From 950395ec8d00695e3b311a101af19ea81074427e Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Sun, 27 Oct 2024 21:49:11 +0100 Subject: [PATCH] feat: handle `JSON_PRETTY_PRINT` option with the JSON normalizer Formats the JSON with whitespaces and line breaks. ```php $input = [ 'value' => 'foo', 'list' => [ 'foo', 42, ['sub'] ], 'associative' => [ 'value' => 'foo', 'sub' => [ 'string' => 'foo', 'integer' => 42, ], ], ]; (new \CuyZ\Valinor\MapperBuilder()) ->normalizer(\CuyZ\Valinor\Normalizer\Format::json()) ->withOptions(\JSON_PRETTY_PRINT) ->normalize($input); // Result: // { // "value": "foo", // "list": [ // "foo", // 42, // [ // "sub" // ] // ], // "associative": { // "value": "foo", // "sub": { // "string": "foo", // "integer": 42 // } // } // } --- docs/pages/serialization/normalizing-json.md | 1 + src/Normalizer/Formatter/JsonFormatter.php | 34 +++++++++-- src/Normalizer/JsonNormalizer.php | 1 + .../Integration/Normalizer/NormalizerTest.php | 61 +++++++++++++++++-- 4 files changed, 86 insertions(+), 11 deletions(-) diff --git a/docs/pages/serialization/normalizing-json.md b/docs/pages/serialization/normalizing-json.md index a935d92e..0b931cda 100644 --- a/docs/pages/serialization/normalizing-json.md +++ b/docs/pages/serialization/normalizing-json.md @@ -109,6 +109,7 @@ representations ([see official doc for more information]): - `JSON_INVALID_UTF8_IGNORE` - `JSON_INVALID_UTF8_SUBSTITUTE` - `JSON_NUMERIC_CHECK` +- `JSON_PRETTY_PRINT` - `JSON_PRESERVE_ZERO_FRACTION` - `JSON_UNESCAPED_LINE_TERMINATORS` - `JSON_UNESCAPED_SLASHES` diff --git a/src/Normalizer/Formatter/JsonFormatter.php b/src/Normalizer/Formatter/JsonFormatter.php index e7dfe298..33c591b1 100644 --- a/src/Normalizer/Formatter/JsonFormatter.php +++ b/src/Normalizer/Formatter/JsonFormatter.php @@ -18,7 +18,6 @@ use function json_encode; use const JSON_FORCE_OBJECT; -use const JSON_THROW_ON_ERROR; /** @internal */ final class JsonFormatter implements StreamFormatter @@ -32,6 +31,11 @@ public function __construct( ) {} public function format(mixed $value): void + { + $this->formatRecursively($value, 1); + } + + private function formatRecursively(mixed $value, int $depth): void { if (is_null($value)) { $this->write('null'); @@ -66,8 +70,14 @@ public function format(mixed $value): void $this->write($isList ? '[' : '{'); foreach ($value as $key => $val) { + $chunk = ''; + if (! $isFirst) { - $this->write(','); + $chunk = ','; + } + + if ($this->jsonEncodingOptions & JSON_PRETTY_PRINT) { + $chunk .= PHP_EOL . str_repeat(' ', $depth); } $isFirst = false; @@ -75,13 +85,27 @@ public function format(mixed $value): void if (! $isList) { $key = json_encode((string)$key, $this->jsonEncodingOptions); - $this->write($key . ':'); + $chunk .= $key . ':'; + + if ($this->jsonEncodingOptions & JSON_PRETTY_PRINT) { + $chunk .= ' '; + } } - $this->format($val); + $this->write($chunk); + + $this->formatRecursively($val, $depth + 1); + } + + $chunk = ''; + + if ($this->jsonEncodingOptions & JSON_PRETTY_PRINT) { + $chunk = PHP_EOL . str_repeat(' ', $depth - 1); } - $this->write($isList ? ']' : '}'); + $chunk .= $isList ? ']' : '}'; + + $this->write($chunk); } else { throw new CannotFormatInvalidTypeToJson($value); } diff --git a/src/Normalizer/JsonNormalizer.php b/src/Normalizer/JsonNormalizer.php index 3ccb8702..2525bd24 100644 --- a/src/Normalizer/JsonNormalizer.php +++ b/src/Normalizer/JsonNormalizer.php @@ -43,6 +43,7 @@ final class JsonNormalizer implements Normalizer | JSON_INVALID_UTF8_IGNORE | JSON_INVALID_UTF8_SUBSTITUTE | JSON_NUMERIC_CHECK + | JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_LINE_TERMINATORS | JSON_UNESCAPED_SLASHES diff --git a/tests/Integration/Normalizer/NormalizerTest.php b/tests/Integration/Normalizer/NormalizerTest.php index 88e1aa95..6758868d 100644 --- a/tests/Integration/Normalizer/NormalizerTest.php +++ b/tests/Integration/Normalizer/NormalizerTest.php @@ -747,6 +747,61 @@ public function getIterator(): Traversable 'expected array' => [], 'expected_json' => '{}', ]; + + yield 'nested array with JSON_PRETTY_PRINT option' => [ + 'input' => [ + 'value' => 'foo', + 'list' => [ + 'foo', + 42, + ['sub'] + ], + 'associative' => [ + 'value' => 'foo', + 'sub' => [ + 'string' => 'foo', + 'integer' => 42, + ], + ], + ], + 'expected array' => [ + 'value' => 'foo', + 'list' => [ + 'foo', + 42, + ['sub'] + ], + 'associative' => [ + 'value' => 'foo', + 'sub' => [ + 'string' => 'foo', + 'integer' => 42, + ], + ], + ], + 'expected_json' => << [], + 'transformerAttributes' => [], + 'jsonEncodingOptions' => JSON_PRETTY_PRINT, + ]; } public function test_generator_of_scalar_yields_expected_array(): void @@ -1176,12 +1231,6 @@ public function test_json_transformer_only_accepts_acceptable_json_options(): vo { $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PARTIAL_OUTPUT_ON_ERROR); self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); - - $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PRETTY_PRINT); - self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); - - $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_PRETTY_PRINT); - self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); } }