From d662f85c65b7f293a0d5389ebb7a3cac79c17924 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Mon, 2 Jun 2025 13:57:50 +0200 Subject: [PATCH] [JsonStreamer] Add `include_null_properties` option --- .../Component/JsonStreamer/CHANGELOG.md | 1 + .../JsonStreamer/JsonStreamWriter.php | 5 +- .../Model/DummyWithDollarNamedProperties.php | 14 +++ .../stream_writer/double_nested_list.php | 45 ++++---- .../Fixtures/stream_writer/nested_list.php | 31 +++--- .../Tests/Fixtures/stream_writer/null.php | 2 +- .../stream_writer/nullable_backed_enum.php | 2 +- .../stream_writer/nullable_object.php | 10 +- .../stream_writer/nullable_object_dict.php | 19 ++-- .../stream_writer/nullable_object_list.php | 19 ++-- .../Tests/Fixtures/stream_writer/object.php | 8 +- .../Fixtures/stream_writer/object_dict.php | 17 +-- .../stream_writer/object_in_object.php | 20 ++-- .../stream_writer/object_iterable.php | 17 +-- .../Fixtures/stream_writer/object_list.php | 17 +-- .../object_with_dollar_named_properties.php | 18 ++++ .../stream_writer/object_with_union.php | 25 +++-- .../object_with_value_transformer.php | 12 ++- .../stream_writer/self_referencing_object.php | 15 +-- .../Tests/Fixtures/stream_writer/union.php | 18 ++-- .../Tests/JsonStreamWriterTest.php | 21 +++- .../Tests/Write/StreamWriterGeneratorTest.php | 2 + .../JsonStreamer/Write/PhpGenerator.php | 101 +++++++++++++----- 23 files changed, 291 insertions(+), 148 deletions(-) create mode 100644 src/Symfony/Component/JsonStreamer/Tests/Fixtures/Model/DummyWithDollarNamedProperties.php create mode 100644 src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_dollar_named_properties.php diff --git a/src/Symfony/Component/JsonStreamer/CHANGELOG.md b/src/Symfony/Component/JsonStreamer/CHANGELOG.md index fdc1439a90748..f271c7e1964c4 100644 --- a/src/Symfony/Component/JsonStreamer/CHANGELOG.md +++ b/src/Symfony/Component/JsonStreamer/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Remove `nikic/php-parser` dependency * Add `_current_object` to the context passed to value transformers during write operations + * Add `include_null_properties` option to encode the properties with `null` value 7.3 --- diff --git a/src/Symfony/Component/JsonStreamer/JsonStreamWriter.php b/src/Symfony/Component/JsonStreamer/JsonStreamWriter.php index bbe31af9de57a..638d0acd07167 100644 --- a/src/Symfony/Component/JsonStreamer/JsonStreamWriter.php +++ b/src/Symfony/Component/JsonStreamer/JsonStreamWriter.php @@ -29,7 +29,10 @@ /** * @author Mathias Arlaud * - * @implements StreamWriterInterface> + * @implements StreamWriterInterface, + * }> * * @experimental */ diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/Model/DummyWithDollarNamedProperties.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/Model/DummyWithDollarNamedProperties.php new file mode 100644 index 0000000000000..531c490aece99 --- /dev/null +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/Model/DummyWithDollarNamedProperties.php @@ -0,0 +1,14 @@ +bar}')] + public bool $bar = true; +} diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/double_nested_list.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/double_nested_list.php index c793d180e648d..507b7b5288950 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/double_nested_list.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/double_nested_list.php @@ -5,36 +5,41 @@ */ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { - yield '['; - $prefix = ''; + yield "["; + $prefix1 = ''; foreach ($data as $value1) { - yield $prefix; - yield '{"dummies":['; - $prefix = ''; + $prefix2 = ''; + yield "{$prefix1}{{$prefix2}\"dummies\":"; + yield "["; + $prefix3 = ''; foreach ($value1->dummies as $value2) { - yield $prefix; - yield '{"dummies":['; - $prefix = ''; + $prefix4 = ''; + yield "{$prefix3}{{$prefix4}\"dummies\":"; + yield "["; + $prefix5 = ''; foreach ($value2->dummies as $value3) { - yield $prefix; - yield '{"id":'; + $prefix6 = ''; + yield "{$prefix5}{{$prefix6}\"id\":"; yield \json_encode($value3->id, \JSON_THROW_ON_ERROR, 506); - yield ',"name":'; + $prefix6 = ','; + yield "{$prefix6}\"name\":"; yield \json_encode($value3->name, \JSON_THROW_ON_ERROR, 506); - yield '}'; - $prefix = ','; + yield "}"; + $prefix5 = ','; } - yield '],"customProperty":'; + $prefix4 = ','; + yield "]{$prefix4}\"customProperty\":"; yield \json_encode($value2->customProperty, \JSON_THROW_ON_ERROR, 508); - yield '}'; - $prefix = ','; + yield "}"; + $prefix3 = ','; } - yield '],"stringProperty":'; + $prefix2 = ','; + yield "]{$prefix2}\"stringProperty\":"; yield \json_encode($value1->stringProperty, \JSON_THROW_ON_ERROR, 510); - yield '}'; - $prefix = ','; + yield "}"; + $prefix1 = ','; } - yield ']'; + yield "]"; } catch (\JsonException $e) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nested_list.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nested_list.php index 292ee5fe00245..debe2321cdfb0 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nested_list.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nested_list.php @@ -5,27 +5,30 @@ */ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { - yield '['; - $prefix = ''; + yield "["; + $prefix1 = ''; foreach ($data as $value1) { - yield $prefix; - yield '{"dummies":['; - $prefix = ''; + $prefix2 = ''; + yield "{$prefix1}{{$prefix2}\"dummies\":"; + yield "["; + $prefix3 = ''; foreach ($value1->dummies as $value2) { - yield $prefix; - yield '{"id":'; + $prefix4 = ''; + yield "{$prefix3}{{$prefix4}\"id\":"; yield \json_encode($value2->id, \JSON_THROW_ON_ERROR, 508); - yield ',"name":'; + $prefix4 = ','; + yield "{$prefix4}\"name\":"; yield \json_encode($value2->name, \JSON_THROW_ON_ERROR, 508); - yield '}'; - $prefix = ','; + yield "}"; + $prefix3 = ','; } - yield '],"customProperty":'; + $prefix2 = ','; + yield "]{$prefix2}\"customProperty\":"; yield \json_encode($value1->customProperty, \JSON_THROW_ON_ERROR, 510); - yield '}'; - $prefix = ','; + yield "}"; + $prefix1 = ','; } - yield ']'; + yield "]"; } catch (\JsonException $e) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/null.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/null.php index bc35cb47ccfd2..3ddfeda41fadf 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/null.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/null.php @@ -5,7 +5,7 @@ */ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { - yield 'null'; + yield "null"; } catch (\JsonException $e) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_backed_enum.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_backed_enum.php index 76ed43bba41f5..c9fec1503601e 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_backed_enum.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_backed_enum.php @@ -8,7 +8,7 @@ if ($data instanceof \Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum) { yield \json_encode($data->value, \JSON_THROW_ON_ERROR, 512); } elseif (null === $data) { - yield 'null'; + yield "null"; } else { throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value.', \get_debug_type($data))); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object.php index 7a9228464cf11..77499e1569d3f 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object.php @@ -6,13 +6,15 @@ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { if ($data instanceof \Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes) { - yield '{"@id":'; + $prefix1 = ''; + yield "{{$prefix1}\"@id\":"; yield \json_encode($data->id, \JSON_THROW_ON_ERROR, 511); - yield ',"name":'; + $prefix1 = ','; + yield "{$prefix1}\"name\":"; yield \json_encode($data->name, \JSON_THROW_ON_ERROR, 511); - yield '}'; + yield "}"; } elseif (null === $data) { - yield 'null'; + yield "null"; } else { throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value.', \get_debug_type($data))); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_dict.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_dict.php index 690346221c8f8..e811c49cff792 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_dict.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_dict.php @@ -6,21 +6,22 @@ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { if (\is_array($data)) { - yield '{'; - $prefix = ''; + yield "{"; + $prefix1 = ''; foreach ($data as $key1 => $value1) { $key1 = \substr(\json_encode($key1), 1, -1); - yield "{$prefix}\"{$key1}\":"; - yield '{"@id":'; + $prefix2 = ''; + yield "{$prefix1}\"{$key1}\":{{$prefix2}\"@id\":"; yield \json_encode($value1->id, \JSON_THROW_ON_ERROR, 510); - yield ',"name":'; + $prefix2 = ','; + yield "{$prefix2}\"name\":"; yield \json_encode($value1->name, \JSON_THROW_ON_ERROR, 510); - yield '}'; - $prefix = ','; + yield "}"; + $prefix1 = ','; } - yield '}'; + yield "}"; } elseif (null === $data) { - yield 'null'; + yield "null"; } else { throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value.', \get_debug_type($data))); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_list.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_list.php index 7cc2df4c67a5a..ed64975b984d0 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_list.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/nullable_object_list.php @@ -6,20 +6,21 @@ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { if (\is_array($data)) { - yield '['; - $prefix = ''; + yield "["; + $prefix1 = ''; foreach ($data as $value1) { - yield $prefix; - yield '{"@id":'; + $prefix2 = ''; + yield "{$prefix1}{{$prefix2}\"@id\":"; yield \json_encode($value1->id, \JSON_THROW_ON_ERROR, 510); - yield ',"name":'; + $prefix2 = ','; + yield "{$prefix2}\"name\":"; yield \json_encode($value1->name, \JSON_THROW_ON_ERROR, 510); - yield '}'; - $prefix = ','; + yield "}"; + $prefix1 = ','; } - yield ']'; + yield "]"; } elseif (null === $data) { - yield 'null'; + yield "null"; } else { throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value.', \get_debug_type($data))); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object.php index 7fbc49cf96edc..8919bf27bb8fa 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object.php @@ -5,11 +5,13 @@ */ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { - yield '{"@id":'; + $prefix1 = ''; + yield "{{$prefix1}\"@id\":"; yield \json_encode($data->id, \JSON_THROW_ON_ERROR, 511); - yield ',"name":'; + $prefix1 = ','; + yield "{$prefix1}\"name\":"; yield \json_encode($data->name, \JSON_THROW_ON_ERROR, 511); - yield '}'; + yield "}"; } catch (\JsonException $e) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_dict.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_dict.php index 7e63d0d052596..aa1be64cf9acb 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_dict.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_dict.php @@ -5,19 +5,20 @@ */ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { - yield '{'; - $prefix = ''; + yield "{"; + $prefix1 = ''; foreach ($data as $key1 => $value1) { $key1 = \substr(\json_encode($key1), 1, -1); - yield "{$prefix}\"{$key1}\":"; - yield '{"@id":'; + $prefix2 = ''; + yield "{$prefix1}\"{$key1}\":{{$prefix2}\"@id\":"; yield \json_encode($value1->id, \JSON_THROW_ON_ERROR, 510); - yield ',"name":'; + $prefix2 = ','; + yield "{$prefix2}\"name\":"; yield \json_encode($value1->name, \JSON_THROW_ON_ERROR, 510); - yield '}'; - $prefix = ','; + yield "}"; + $prefix1 = ','; } - yield '}'; + yield "}"; } catch (\JsonException $e) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_in_object.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_in_object.php index 1e04f6b1d8e6a..24f797633d2db 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_in_object.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_in_object.php @@ -5,17 +5,25 @@ */ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { - yield '{"name":'; + $prefix1 = ''; + yield "{{$prefix1}\"name\":"; yield \json_encode($data->name, \JSON_THROW_ON_ERROR, 511); - yield ',"otherDummyOne":{"@id":'; + $prefix1 = ','; + yield "{$prefix1}\"otherDummyOne\":"; + $prefix2 = ''; + yield "{{$prefix2}\"@id\":"; yield \json_encode($data->otherDummyOne->id, \JSON_THROW_ON_ERROR, 510); - yield ',"name":'; + $prefix2 = ','; + yield "{$prefix2}\"name\":"; yield \json_encode($data->otherDummyOne->name, \JSON_THROW_ON_ERROR, 510); - yield '},"otherDummyTwo":{"id":'; + yield "}{$prefix1}\"otherDummyTwo\":"; + $prefix2 = ''; + yield "{{$prefix2}\"id\":"; yield \json_encode($data->otherDummyTwo->id, \JSON_THROW_ON_ERROR, 510); - yield ',"name":'; + $prefix2 = ','; + yield "{$prefix2}\"name\":"; yield \json_encode($data->otherDummyTwo->name, \JSON_THROW_ON_ERROR, 510); - yield '}}'; + yield "}}"; } catch (\JsonException $e) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_iterable.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_iterable.php index b47c07b6d9491..9ada91b74d888 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_iterable.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_iterable.php @@ -5,19 +5,20 @@ */ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { - yield '{'; - $prefix = ''; + yield "{"; + $prefix1 = ''; foreach ($data as $key1 => $value1) { $key1 = is_int($key1) ? $key1 : \substr(\json_encode($key1), 1, -1); - yield "{$prefix}\"{$key1}\":"; - yield '{"id":'; + $prefix2 = ''; + yield "{$prefix1}\"{$key1}\":{{$prefix2}\"id\":"; yield \json_encode($value1->id, \JSON_THROW_ON_ERROR, 510); - yield ',"name":'; + $prefix2 = ','; + yield "{$prefix2}\"name\":"; yield \json_encode($value1->name, \JSON_THROW_ON_ERROR, 510); - yield '}'; - $prefix = ','; + yield "}"; + $prefix1 = ','; } - yield '}'; + yield "}"; } catch (\JsonException $e) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_list.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_list.php index b6da99bae7d72..a14bc5423a14e 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_list.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_list.php @@ -5,18 +5,19 @@ */ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { - yield '['; - $prefix = ''; + yield "["; + $prefix1 = ''; foreach ($data as $value1) { - yield $prefix; - yield '{"@id":'; + $prefix2 = ''; + yield "{$prefix1}{{$prefix2}\"@id\":"; yield \json_encode($value1->id, \JSON_THROW_ON_ERROR, 510); - yield ',"name":'; + $prefix2 = ','; + yield "{$prefix2}\"name\":"; yield \json_encode($value1->name, \JSON_THROW_ON_ERROR, 510); - yield '}'; - $prefix = ','; + yield "}"; + $prefix1 = ','; } - yield ']'; + yield "]"; } catch (\JsonException $e) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_dollar_named_properties.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_dollar_named_properties.php new file mode 100644 index 0000000000000..ff9c70eb028db --- /dev/null +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_dollar_named_properties.php @@ -0,0 +1,18 @@ +foo ? 'true' : 'false'; + $prefix1 = ','; + yield "{$prefix1}\"{\$foo->bar}\":"; + yield $data->bar ? 'true' : 'false'; + yield "}"; + } catch (\JsonException $e) { + throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); + } +}; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_union.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_union.php index cd99dd4630fe7..debcb94c4772a 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_union.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_union.php @@ -5,17 +5,22 @@ */ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { - yield '{"value":'; - if ($data->value instanceof \Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum) { - yield \json_encode($data->value->value, \JSON_THROW_ON_ERROR, 511); - } elseif (null === $data->value) { - yield 'null'; - } elseif (\is_string($data->value)) { - yield \json_encode($data->value, \JSON_THROW_ON_ERROR, 511); - } else { - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value.', \get_debug_type($data->value))); + $prefix1 = ''; + yield "{"; + if (null === $data->value && ($options['include_null_properties'] ?? false)) { + yield "{$prefix1}\"value\":null"; } - yield '}'; + if (null !== $data->value) { + yield "{$prefix1}\"value\":"; + if ($data->value instanceof \Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum) { + yield \json_encode($data->value->value, \JSON_THROW_ON_ERROR, 511); + } elseif (\is_string($data->value)) { + yield \json_encode($data->value, \JSON_THROW_ON_ERROR, 511); + } else { + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value.', \get_debug_type($data->value))); + } + } + yield "}"; } catch (\JsonException $e) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_value_transformer.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_value_transformer.php index 1b6fb0d2c4e10..e960b5e7057d1 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_value_transformer.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/object_with_value_transformer.php @@ -5,15 +5,17 @@ */ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { - yield '{"id":'; + $prefix1 = ''; + yield "{{$prefix1}\"id\":"; yield \json_encode($valueTransformers->get('Symfony\Component\JsonStreamer\Tests\Fixtures\ValueTransformer\DoubleIntAndCastToStringValueTransformer')->transform($data->id, ['_current_object' => $data] + $options), \JSON_THROW_ON_ERROR, 511); - yield ',"active":'; + $prefix1 = ','; + yield "{$prefix1}\"active\":"; yield \json_encode($valueTransformers->get('Symfony\Component\JsonStreamer\Tests\Fixtures\ValueTransformer\BooleanToStringValueTransformer')->transform($data->active, ['_current_object' => $data] + $options), \JSON_THROW_ON_ERROR, 511); - yield ',"name":'; + yield "{$prefix1}\"name\":"; yield \json_encode(strtolower($data->name), \JSON_THROW_ON_ERROR, 511); - yield ',"range":'; + yield "{$prefix1}\"range\":"; yield \json_encode(Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithValueTransformerAttributes::concatRange($data->range, ['_current_object' => $data] + $options), \JSON_THROW_ON_ERROR, 511); - yield '}'; + yield "}"; } catch (\JsonException $e) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException($e->getMessage(), 0, $e); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/self_referencing_object.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/self_referencing_object.php index f55d8045ce4db..5c13782a08e27 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/self_referencing_object.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/self_referencing_object.php @@ -8,15 +8,16 @@ if ($depth >= 512) { throw new \Symfony\Component\JsonStreamer\Exception\NotEncodableValueException('Maximum stack depth exceeded'); } - yield '{"@self":'; - if ($data->self instanceof \Symfony\Component\JsonStreamer\Tests\Fixtures\Model\SelfReferencingDummy) { + $prefix1 = ''; + yield "{"; + if (null === $data->self && ($options['include_null_properties'] ?? false)) { + yield "{$prefix1}\"@self\":null"; + } + if (null !== $data->self) { + yield "{$prefix1}\"@self\":"; yield from $generators['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\SelfReferencingDummy']($data->self, $depth + 1); - } elseif (null === $data->self) { - yield 'null'; - } else { - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value.', \get_debug_type($data->self))); } - yield '}'; + yield "}"; }; try { yield from $generators['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\SelfReferencingDummy']($data, 0); diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/union.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/union.php index 3080920e02c07..f001fce54aebd 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/union.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_writer/union.php @@ -6,20 +6,22 @@ return static function (mixed $data, \Psr\Container\ContainerInterface $valueTransformers, array $options): \Traversable { try { if (\is_array($data)) { - yield '['; - $prefix = ''; + yield "["; + $prefix1 = ''; foreach ($data as $value1) { - yield $prefix; + yield "{$prefix1}"; yield \json_encode($value1->value, \JSON_THROW_ON_ERROR, 511); - $prefix = ','; + $prefix1 = ','; } - yield ']'; + yield "]"; } elseif ($data instanceof \Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes) { - yield '{"@id":'; + $prefix1 = ''; + yield "{{$prefix1}\"@id\":"; yield \json_encode($data->id, \JSON_THROW_ON_ERROR, 511); - yield ',"name":'; + $prefix1 = ','; + yield "{$prefix1}\"name\":"; yield \json_encode($data->name, \JSON_THROW_ON_ERROR, 511); - yield '}'; + yield "}"; } elseif (\is_int($data)) { yield \json_encode($data, \JSON_THROW_ON_ERROR, 512); } else { diff --git a/src/Symfony/Component/JsonStreamer/Tests/JsonStreamWriterTest.php b/src/Symfony/Component/JsonStreamer/Tests/JsonStreamWriterTest.php index 52f40a94087c3..df059fec3e81f 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/JsonStreamWriterTest.php +++ b/src/Symfony/Component/JsonStreamer/Tests/JsonStreamWriterTest.php @@ -18,6 +18,7 @@ use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithArray; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithDateTimes; +use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithDollarNamedProperties; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithGenerics; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNestedArray; @@ -81,7 +82,7 @@ public function testWriteUnion() $this->assertWritten('{"value":"foo"}', $dummy, Type::object(DummyWithUnionProperties::class)); $dummy->value = null; - $this->assertWritten('{"value":null}', $dummy, Type::object(DummyWithUnionProperties::class)); + $this->assertWritten('{}', $dummy, Type::object(DummyWithUnionProperties::class)); } public function testWriteCollection() @@ -238,7 +239,18 @@ public function testWriteObjectWithNullableProperties() { $dummy = new DummyWithNullableProperties(); - $this->assertWritten('{"name":null,"enum":null}', $dummy, Type::object(DummyWithNullableProperties::class)); + $this->assertWritten('{}', $dummy, Type::object(DummyWithNullableProperties::class)); + + $dummy->name = 'name'; + + $this->assertWritten('{"name":"name"}', $dummy, Type::object(DummyWithNullableProperties::class)); + $this->assertWritten('{"name":"name","enum":null}', $dummy, Type::object(DummyWithNullableProperties::class), options: ['include_null_properties' => true]); + + $dummy->name = null; + $dummy->enum = DummyBackedEnum::ONE; + + $this->assertWritten('{"enum":1}', $dummy, Type::object(DummyWithNullableProperties::class)); + $this->assertWritten('{"name":null,"enum":1}', $dummy, Type::object(DummyWithNullableProperties::class), options: ['include_null_properties' => true]); } public function testWriteObjectWithDateTimes() @@ -255,6 +267,11 @@ public function testWriteObjectWithDateTimes() ); } + public function testWriteObjectWithDollarNamedProperties() + { + $this->assertWritten('{"$foo":true,"{$foo->bar}":true}', new DummyWithDollarNamedProperties(), Type::object(DummyWithDollarNamedProperties::class)); + } + /** * @dataProvider throwWhenMaxDepthIsReachedDataProvider */ diff --git a/src/Symfony/Component/JsonStreamer/Tests/Write/StreamWriterGeneratorTest.php b/src/Symfony/Component/JsonStreamer/Tests/Write/StreamWriterGeneratorTest.php index 6f1024303a358..723ba8828812f 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Write/StreamWriterGeneratorTest.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Write/StreamWriterGeneratorTest.php @@ -22,6 +22,7 @@ use Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyEnum; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithArray; +use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithDollarNamedProperties; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNestedArray; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithOtherDummies; @@ -110,6 +111,7 @@ public static function generatedStreamWriterDataProvider(): iterable yield ['object_in_object', Type::object(DummyWithOtherDummies::class)]; yield ['object_with_value_transformer', Type::object(DummyWithValueTransformerAttributes::class)]; yield ['self_referencing_object', Type::object(SelfReferencingDummy::class)]; + yield ['object_with_dollar_named_properties', Type::object(DummyWithDollarNamedProperties::class)]; yield ['union', Type::union(Type::int(), Type::list(Type::enum(DummyBackedEnum::class)), Type::object(DummyWithNameAttributes::class))]; yield ['object_with_union', Type::object(DummyWithUnionProperties::class)]; diff --git a/src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php b/src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php index 3d79403e5dde4..f8280eece6ef4 100644 --- a/src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php +++ b/src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php @@ -23,6 +23,7 @@ use Symfony\Component\JsonStreamer\Exception\RuntimeException; use Symfony\Component\JsonStreamer\Exception\UnexpectedValueException; use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\Type\NullableType; use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; use Symfony\Component\TypeInfo\TypeIdentifier; @@ -159,7 +160,7 @@ private function generateYields(DataModelNodeInterface $dataModelNode, array $op if ($dataModelNode instanceof ScalarNode) { return match (true) { - TypeIdentifier::NULL === $dataModelNode->getType()->getTypeIdentifier() => $this->yieldString('null', $context), + TypeIdentifier::NULL === $dataModelNode->getType()->getTypeIdentifier() => $this->yieldInterpolatedString('null', $context), TypeIdentifier::BOOL === $dataModelNode->getType()->getTypeIdentifier() => $this->yield("$accessor ? 'true' : 'false'", $context), default => $this->yield($this->encode($accessor, $context), $context), }; @@ -191,22 +192,22 @@ private function generateYields(DataModelNodeInterface $dataModelNode, array $op ++$context['depth']; if ($dataModelNode->getType()->isList()) { - $php = $this->yieldString('[', $context) + $php = $this->yieldInterpolatedString('[', $context) .$this->flushYieldBuffer($context) - .$this->line('$prefix = \'\';', $context) + .$this->line('$prefix'.$context['depth'].' = \'\';', $context) .$this->line("foreach ($accessor as ".$dataModelNode->getItemNode()->getAccessor().') {', $context); ++$context['indentation_level']; - $php .= $this->yield('$prefix', $context) + $php .= $this->yieldInterpolatedString('{$prefix'.$context['depth'].'}', $context, false) .$this->generateYields($dataModelNode->getItemNode(), $options, $context) .$this->flushYieldBuffer($context) - .$this->line('$prefix = \',\';', $context); + .$this->line('$prefix'.$context['depth'].' = \',\';', $context); --$context['indentation_level']; return $php .$this->line('}', $context) - .$this->yieldString(']', $context); + .$this->yieldInterpolatedString(']', $context); } $keyAccessor = $dataModelNode->getKeyNode()->getAccessor(); @@ -215,23 +216,23 @@ private function generateYields(DataModelNodeInterface $dataModelNode, array $op ? "$keyAccessor = is_int($keyAccessor) ? $keyAccessor : \substr(\json_encode($keyAccessor), 1, -1);" : "$keyAccessor = \substr(\json_encode($keyAccessor), 1, -1);"; - $php = $this->yieldString('{', $context) + $php = $this->yieldInterpolatedString('{', $context) .$this->flushYieldBuffer($context) - .$this->line('$prefix = \'\';', $context) + .$this->line('$prefix'.$context['depth'].' = \'\';', $context) .$this->line("foreach ($accessor as $keyAccessor => ".$dataModelNode->getItemNode()->getAccessor().') {', $context); ++$context['indentation_level']; $php .= $this->line($escapedKey, $context) - .$this->yield('"{$prefix}\"{'.$keyAccessor.'}\":"', $context) + .$this->yieldInterpolatedString('{$prefix'.$context['depth'].'}"{'.$keyAccessor.'}":', $context, false) .$this->generateYields($dataModelNode->getItemNode(), $options, $context) .$this->flushYieldBuffer($context) - .$this->line('$prefix = \',\';', $context); + .$this->line('$prefix'.$context['depth'].' = \',\';', $context); --$context['indentation_level']; return $php .$this->line('}', $context) - .$this->yieldString('}', $context); + .$this->yieldInterpolatedString('}', $context); } if ($dataModelNode instanceof ObjectNode) { @@ -241,11 +242,13 @@ private function generateYields(DataModelNodeInterface $dataModelNode, array $op return $this->line('yield from $generators[\''.$dataModelNode->getIdentifier().'\']('.$accessor.', '.$depthArgument.');', $context); } - $php = $this->yieldString('{', $context); - $separator = ''; - ++$context['depth']; + $php = $this->line('$prefix'.$context['depth'].' = \'\';', $context) + .$this->yieldInterpolatedString('{', $context); + + $prefixIsCommaForSure = false; + foreach ($dataModelNode->getProperties() as $name => $propertyNode) { $encodedName = json_encode($name); if (false === $encodedName) { @@ -254,17 +257,67 @@ private function generateYields(DataModelNodeInterface $dataModelNode, array $op $encodedName = substr($encodedName, 1, -1); - $php .= $this->yieldString($separator, $context) - .$this->yieldString('"', $context) - .$this->yieldString($encodedName, $context) - .$this->yieldString('":', $context) - .$this->generateYields($propertyNode, $options, $context); + if ($propertyNode instanceof CompositeNode && $propertyNode->getType() instanceof NullableType) { + $nonNullableCompositeParts = array_values(array_filter( + $propertyNode->getNodes(), + static fn (DataModelNodeInterface $n): bool => !($n instanceof ScalarNode && $n->getType()->isIdentifiedBy(TypeIdentifier::NULL)), + )); + + $propertyNode = 1 === \count($nonNullableCompositeParts) + ? $nonNullableCompositeParts[0] + : new CompositeNode($propertyNode->getAccessor(), $nonNullableCompositeParts); + + $php .= $this->flushYieldBuffer($context) + .$this->line('if (null === '.$propertyNode->getAccessor().' && ($options[\'include_null_properties\'] ?? false)) {', $context); + + ++$context['indentation_level']; + + $php .= $this->yieldInterpolatedString('{$prefix'.$context['depth'].'}', $context, false) + .$this->yieldInterpolatedString('"'.$encodedName.'":', $context) + .$this->yieldInterpolatedString('null', $context) + .$this->flushYieldBuffer($context); + + if (!$prefixIsCommaForSure && $name !== array_key_last($dataModelNode->getProperties())) { + $php .= $this->line('$prefix'.$context['depth'].' = \',\';', $context); + } + + --$context['indentation_level']; - $separator = ','; + $php .= $this->line('}', $context) + .$this->flushYieldBuffer($context) + .$this->line('if (null !== '.$propertyNode->getAccessor().') {', $context); + + ++$context['indentation_level']; + + $php .= $this->yieldInterpolatedString('{$prefix'.$context['depth'].'}', $context, false) + .$this->yieldInterpolatedString('"'.$encodedName.'":', $context) + .$this->flushYieldBuffer($context) + .$this->generateYields($propertyNode, $options, $context) + .$this->flushYieldBuffer($context); + + if (!$prefixIsCommaForSure && $name !== array_key_last($dataModelNode->getProperties())) { + $php .= $this->line('$prefix'.$context['depth'].' = \',\';', $context); + } + + --$context['indentation_level']; + + $php .= $this->line('}', $context); + } else { + $php .= $this->yieldInterpolatedString('{$prefix'.$context['depth'].'}', $context, false) + .$this->yieldInterpolatedString('"'.$encodedName.'":', $context) + .$this->flushYieldBuffer($context) + .$this->generateYields($propertyNode, $options, $context); + + if (!$prefixIsCommaForSure && $name !== array_key_last($dataModelNode->getProperties())) { + $php .= $this->line('$prefix'.$context['depth'].' = \',\';', $context); + } + + $prefixIsCommaForSure = true; + } } return $php - .$this->yieldString('}', $context); + .$this->yieldInterpolatedString('}', $context); } throw new LogicException(\sprintf('Unexpected "%s" node', $dataModelNode::class)); @@ -290,9 +343,9 @@ private function yield(string $value, array $context): string /** * @param array $context */ - private function yieldString(string $string, array $context): string + private function yieldInterpolatedString(string $string, array $context, bool $escapeDollar = true): string { - $this->yieldBuffer .= $string; + $this->yieldBuffer .= addcslashes($string, "\\\"\n\r\t\v\e\f".($escapeDollar ? '$' : '')); return ''; } @@ -309,7 +362,7 @@ private function flushYieldBuffer(array $context): string $yieldBuffer = $this->yieldBuffer; $this->yieldBuffer = ''; - return $this->yield("'$yieldBuffer'", $context); + return $this->yield('"'.$yieldBuffer.'"', $context); } private function generateCompositeNodeItemCondition(DataModelNodeInterface $node): string pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy