From d78df129911e91e09774ac233a26d4dccaadc58a Mon Sep 17 00:00:00 2001 From: Sam <40273116+Aweptimum@users.noreply.github.com> Date: Fri, 27 Oct 2023 12:32:34 -0500 Subject: [PATCH 1/4] Test Iterable Data Added a test class, Vector, implementing \Iterator and a corresponding test case in the SerializerTest that demonstrates iterable data doesn't play nice with the serializer. --- tests/Fixtures/TestBundle/Document/Vector.php | 46 +++++++++++++++++++ tests/SerializerTest.php | 13 ++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/Fixtures/TestBundle/Document/Vector.php diff --git a/tests/Fixtures/TestBundle/Document/Vector.php b/tests/Fixtures/TestBundle/Document/Vector.php new file mode 100644 index 0000000..9f1e9a7 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/Vector.php @@ -0,0 +1,46 @@ +array = $array ?? []; + $this->position = 0; + } + + public function getArray(): array + { + return $this->array; + } + + public function current(): mixed + { + return $this->array[$this->key()]; + } + + public function key(): mixed + { + return $this->position; + } + + public function next(): void + { + ++$this->position; + } + + public function rewind(): void + { + $this->position = 0; + } + + public function valid(): bool + { + return isset($this->array[$this->key()]); + } +} diff --git a/tests/SerializerTest.php b/tests/SerializerTest.php index a3aa259..84c159b 100644 --- a/tests/SerializerTest.php +++ b/tests/SerializerTest.php @@ -16,6 +16,7 @@ use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Bar; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Baz; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\ScalarValue; +use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Vector; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\WithMappedType; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Entity\Foo; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Enum\InputMode; @@ -223,4 +224,16 @@ public function testSerializeEnum(): void $this->assertEquals($value, $restoredValue); } + + public function testIterable(): void + { + $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); + + $vector = new Vector([1,2,3,4,5]); + + $data = $serializer->serialize($vector, 'json'); + $restoredVector = $serializer->deserialize($data, '', 'json'); + + $this->assertEquals($vector, $restoredVector); + } } From 522a589f02304c917cc5b30f596dcde8eb726634 Mon Sep 17 00:00:00 2001 From: Arkemlar Date: Mon, 26 Aug 2024 01:33:33 +0300 Subject: [PATCH 2/4] SerializerTrait optimization Add de&normalizer for Traversable types Add VectorNormalizer as an example of how to make custom normalizers for cases when other normalizers fails --- src/Bundle/Resources/config/services.xml | 3 ++ src/Normalizer/TraversableNormalizer.php | 52 ++++++++++++++++++ src/SerializerTrait.php | 53 ++++++++++++++----- .../Fixtures/Normalizer/VectorNormalizer.php | 52 ++++++++++++++++++ .../InjectCustomNormalizerPass.php | 10 +++- .../TestBundle/Document/TraversableValue.php | 43 +++++++++++++++ tests/Fixtures/TestBundle/Document/Vector.php | 18 ++++--- tests/SerializerTest.php | 28 +++++++++- 8 files changed, 234 insertions(+), 25 deletions(-) create mode 100644 src/Normalizer/TraversableNormalizer.php create mode 100644 tests/Fixtures/Normalizer/VectorNormalizer.php create mode 100644 tests/Fixtures/TestBundle/Document/TraversableValue.php diff --git a/src/Bundle/Resources/config/services.xml b/src/Bundle/Resources/config/services.xml index 5cbe5dc..ed7726e 100644 --- a/src/Bundle/Resources/config/services.xml +++ b/src/Bundle/Resources/config/services.xml @@ -12,6 +12,8 @@ + + @@ -29,6 +31,7 @@ + diff --git a/src/Normalizer/TraversableNormalizer.php b/src/Normalizer/TraversableNormalizer.php new file mode 100644 index 0000000..7332f2c --- /dev/null +++ b/src/Normalizer/TraversableNormalizer.php @@ -0,0 +1,52 @@ + true, + ]; + } + + public function normalize(mixed $object, ?string $format = null, array $context = []): array + { + $result = []; + foreach (iterator_to_array($object) as $key => $item) { + $result[$key] = $this->normalizer->normalize($item, $format, $context); + } + return $result; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof Traversable; + } + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): object + { + $result = []; + foreach ($data as $key => $item) { + $result[$key] = $this->denormalizer->denormalize($item, $type, $format, $context); + } + return new $type($result); + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return is_array($data) && is_subclass_of($type, Traversable::class); + } +} diff --git a/src/SerializerTrait.php b/src/SerializerTrait.php index 8c98237..65284dc 100644 --- a/src/SerializerTrait.php +++ b/src/SerializerTrait.php @@ -41,53 +41,78 @@ public function __construct(array $normalizers = [], array $encoders = [], ?Type * @param mixed $data * @param string|null $format * - * @return array|\ArrayObject|bool|float|int|string|null + * @return array|\ArrayObject|scalar|null */ public function normalize($data, $format = null, array $context = []) { - $normalizedData = parent::normalize($data, $format, $context); + if (\is_array($data)) { + foreach ($data as &$datum) { + $datum = $this->normalize($datum, $format, $context); + } + + return $data; + } if (\is_object($data)) { $typeName = \get_class($data); + $data = parent::normalize($data, $format, $context); + if ($this->typeMapper) { $typeName = $this->typeMapper->getTypeByClass($typeName); } $typeData = [self::KEY_TYPE => $typeName]; - $valueData = is_scalar($normalizedData) ? [self::KEY_SCALAR => $normalizedData] : $normalizedData; - $normalizedData = array_merge($typeData, $valueData); + + if (\is_scalar($data)) { + $data = [self::KEY_SCALAR => $data]; + } + if (\is_array($data)) { + foreach ($data as &$datum) { + $datum = $this->normalize($datum, $format, $context); + } + } + + $data = \array_merge($typeData, $data); } - return $normalizedData; + return $data; } /** - * @param $data + * @param null|scalar|array $data + * @param string $type + * @param string|null $format * * @return mixed */ public function denormalize($data, $type, $format = null, array $context = []) { - if (\is_array($data) && (isset($data[self::KEY_TYPE]))) { + if (!\is_array($data)) { + return $data; + } + + if (isset($data[self::KEY_TYPE])) { $keyType = $data[self::KEY_TYPE]; + unset($data[self::KEY_TYPE]); if ($this->typeMapper) { $keyType = $this->typeMapper->getClassByType($keyType); } - unset($data[self::KEY_TYPE]); - $data = $data[self::KEY_SCALAR] ?? $data; - $data = $this->denormalize($data, $keyType, $format, $context); + + if (\is_array($data)) { + foreach ($data as &$datum) { + $datum = $this->denormalize($datum, $keyType, $format, $context); + } + } return parent::denormalize($data, $keyType, $format, $context); } - if (is_iterable($data)) { - $type = ('' === $type) ? 'stdClass' : $type; - - return parent::denormalize($data, $type.'[]', $format, $context); + foreach ($data as &$datum) { + $datum = $this->denormalize($datum, '', $format, $context); } return $data; diff --git a/tests/Fixtures/Normalizer/VectorNormalizer.php b/tests/Fixtures/Normalizer/VectorNormalizer.php new file mode 100644 index 0000000..355d600 --- /dev/null +++ b/tests/Fixtures/Normalizer/VectorNormalizer.php @@ -0,0 +1,52 @@ + true, + ]; + } + + public function normalize(mixed $object, ?string $format = null, array $context = []): array + { + if (!$this->supportsNormalization($object)) { + throw new InvalidArgumentException(sprintf('The object must be an instance of "%s".', Vector::class)); + } + + return ['position' => $object->key(), '[]' => $object->getArray()]; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof Vector; + } + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): object + { + if (!$this->supportsDenormalization($data, $type)) { + throw NotNormalizableValueException::createForUnexpectedDataType('Data expected to be a array of shape {"position": int, "[]": array}.', $data, ['array'], $context['deserialization_path'] ?? null); + } + + return new Vector($data['[]'], $data['position']); + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return is_array($data) && is_a($type, Vector::class, true); + } +} diff --git a/tests/Fixtures/TestBundle/DependencyInjection/InjectCustomNormalizerPass.php b/tests/Fixtures/TestBundle/DependencyInjection/InjectCustomNormalizerPass.php index 77885d5..b7e0c4e 100644 --- a/tests/Fixtures/TestBundle/DependencyInjection/InjectCustomNormalizerPass.php +++ b/tests/Fixtures/TestBundle/DependencyInjection/InjectCustomNormalizerPass.php @@ -9,6 +9,7 @@ namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\DependencyInjection; +use Dunglas\DoctrineJsonOdm\Tests\Fixtures\Normalizer\VectorNormalizer; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -21,9 +22,16 @@ public function process(ContainerBuilder $container): void { $container->setDefinition('dunglas_doctrine_json_odm.normalizer.custom', new Definition(CustomNormalizer::class)); + $vectorDefinition = new Definition(VectorNormalizer::class); + $vectorDefinition->addTag('serializer.normalizer'); + $container->setDefinition(VectorNormalizer::class, $vectorDefinition); + $serializerDefinition = $container->getDefinition('dunglas_doctrine_json_odm.serializer'); $arguments = $serializerDefinition->getArguments(); - $arguments[0] = array_merge([new Reference('dunglas_doctrine_json_odm.normalizer.custom')], $arguments[0]); + $arguments[0] = array_merge([ + new Reference(VectorNormalizer::class), + new Reference('dunglas_doctrine_json_odm.normalizer.custom'), + ], $arguments[0]); $serializerDefinition->setArguments($arguments); } } diff --git a/tests/Fixtures/TestBundle/Document/TraversableValue.php b/tests/Fixtures/TestBundle/Document/TraversableValue.php new file mode 100644 index 0000000..1e2a87e --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/TraversableValue.php @@ -0,0 +1,43 @@ +array = $array; + } + + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->array); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->array[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->array[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + unset($this->array[$offset]); + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->array); + } +} diff --git a/tests/Fixtures/TestBundle/Document/Vector.php b/tests/Fixtures/TestBundle/Document/Vector.php index a6ab516..7e310d5 100644 --- a/tests/Fixtures/TestBundle/Document/Vector.php +++ b/tests/Fixtures/TestBundle/Document/Vector.php @@ -2,16 +2,18 @@ namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document; -class Vector implements \Iterator +use Iterator; + +class Vector implements Iterator { private int $position; private array $array; - public function __construct(array $array = null) + public function __construct(array $array = [], int $position = 0) { - $this->array = $array ?? []; - $this->position = 0; + $this->array = $array; + $this->position = $position; } public function getArray(): array @@ -24,22 +26,22 @@ public function current(): mixed return $this->array[$this->key()]; } - public function key(): mixed + public function key(): mixed { return $this->position; } - public function next(): void + public function next(): void { ++$this->position; } - public function rewind(): void + public function rewind(): void { $this->position = 0; } - public function valid(): bool + public function valid(): bool { return isset($this->array[$this->key()]); } diff --git a/tests/SerializerTest.php b/tests/SerializerTest.php index 5c70309..5e628f9 100644 --- a/tests/SerializerTest.php +++ b/tests/SerializerTest.php @@ -16,6 +16,7 @@ use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Bar; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Baz; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\ScalarValue; +use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\TraversableValue; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Vector; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\WithMappedType; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Entity\Foo; @@ -243,11 +244,34 @@ public function testSerializeUid(): void $this->assertEquals($value, $restoredValue); } - public function testIterable(): void + /** Uses {@link VectorNormalizer} to normalize Vector, otherwise it will be treated as Traversable and fail */ + public function testSerializeObjectWithConfiguredNormalizer(): void { $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); - $vector = new Vector([1,2,3,4,5]); + $attribute = new Attribute(); + $attribute->key = 'foo'; + $attribute->value = 'bar'; + + $vector = new Vector([$attribute, 2, 3, 4, 5]); + $vector->next(); + + $data = $serializer->serialize($vector, 'json'); + $restoredVector = $serializer->deserialize($data, '', 'json'); + + $this->assertEquals($vector, $restoredVector); + } + + /** {@see \Dunglas\DoctrineJsonOdm\Normalizer\TraversableNormalizer} */ + public function testTraversableNormalizer(): void + { + $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); + + $attribute = new Attribute(); + $attribute->key = 'foo'; + $attribute->value = 'bar'; + + $vector = new TraversableValue([$attribute, 'x' => 2, 'y'=>[3], 'z'=>'4', 5, ['' => null]]); $data = $serializer->serialize($vector, 'json'); $restoredVector = $serializer->deserialize($data, '', 'json'); From 6d00da2d915db2d313dd7b1c955b73644efe3232 Mon Sep 17 00:00:00 2001 From: Arkemlar Date: Tue, 24 Dec 2024 17:36:27 +0300 Subject: [PATCH 3/4] Add serializer to context. This allows us to easily reuse current normalizer service inside custom normalizers. This is needed when multiple serializer configs exists in the system. --- src/Serializer.php | 2 ++ src/SerializerTrait.php | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Serializer.php b/src/Serializer.php index c820c5c..15f3477 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -18,6 +18,7 @@ final class Serializer extends BaseSerializer { use SerializerTrait; + private const CONTEXT_SERIALIZER = '#serializer'; private const KEY_TYPE = '#type'; private const KEY_SCALAR = '#scalar'; } @@ -27,6 +28,7 @@ final class Serializer extends BaseSerializer { use TypedSerializerTrait; + private const CONTEXT_SERIALIZER = '#serializer'; private const KEY_TYPE = '#type'; private const KEY_SCALAR = '#scalar'; } diff --git a/src/SerializerTrait.php b/src/SerializerTrait.php index 65284dc..6d469db 100644 --- a/src/SerializerTrait.php +++ b/src/SerializerTrait.php @@ -56,7 +56,7 @@ public function normalize($data, $format = null, array $context = []) if (\is_object($data)) { $typeName = \get_class($data); - $data = parent::normalize($data, $format, $context); + $data = parent::normalize($data, $format, $context + [self::CONTEXT_SERIALIZER => $this]); if ($this->typeMapper) { $typeName = $this->typeMapper->getTypeByClass($typeName); @@ -108,7 +108,7 @@ public function denormalize($data, $type, $format = null, array $context = []) } } - return parent::denormalize($data, $keyType, $format, $context); + return parent::denormalize($data, $keyType, $format, $context + [self::CONTEXT_SERIALIZER => $this]); } foreach ($data as &$datum) { From 77a2010d33d17504276369b95c12b46747dbc54b Mon Sep 17 00:00:00 2001 From: Arkemlar Date: Fri, 21 Feb 2025 23:13:24 +0300 Subject: [PATCH 4/4] Fix reference issues --- src/SerializerTrait.php | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/SerializerTrait.php b/src/SerializerTrait.php index 6d469db..15e8484 100644 --- a/src/SerializerTrait.php +++ b/src/SerializerTrait.php @@ -46,17 +46,18 @@ public function __construct(array $normalizers = [], array $encoders = [], ?Type public function normalize($data, $format = null, array $context = []) { if (\is_array($data)) { - foreach ($data as &$datum) { - $datum = $this->normalize($datum, $format, $context); + $normData = []; + foreach ($data as $idx => $datum) { + $normData[$idx] = $this->normalize($datum, $format, $context); } - return $data; + return $normData; } if (\is_object($data)) { $typeName = \get_class($data); - $data = parent::normalize($data, $format, $context + [self::CONTEXT_SERIALIZER => $this]); + $normData = parent::normalize($data, $format, $context + [self::CONTEXT_SERIALIZER => $this]); if ($this->typeMapper) { $typeName = $this->typeMapper->getTypeByClass($typeName); @@ -64,16 +65,14 @@ public function normalize($data, $format = null, array $context = []) $typeData = [self::KEY_TYPE => $typeName]; - if (\is_scalar($data)) { - $data = [self::KEY_SCALAR => $data]; + if (\is_array($normData) && !isset($normData[self::KEY_TYPE])) { + $normData = $this->normalize($normData, $format, $context); } - if (\is_array($data)) { - foreach ($data as &$datum) { - $datum = $this->normalize($datum, $format, $context); - } + if (\is_scalar($normData)) { + $normData = [self::KEY_SCALAR => $normData]; } - $data = \array_merge($typeData, $data); + return \array_merge($typeData, $normData); } return $data; @@ -103,16 +102,16 @@ public function denormalize($data, $type, $format = null, array $context = []) $data = $data[self::KEY_SCALAR] ?? $data; if (\is_array($data)) { - foreach ($data as &$datum) { - $datum = $this->denormalize($datum, $keyType, $format, $context); + foreach ($data as $idx => $datum) { + $data[$idx] = $this->denormalize($datum, $keyType, $format, $context); } } return parent::denormalize($data, $keyType, $format, $context + [self::CONTEXT_SERIALIZER => $this]); } - foreach ($data as &$datum) { - $datum = $this->denormalize($datum, '', $format, $context); + foreach ($data as $idx => $datum) { + $data[$idx] = $this->denormalize($datum, '', $format, $context); } return $data;